Design Patterns în Java

Unul dintre faptele neschimbate ale vieții este că schimbarea este constanta nesfârșită în fiecare ciclu de viață al software-ului - unul din care nu puteți fugi. Provocarea este de a se adapta la această schimbare cu o latență minimă și flexibilitate maximă.

Vestea bună este că probabil că cineva ți-a rezolvat deja problemele de proiectare, iar soluțiile lor au evoluat în cele mai bune practici; aceste bune practici convenite sunt denumite "modele de design". Astăzi, vom explora două modele populare de design și vom afla cum designul bun vă poate ajuta să scrieți un cod care este curat și extensibil.


Modelul de adaptare a adaptorului

Să presupunem că aveți un sistem de moștenire existent. Acum aveți sarcina de a face ca lucrul să funcționeze cu o nouă bibliotecă terță parte, dar această bibliotecă are un API diferit față de ultima pe care o utilizați. Sistemul vechi așteaptă acum o interfață diferită de ceea ce oferă noua bibliotecă. Bineînțeles, ați putea fi curajoși (citiți, nebuni) suficient pentru a vă gândi la schimbarea codului vechi pentru a vă adapta la noua interfață, dar la fel ca la fiecare sistem vechi - niciodată.

Adaptoare la salvare! Pur și simplu scrie un adaptor(o nouă clasă de împachetare) între sistemele care asculta cererile clienților la interfața mai veche și redirecționează sau le traduce în apeluri către noua interfață. Această conversie poate fi implementată fie cu moștenire, fie cu o compoziție.

Designul excelent nu este doar despre reutilizabilitate, ci despre extensibilitate.

Adaptorii ajută clase incompatibile să colaboreze prin a lua o interfață și să o adapteze la una pe care clientul o poate analiza.


Adaptoare în acțiune

Destul de chit-chat; Să mergem la afaceri, nu? Sistemul nostru de software vechi utilizează următoarele LegacyVideoController interfață pentru controlul sistemului video.

 interfață publică LegacyVideoController Începe redarea după startTimeTicks * de la începutul videoclipului * @param startTimeTicks timp în milisecunde * / public void startPlayback (long startTimeTicks); ...

Codul clientului care utilizează acest controler arată astfel:

 void public playBackVideo (timpToStart, controller LegacyVideoController) if (controller! = null) controller.startPlayback (timeToStart); 

Modificarea cerințelor utilizatorilor!

Nu este nimic nou aici, într-adevăr - se întâmplă destul de frecvent. Cerințele utilizatorilor sunt predispuse să se schimbe tot timpul, iar sistemul nostru vechi trebuie acum să funcționeze cu un nou controler video, având următoarea interfață:

 interfață publică AdvancedVideoController / ** * localizează capul controlerului după timp * de la începutul pistei * timpul @param time definește numărul de căutări necesare * / public void seek (Time time); / ** * Redă piesa * / void public play (); 

Ca urmare, codul clientului se rupe, deoarece această nouă interfață nu este compatibilă.

Adaptorul salvează Ziua

Deci, cum să ne ocupăm de această interfață modificată fără a schimba codul nostru vechi? Știi răspunsul acum, nu-i așa? Scriem un adaptor simplu pentru a modifica interfața sa pentru a se adapta la cea existentă, după cum urmează:

 public class AdvancedVideoControllerAdapter implementează LegacyVideoController private AdvancedVideoController advancedVideoController; publicul AdvancedVideoControllerAdapter (AdvancedVideoController advancedVideoController) this.advancedVideoController = advancedVideoController;  @Override public void startPlayback (lung startTimeTicks) // Convertiți lung în TimeTime Time startTime = getTime (startTimeTicks); // Adaptați avansateVideoController.seek (startTime); advancedVideoController.play (); 

Acest adaptor implementează interfața țintă, pe care clientul o așteaptă, astfel încât nu este nevoie să modificați codul clientului. Componem adaptorul cu o instanță a interfeței de adaptare.

Această relație "a-a" permite adaptorului să delege solicitarea clientului la instanța reală.

Adaptorii ajută, de asemenea, la decuplarea codului clientului și la implementarea acestuia.

