de Abdul Kadir

Modelul Strategiei explicat folosind Java

Modelul Strategiei explicat folosind Java

În această postare, voi vorbi despre unul dintre modelele populare de design – modelul Strategiei. Dacă nu sunteți deja conștienți, modelele de proiectare sunt o grămadă de principii de programare orientate pe obiecte create de nume notabile din industria software-ului, adesea denumite Gang of Four (GoF). Aceste modele de proiectare au avut un impact imens în ecosistemul software și sunt utilizate până în prezent pentru a rezolva problemele comune cu care se confruntă programarea orientată pe obiecte.

Să definim formal modelul de strategie:

Modelul Strategiei definește o familie de algoritmi, încapsulează pe fiecare și le face interschimbabile. Strategia permite algoritmului să varieze independent de clienții care îl utilizează

Bine cu asta din drum, să ne scufundăm într-un cod pentru a înțelege ce înseamnă cu adevărat aceste cuvinte. Vom lua un exemplu cu o posibilă capcană și apoi vom aplica modelul de strategie pentru a vedea cum depășește problema.

Vă voi arăta cum să creați un program de simulator de câine drogat pentru a învăța modelul Strategiei. Iată cum vor arăta clasele noastre: o superclasă „Dog” cu comportamente comune și apoi clase concrete de Dog create prin subclasarea clasei Dog.

Iată cum arată codul

public abstract class Dog {
public abstract void display(); //different dogs have different looks!

public void eat(){}
public void bark(){} 
// Other dog-like methods
...
}

Metoda display () este făcută abstractă, deoarece câinii diferiți au un aspect diferit. Toate celelalte subclase vor moșteni comportamentele de mâncare și scoarță sau le vor suprascrie cu propria lor implementare. Până acum, bine!

ad-banner

Acum, dacă ai vrea să adaugi un comportament nou? Să presupunem că aveți nevoie de un câine robot rece, care poate face tot felul de trucuri. Nu este o problemă, trebuie doar să adăugăm o metodă performTricks () în superclasa noastră de câini și suntem bine să mergem.

Dar așteaptă un minut … Un câine robot nu ar trebui să poată mânca nu? Obiectele neînsuflețite nu pot mânca, desigur. Bine, cum rezolvăm această problemă atunci? Ei bine, putem anula metoda eat () pentru a nu face nimic și funcționează foarte bine!

public class RobotDog extends Dog {
@override
public void eat(){} // Do nothing

}

Bine făcut! Acum, câinii robot nu pot mânca, pot să latre sau să facă trucuri. Dar câinii de cauciuc? Nu pot mânca și nu pot face trucuri. Și câinii din lemn nu pot mânca, latra sau executa trucuri. Nu putem întotdeauna să suprascrieți metodele pentru a nu face nimic, nu este curat și se simte pur și simplu ciudat. Imaginați-vă că faceți acest lucru într-un proiect a cărui specificație de proiectare se modifică la fiecare câteva luni. Al nostru este doar un exemplu naiv, dar îți iese ideea. Deci, trebuie să găsim o modalitate mai curată de a rezolva această problemă.

Poate interfața să ne rezolve problema?

Ce zici de interfețe? Să vedem dacă ne pot rezolva problema. Bine, așa că creăm o interfață CanEat și o interfață CanBark:

interface CanEat {
public void eat();

}

interface CanBark {
public void bark();

}

Acum am eliminat metodele de scoarță () și eat () din superclasa Dog și le-am adăugat la interfețele respective. Astfel, numai câinii care pot latra vor implementa interfața CanBark, iar câinii care pot mânca vor implementa interfața CanEat. Acum, nu mai suntem îngrijorați de câinele care moștenesc un comportament pe care nu ar trebui, problema noastră este rezolvată … sau nu?

Ce se întâmplă când trebuie să schimbăm comportamentul alimentar al câinilor? Să presupunem că de acum încolo fiecare câine trebuie să includă o cantitate de proteine ​​împreună cu masa lor. Acum trebuie să modificați metoda eat () a tuturor subclaselor Dog. Dacă există 50 de astfel de clase, oh, groază!

