Ontwerppatronen in Java

Een van de onveranderlijke feiten van het leven is dat verandering de onsterfelijke constante is in elke levenscyclus van software - een die je niet kunt ontvluchten. De uitdaging is om zich aan te passen aan deze verandering met minimale latentie en maximale flexibiliteit.

Het goede nieuws is dat iemand waarschijnlijk al uw ontwerpproblemen heeft opgelost en dat hun oplossingen zijn uitgegroeid tot best practices; deze overeengekomen 'best practices' worden 'ontwerppatronen' genoemd. Vandaag gaan we twee populaire ontwerppatronen verkennen en leren hoe goed ontwerp je kan helpen code te schrijven die piepend schoon en uitbreidbaar is.


Het adapterontwerppatroon

Laten we aannemen dat je een bestaand legacy-systeem hebt. U moet nu werken met een nieuwe externe bibliotheek, maar deze bibliotheek heeft een andere API dan de vorige die u gebruikte. Het oude systeem verwacht nu een andere interface dan wat de nieuwe bibliotheek biedt. Je zou natuurlijk dapper (lees, dwaas) genoeg kunnen zijn om na te denken over het veranderen van je oude code om je aan te passen aan de nieuwe interface, maar zoals bij elk legacy-systeem - nooit, nooit.

Adapters voor de redding! Schrijf gewoon een adapter(een nieuwe wrappingklasse) tussen de systemen, die naar clientverzoeken naar de oudere interface luistert, en deze doorstuurt of vertaalt naar oproepen naar de nieuwe interface. Deze conversie kan worden geïmplementeerd met overerving of compositie.

Een goed ontwerp gaat niet alleen over herbruikbaarheid, maar ook over uitbreidbaarheid.

Adapters helpen incompatibele klassen samen te werken door een interface te nemen en deze aan te passen aan een interface die de client kan analyseren.


Adapters in actie

Genoeg chit-chat; laten we aan de slag gaan, zullen we? Ons verouderde softwaresysteem gebruikt het volgende LegacyVideoController interface om het videosysteem te bedienen.

 openbare interface LegacyVideoController / ** * Begint het afspelen na startTimeTicks * vanaf het begin van de video * @param startTimeTicks tijd in milliseconden * / public void startPlayback (long startTimeTicks); ...

De clientcode die deze controller gebruikt, ziet er als volgt uit:

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

Gebruikersvereisten wijzigen!

Er is hier niets nieuws, het gebeurt vrij vaak. Gebruikersvereisten kunnen voortdurend veranderen en ons oude systeem moet nu werken met een nieuwe videocontroller met de volgende interface:

 public interface AdvancedVideoController / ** * Plaatst de regelaar na verloop van tijd * vanaf het begin van de track * @param time time bepaalt hoeveel zoeken vereist is * / public void seek (Time time); / ** * Speelt de track * / openbare ongeldige weergave () af; 

Als gevolg hiervan breekt de clientcode, omdat deze nieuwe interface niet compatibel is.

Adapter slaat de dag op

Hoe gaan we om met deze gewijzigde interface zonder onze oude code te wijzigen? U kent het antwoord nu, nietwaar? We schrijven een eenvoudige adapter om de interface aan te passen om aan te passen aan de bestaande, zoals hieronder:

 public class AdvancedVideoControllerAdapter implementeert LegacyVideoController private AdvancedVideoController advancedVideoController; public AdvancedVideoControllerAdapter (AdvancedVideoController advancedVideoController) this.advancedVideoController = advancedVideoController;  @Override public void startPlayback (long startTimeTicks) // Converteer lang naar DateTime Time startTime = getTime (startTimeTicks); // Pas geavanceerdVideoController.seek aan (startTime); advancedVideoController.play (); 

Deze adapter implementeert de doelinterface, die de klant verwacht, dus het is niet nodig om de clientcode te wijzigen. We stellen de adapter samen met een exemplaar van de adapte-interface.

Door deze "has-a" -relatie kan de adapter het clientverzoek delegeren aan de werkelijke instantie.

Adapters helpen ook bij het ontkoppelen van de code van de klant en de implementatie.