Putem acum să înfășurăm noul obiect din acest adaptor și să facem acest lucru fără a face modificări în codul clientului, deoarece obiectul nou este acum convertit / adaptat la aceeași interfață.

 AdvancedVideoController avansatăController = controllerFactory.createController (); // adaptați LegacyVideoController controllerAdapter = nou AdvancedVideoControllerAdapter (advancedController); playBackVideo (20, controllerAdapter);

Un adaptor poate fi o trecere simplă sau poate fi suficient de inteligent pentru a furniza unele add-on-uri, în funcție de complexitatea interfeței care trebuie suportată. În mod similar, un adaptor poate fi utilizat pentru a înfășura mai multe obiecte dacă interfața țintă este complexă și noua funcționalitate a fost împărțită în două sau mai multe clase.

Comparație cu alte modele

  • Decorator : Decoratorul schimbă interfața și împachetează un obiect prin adăugarea unei noi responsabilități. Pe de altă parte, adaptorul este utilizat pentru a converti interfața adaptabilă la interfața țintă, înțeleasă de client.
  • Faţadă : Lucrările de fațadă prin definirea unei interfețe complet noi care abstracționează complexitatea interfețelor anterioare, în timp ce adaptorul este utilizat pentru a permite comunicarea între interfețele incompatibile prin conversia unul în altul.
  • împuternicire : Proxy oferă aceeași interfață. În timp ce adaptorul oferă o interfață diferită subiectului său.
  • Pod : Bridge este proiectat în avans pentru a permite abstractizarea și implementarea să varieze în mod independent, dar adaptorul este utilizat pentru a se adapta la o interfață existentă prin delegarea cererii de adaptare.

Modelul de design Singleton

Deși există multe modele care se ocupă cu crearea obiectelor, un model specific se evidențiază. Astăzi vom inspecta unul dintre cele mai simple, dar neînțelese: modelul Singleton.

După cum sugerează și numele, scopul singleton este de a crea o singură instanță a clasei și de a oferi acces global la ea. Exemplele pot fi un nivel de aplicare ascunzătoare, un grup de obiecte de fire, conexiuni etc. Pentru astfel de entități, singura instanță trebuie să fie suficientă, altfel amenință stabilitatea și învinge scopul aplicației.

Implementarea modelului Singleton

O implementare cu oase goale în Java ar arata ca:

 public class ApplicationCache hartă privată attributeMap; // instanță statică privată Static ApplicationCache instanță; // Static accesor method public static ApplicationCache getInstance () if (instanță == null) instance == new ApplicationCache ();  retur;  // private Constructor private ApplicationCache () attributeMap = createCache (); // Initializeaza cache-ul

În exemplul nostru, clasa deține un membru static de același tip ca cel al clasei, care este accesat printr-o metodă statică. Noi facem uz de Inițierea leneșilor aici, întârziind inițializarea cache-ului, până când este efectiv necesar în timpul rulării. Constructorul este, de asemenea, făcut privat, astfel încât o nouă instanță a acestei clase să nu poată fi creată utilizând nou operator. Pentru a prelua memoria cache, invocăm:

 ApplicationCache cache = ApplicationCache.getInstance (); // utilizați cache pentru a îmbunătăți performanța

Funcționează perfect, atâta timp cât avem de-a face cu un model cu un singur filet. Dar viața, așa cum o știm, nu este atât de simplă. Într-un mediu cu mai multe fire, trebuie să sincronizați inițializarea leneș sau să o eliminați, creând memoria cache de îndată ce se încarcă clasa, fie prin utilizarea blocurilor statice, fie prin inițializarea în timp ce se declară cache-ul.

Verificați blocarea dublă

Sincronizăm inițializarea leneșă pentru a ne asigura că codul de inițializare este rulat o singură dată. Acest cod funcționează cu versiunea Java 5.0 și mai mult datorită idiosincrasiilor asociate implementării sincronizate și volatil în Java.

 public class ApplicationCache hartă privată attributeMap; // volatile astfel încât JVM out-of-order scrie nu se întâmplă privat static volatilă ApplicationCache exemplu; public Static AplicațieCache getInstance () // Verificat o dată dacă (exemplu == null) // Sincronizat la blocarea la nivel de clasă sincronizat (ApplicationCache.class) // Checked din nou dacă (instance == null) instance == new ApplicationCache ();  retur;  aplicație privată ApplicationCache () attributeMap = createCache (); // Initializeaza cache-ul