Deci, interfețele rezolvă doar parțial problema noastră de a face câinii doar ceea ce sunt capabili să facă – dar creează cu totul o altă problemă. Interfețele nu au niciun cod de implementare, deci există zero cod de reutilizare și potențial pentru o mulțime de coduri duplicat. Cum rezolvăm acest lucru? Modelul de strategie vine în salvare!

Modelul strategiei

Așa că vom face acest lucru pas cu pas. Înainte de a continua, permiteți-mi să vă prezint un principiu de proiectare:

Identificați părțile din program care variază și separați-le de ceea ce rămâne la fel.

Este de fapt foarte simplu – principiul afirmă să separe și să „încapsuleze” orice se schimbă frecvent, astfel încât tot codul care se schimbă să trăiască într-un singur loc. În acest fel, codul care se modifică nu va avea niciun efect asupra restului programului, iar aplicația noastră este mai flexibilă și mai robustă.

În cazul nostru, comportamentul „scoarță” și „mâncare” pot fi scoase din clasa câinilor și pot fi încapsulate în altă parte. Știm că aceste comportamente variază în funcție de câini și trebuie să primească o clasă separată.

Vom crea două seturi de clase în afară de clasa Dog, una pentru definirea comportamentului alimentar și una pentru comportamentul de lătrat. Vom folosi interfețe pentru a reprezenta comportamentul, cum ar fi „EatBehavior” și „BarkBehavior”, iar clasa de comportament concret va implementa aceste interfețe. Deci, clasa Dog nu mai implementează interfața. Creăm clase separate a căror singură sarcină este reprezentarea comportamentului specific!

Așa arată interfața EatBehavior

interface EatBehavior {
public void eat();
}

Și BarkBehavior

interface BarkBehavior {
public void bark();
}

Toate clasele care reprezintă aceste comportamente vor implementa interfața respectivă.

Clase concrete pentru BarkBehavior

public class PlayfulBark implements BarkBehavior {
 @override
 public void bark(){
 System.out.println("Bark! Bark!");
 }
}