We kunnen nu eenvoudigweg het nieuwe object in deze adapter omwikkelen en klaar zijn, zonder de clientcode te wijzigen omdat het nieuwe object nu is geconverteerd / aangepast aan dezelfde interface.

 AdvancedVideoController advancedController = controllerFactory.createController (); // adapt LegacyVideoController controllerAdapter = new AdvancedVideoControllerAdapter (advancedController); playBackVideo (20, controllerAdapter);

Een adapter kan een eenvoudige doorgang zijn of kan intelligent genoeg zijn om enkele add-ons te bieden, afhankelijk van de complexiteit van de te ondersteunen interface. Op dezelfde manier kan één adapter worden gebruikt om meer dan één object in te pakken als de doelinterface complex is en de nieuwe functionaliteit over twee of meer klassen is verdeeld..

Vergelijking met andere patronen

  • Decorateur : Decorator verandert de interface en wikkelt een object rond door nieuwe verantwoordelijkheid toe te voegen. Aan de andere kant wordt adapter gebruikt om de adapte-interface naar de doelinterface te converteren, die door de klant wordt begrepen.
  • Facade : Facade werkt door een geheel nieuwe interface te definiëren die de complexiteit van eerdere interfaces abstraheert, terwijl de adapter wordt gebruikt om communicatie tussen incompatibele interfaces mogelijk te maken door deze in een andere te converteren.
  • volmacht : Proxy biedt dezelfde interface. Terwijl adapter een andere interface biedt voor zijn onderwerp.
  • Brug : Bridge is van tevoren ontworpen om de abstractie en de implementatie onafhankelijk te laten variëren, maar adapter wordt gebruikt om aan te passen aan een bestaande interface door het verzoek te delegeren aan adaptee.

Het Singleton ontwerppatroon

Hoewel er veel patronen zijn die zich bezighouden met het maken van objecten, valt een specifiek patroon op. Vandaag gaan we een van de meest eenvoudige, nog onbegrepen, inspecteren: het Singleton-patroon.

Zoals de naam al doet vermoeden, is het doel van de singleton om één instantie van de klasse te maken en er globale toegang toe te bieden. Voorbeelden kunnen een toepassingsniveau zijn Cache, een object pool van threads, verbindingen etc. Voor dergelijke entiteiten, een en enige instantie moet voldoende zijn anders bedreigen ze de stabiliteit en verslaan het doel van de applicatie.

Het Singleton-patroon implementeren

Een kale implementatie in Java zou er als volgt uitzien:

 public class ApplicationCache private map attributeMap; // statische instance van een eigen statische ApplicationCache-instantie; // Statische accessor-methode public static ApplicationCache getInstance () if (instance == null) instance == nieuwe ApplicationCache ();  return-instantie;  // private Constructor private ApplicationCache () attributeMap = createCache (); // Initialiseer de cache

In ons voorbeeld bevat de klasse een statisch lid van hetzelfde type als dat van de klasse, dat via een statische methode wordt benaderd. Wij maken gebruik van Luie initialisatie hier, waardoor de initialisatie van de cache wordt vertraagd, totdat deze tijdens runtime daadwerkelijk nodig is. De constructor is ook privé gemaakt, zodat een nieuw exemplaar van deze klasse niet kan worden gemaakt met behulp van de nieuwe operator. Om de cache op te halen, roepen we:

 ApplicationCache cache = ApplicationCache.getInstance (); // gebruik cache om de prestaties te verbeteren

Het werkt prima zolang we te maken hebben met een single-threaded model. Maar het leven zoals we het kennen, is niet zo eenvoudig. In een omgeving met meerdere threads moet u de luie initialisatie synchroniseren of gewoon verwijderen, door de cache te maken zodra de klasse is geladen, door statische blokken te gebruiken of door te initialiseren tijdens het declareren van de cache.

Double Checked Locking

We synchroniseren de luie initialisatie om te zorgen dat de initialisatiecode maar één keer wordt uitgevoerd. Deze code werkt met Java-versie 5.0 en hoger vanwege idiosyncrasies die zijn gekoppeld aan de implementatie van gesynchroniseerd en vluchtig in Java.

 public class ApplicationCache private map attributeMap; / / volatile zodat JVM niet-order schrijft niet gebeuren privé statische vluchtige ApplicationCache-instantie; openbare statische ApplicationCache getInstance () // eenmaal gecontroleerd (instance == null) // gesynchroniseerd op klasseniveau vergrendeld (ApplicationCache.class) // opnieuw gecontroleerd als (instantie == null) instance == nieuwe ApplicationCache ();  return-instantie;  private ApplicationCache () attributeMap = createCache (); // Initialiseer de cache