Noi facem variabila de instanță volatilă astfel încât JVM să împiedice scrierea în afara ordinului pentru aceasta. De asemenea, efectuăm un control dublu (de aici numele), de exemplu, în timpul sincronizării inițializării, astfel încât orice secvență de 2 sau mai multe fire să nu corupe statul sau să ducă la crearea a mai mult de o instanță a cache-ului. S-ar fi putut sincroniza întreaga metodă de acces static, dar ar fi fost o depășire, deoarece sincronizarea este necesară doar până când obiectul este complet inițializat; niciodată din nou în timp ce îl accesați.

Nicio inițiere blândă

O modalitate mai ușoară ar fi să elimini avantajele inițializării lenece, rezultând și un cod mai curat:

 public class ApplicationCache hartă privată attributeMap; // inițializat în timp ce declarația privată statică ApplicationCache instanță = new ApplicationCache (); statică publică ApplicationCache getInstance () instanță retur;  / / private Constructor private ApplicationCache () attributeMap = createCache (); // Initializeaza cache-ul

Imediat ce încărcarea clasei și variabilele sunt inițializate, invocăm constructorul privat pentru a crea singura instanță a cache-ului. Pierdem beneficiile inițializării inițiale a instanței, dar codul este mult mai curat. Ambele metode sunt sigure pentru fir și puteți alege unul potrivit pentru mediul dvs. de proiect.

Protejarea împotriva reflecției și a serializării

În funcție de cerințele dvs., ați putea dori, de asemenea, să vă protejați împotriva:

  • Cod folosind API-ul Reflection pentru a invoca constructorul privat, care poate fi tratat de către aruncarea unei excepții de la constructor, în cazul în care se numește mai mult de o dată.
  • În mod similar, serializarea și de-serializarea instanței ar putea avea ca rezultat două cazuri diferite ale cache-ului nostru, care pot fi tratate prin suprimarea readResolve () metoda de la Serialization API

Modelele de proiectare sunt Agnosticul lingvistic

Titlul tutorialului nostru este puțin înșelător, recunosc, deoarece modelele de design sunt cu adevărat limbaj-agnostic. Ele sunt pur și simplu o colecție de cele mai bune strategii de design dezvoltate pentru a contracara problemele recurente cu care se confruntă în designul de software. Nimic mai mult, nimic mai puțin.

De exemplu, mai jos este o privire rapidă asupra modului în care am putea implementa o Singleton în Javascript. Intenția rămâne aceeași: controlul creării obiectului și menținerea unui punct de acces global, dar implementarea diferă cu constructurile și semantica fiecărui limbaj.

 var applicationCache = funcția () // Private stuff var instance; funcția initCache () return proxyUrl: "/bin/getCache.json", cachePurgeTime: 5000, permisiuni: read: "toată lumea", scrie: "admin";  // Returnarea publicului getInstance: function () if (! Instance) instance = initCache (); instanță retur; , purgeCache: funcție () instanță = null; ; ;

Pentru a cita un alt exemplu, jQuery folosește de asemenea modelul de proiectare a fațadelor, abstractizând complexitatea unui subsistem și expunând o interfață mai simplă utilizatorului.


Remarci finale

Nu orice problemă necesită utilizarea unui model specific de design

Un cuvânt de precauție este necesar: nu excesiv! Nu orice problemă necesită utilizarea unui model specific de design. Trebuie să analizați cu atenție situația înainte de a vă stabili un model. Modelele de design de învățare ajută de asemenea la înțelegerea altor biblioteci, cum ar fi jQuery, Spring etc., care folosesc în mod greu multe astfel de modele.

Sper că, după ce ați citit acest articol, veți reuși să faceți un pas mai aproape de înțelegerea modelelor de design. Dacă aveți întrebări sau doriți să aflați un model de design suplimentar, anunțați-ne în comentariile de mai jos și voi face tot posibilul pentru a vă răspunde la întrebări!

Cod