public class Growl implements BarkBehavior {
 @override
 public void bark(){
  System.out.println("This is a growl");
 }
 
public class MuteBark implements BarkBehavior {
 @override
 public void bark(){
  System.out.println("This is a mute bark");
 }

Clase concrete pentru EatBehavior

public class NormalDiet implements EatBehavior {
@override
 public void eat(){
   System.out.println("This is a normal diet");
 }
}

public class ProteinDiet implements EatBehavior {
@override
 public void eat(){
   System.out.println("This is a protein diet");
 }
}

Acum, în timp ce realizăm implementări concrete prin subclasarea superclasei „Dog”, vrem în mod firesc să putem atribui comportamentele în mod dinamic instanțelor câinilor. La urma urmei, inflexibilitatea codului anterior a cauzat problema. Putem defini metode setter în subclasa Dog care ne va permite să stabilim comportamente diferite în timpul rulării.

Aceasta ne aduce la un alt principiu de proiectare:

Programează pe o interfață și nu pe o implementare.

Ceea ce înseamnă acest lucru este că, în loc să folosim clasele concrete, folosim variabile care sunt supertipuri ale acestor clase. Cu alte cuvinte, folosim variabile de tip EatBehavior și BarkBehavior și atribuim aceste variabile obiecte ale claselor care implementează aceste comportamente. În acest fel, clasele de câini nu trebuie să aibă nicio informație despre tipurile reale de obiecte ale acelor variabile!

Pentru a clarifica conceptul, iată un exemplu care diferențiază cele două moduri – Luați în considerare o clasă Animală abstractă care are două implementări concrete, Dog and Cat.

Programarea pentru o implementare ar fi:

Dog d = new Dog();
d.bark();

Iată cum arată programarea unei interfețe:

Animal animal = new Dog();
animal.animalSound();

Aici, știm că animalul conține o instanță de „câine”, dar putem folosi această referință polimorfă peste tot în codul nostru. Tot ceea ce ne pasă este că instanța animalului este capabilă să răspundă la metoda animalSound () și se apelează metoda adecvată, în funcție de obiectul atribuit.

A fost o mulțime de luat în calcul. Fără alte explicații, să vedem cum arată superclasa „Câinelui” nostru acum:

public abstract class Dog {
EatBehavior eatBehavior;
BarkBehaviour barkBehavior;

public Dog(){}

public void doBark() {
 barkBehavior.bark();
 }
 
public void doEat() {
eatBehavior.eat();
 }
}

Acordați o atenție deosebită metodelor acestei clase. Clasa Câinilor „delegă” acum sarcina de a mânca și lătrat în loc să o implementeze singură sau să o moștenească (subclasă). În metoda doBark () numim pur și simplu metoda bark () pe obiectul la care se face referire prin barkBehavior. Acum, nu ne pasă de tipul real al obiectului, ci ne interesează doar dacă știe să latre!

Acum, momentul adevărului, să creăm un câine concret!

public class Labrador extends Dog {

public Labrador(){
  barkBehavior = new PlayfulBark();
  eatBehavior = new NormalDiet();
 }
 
public void display(){
  System.out.println("I'm a playful Labrador");
 } 
...
}

Ce se întâmplă în constructorul clasei Labrador? atribuim instanțelor concrete supertipului (amintiți-vă că tipurile de interfață sunt moștenite din superclasa Dog). Acum, când apelăm doEat () pe instanța Labrador, responsabilitatea este predată clasei ProteinDiet și execută metoda eat ().

Modelul de strategie în acțiune

Bine, să vedem asta în acțiune. A sosit timpul să rulăm programul nostru de simulare pentru câini de droguri!

public class DogSimulatorApp {
 public static void main(String[] args) {
  Dog lab = new Labrador();
  
  lab.doEat(); // Prints "This is a normal diet"
  lab.doBark(); // "Bark! Bark!" 
 }
}

Cum putem îmbunătăți acest program? Prin adăugarea de flexibilitate! Să adăugăm metode de setare în clasa Dog pentru a putea schimba comportamentele în timpul rulării. Să adăugăm încă două metode superclasei de câini:

public void setEatBehavior(EatBehavior eb){
 eatBehavior = eb;
}

public void setBarkBehavior(BarkBehavior bb){
 barkBehavior = bb;
}

Acum ne putem modifica programul și putem alege orice comportament ne place în timpul rulării!

public class DogSimulatorApp {
 public static void main(String[] args){
  Dog lab = new Labrador();
  
  lab.doEat(); // This is a normal diet
  lab.setEatBehavior(new ProteinDiet());
  lab.doEat(); // This is a protein diet
  
lab.doBark(); // Bark! Bark!
 }
}

Să ne uităm la imaginea de ansamblu:

Modelul Strategiei explicat folosind Java
Diagrama clasei

Avem superclasa Dog și clasa „Labrador”, care este o subclasă de Dog. Apoi avem familia de algoritmi (Comportamente) „încapsulate” cu tipurile lor de comportament respective.

Uitați-vă la definiția formală pe care am dat-o la început: algoritmii nu sunt altceva decât interfețele de comportament. Acum pot fi utilizate nu numai în acest program, dar și alte programe pot face uz de el. Observați relațiile dintre clasele din diagramă. Relațiile IS-A și HAS-A pot fi deduse din diagramă.

Asta e! Sper că ați obținut o imagine de ansamblu asupra modelului Strategiei. Modelul Strategiei este extrem de util atunci când aveți anumite comportamente în aplicație care se schimbă constant.

Acest lucru ne aduce la sfârșitul implementării Java. Vă mulțumesc mult că ați rămas cu mine până acum! Dacă sunteți interesat să aflați despre versiunea Kotlin, rămâneți la curent cu următoarea postare. Vorbesc despre caracteristicile lingvistice interesante și despre cum putem reduce toate codurile de mai sus într-un singur fișier Kotlin 🙂

PS

Am citit Cartea Head First Design Patterns și cea mai mare parte a acestei postări este inspirată de conținutul său. Aș recomanda această carte oricui este în căutarea unei introduceri blânde a modelelor de design.