We maken de instantievariabele veranderlijk, zodat de JVM ervoor zorgt dat er geen off-of-order-schrijfbewerkingen zijn. We voeren ook een dubbele nulcontrole uit (vandaar de naam), bijvoorbeeld tijdens het synchroniseren van de initialisatie, zodat elke reeks van 2 of meer threads de status of het resultaat van het maken van meer dan één exemplaar van de cache niet beschadigt. We hadden in plaats daarvan de hele statische accessor-methode kunnen synchroniseren, maar dat zou een overkill zijn geweest omdat synchronisatie alleen nodig is totdat het object volledig is geïnitialiseerd; nooit meer tijdens het openen ervan.

Geen luie initialisatie

Een eenvoudigere manier zou zijn om de voordelen van luie initialisatie op te heffen, wat ook resulteert in schonere code:

 public class ApplicationCache private map attributeMap; // Geïnitialiseerd tijdens het declareren van statische ApplicationCache-instantie voor de instance = nieuwe ApplicationCache (); public static ApplicationCache getInstance () return instantie;  // private Construcutor private ApplicationCache () attributeMap = createCache (); // Initialiseer de cache

Zodra de klasse wordt geladen en de variabelen worden geïnitialiseerd, roepen we de particuliere constructor aan om het enige exemplaar van de cache te maken. We verliezen de voordelen van het lui initialiseren van de instantie, maar de code is veel schoner. Beide methoden zijn thread-safe en u kunt degene kiezen die geschikt is voor uw projectomgeving.

Beveiliging tegen bezinning en serialisatie

Afhankelijk van uw behoefte, wilt u misschien ook beschermen tegen:

  • Codeer met Reflection API om de particuliere constructor aan te roepen, die kan worden afgehandeld een uitzondering maken van de constructor, in het geval dat het meer dan eens wordt aangeroepen.
  • Evenzo kan het serialiseren en de-serialiseren van de instantie ook resulteren in twee verschillende exemplaren van onze cache, die kunnen worden afgehandeld door de readResolve () methode van de Serialization API

Ontwerppatronen zijn Language Agnostic

De titel van onze tutorial is een beetje misleidend, geef ik toe, omdat ontwerppatronen echt taal-agnostisch zijn. Ze zijn eenvoudigweg een verzameling van de beste ontwerpstrategieën die zijn ontwikkeld om terugkerende problemen bij het ontwerpen van software tegen te gaan. Niets meer niets minder.

Hieronder ziet u bijvoorbeeld snel hoe we een kunnen implementeren eenling in Javascript. De intentie blijft hetzelfde: controle over de creatie van het object en behoud van een globaal toegangspunt, maar de implementatie verschilt met de constructen en semantiek van elke taal.

 var applicationCache = function () // Privédingen var-instantie; function initCache () return proxyUrl: "/bin/getCache.json", cachePurgeTime: 5000, permissies: lees: "everyone", write: "admin";  // Public return getInstance: function () if (! Instance) instance = initCache (); terugkeer instantie; , purgeCache: function () instance = null; ; ;

Om een ​​ander voorbeeld te citeren, maakt jQuery ook veel gebruik van het ontwerppatroon van de gevel, waardoor de complexiteit van een subsysteem wordt weggenomen en een eenvoudigere interface aan de gebruiker wordt getoond.


Slotopmerkingen

Niet elk probleem vereist het gebruik van een specifiek ontwerppatroon

Een waarschuwing is nodig: niet overmatig gebruik! Niet elk probleem vereist het gebruik van een specifiek ontwerppatroon. U moet de situatie zorgvuldig analyseren voordat u zich op een patroon kunt vestigen. Het leren van ontwerppatronen helpt ook bij het begrijpen van andere bibliotheken zoals jQuery, Spring enz. Die veel van dergelijke patronen intensief gebruiken.

Ik hoop dat je na het lezen van dit artikel een stap dichterbij je begrip van ontwerppatronen kunt komen. Als u vragen heeft of een ander ontwerppatroon wilt leren, kunt u me dit laten weten via de onderstaande opmerkingen en ik zal mijn best doen om uw vragen te beantwoorden!