Kolme tüüpi kujundusmustrid, mida kõik arendajad peaksid teadma (koos koodinäidetega)

Mis on kujundusmuster?

Kujundusmustrid on disainitaseme lahendused korduvate probleemide jaoks, millega me tarkvarainsenerid sageli kokku puutume. See pole kood - ma kordan,KOOD . See on nagu kirjeldus nende probleemide lahendamiseks ja lahenduse kujundamiseks.

Nende mustrite kasutamist peetakse heaks tavaks, kuna lahenduse kujundus on üsna hästi läbi proovitud, mille tulemuseks on lõpliku koodi parem loetavus. Kujundusmustrid on üsna sageli loodud ja neid kasutavad OOP-keeled, näiteks Java, kuhu kirjutatakse enamik siinsetest näidetest.

Kujundusmustrite tüübid

Praegu on umbes 26 mustrit (ma arvan, et ma ei tee neid kõiki ...).

Need 26 saab jagada kolme tüüpi:

1. Loominguline: need mustrid on mõeldud klassi koheseks muutmiseks. Need võivad olla kas klassi loomise mustrid või objekti loomise mustrid.

2. Struktuurne: need mustrid on kujundatud klassi struktuuri ja koosseisu silmas pidades. Enamiku nende mustrite peamine eesmärk on suurendada kaasatud klassi (de) funktsionaalsust, muutmata palju selle koosseisu.

3. Käitumine: need mustrid kujundatakse sõltuvalt sellest, kuidas üks klass teistega suhtleb.

Selles postituses tutvume iga salastatud tüübi jaoks ühe põhilise kujundusmustriga.

1. tüüp: loominguline - Singletoni kujundusmuster

Singletoni kujundusmuster on loomemuster, mille eesmärk on luua klassist ainult üks eksemplar ja pakkuda sellele objektile ainult üks globaalne pääsupunkt. Üks sellise klassi Java-s levinud näide on Kalender, kus te ei saa selle klassi eksemplari teha. getInstance()Kasutatava objekti saamiseks kasutab ta ka oma meetodit.

Üksikkujunduse mustrit kasutav klass sisaldab:

  1. Privaatne staatiline muutuja, millel on klassi ainus eksemplar.
  2. Erakonstruktor, seega ei saa seda kusagil mujal instantsida.
  3. Avalik staatiline meetod klassi ühe eksemplari tagastamiseks.

Singletoni disaini on palju erinevaid rakendusi. Täna vaatan läbi rakendused;

1. Innukas kohestumine

2. Laisk Instantiation

3. Niidikindel kiirmenetlus

Agar kobras

public class EagerSingleton { // create an instance of the class. private static EagerSingleton instance = new EagerSingleton(); // private constructor, so it cannot be instantiated outside this class. private EagerSingleton() { } // get the only instance of the object created. public static EagerSingleton getInstance() { return instance; } }

Seda tüüpi kiirendamine toimub klassi laadimise ajal, kuna muutuja eksemplari kiirendamine toimub väljaspool mis tahes meetodit. See kujutab endast kopsakat puudust, kui seda klassi kliendirakendus üldse ei kasuta. Kui seda klassi ei kasutata, on situatsiooniplaan Lazy Instantiation.

Laiskepäevad

Eespool toodud rakendusest pole palju erinevusi. Peamised erinevused seisnevad selles, et staatiline muutuja kuulutatakse algselt nulliks ja seda saab getInstance()meetodi raames instantsida ainult siis ja ainult siis, kui eksemplari muutuja jääb kontrollimise ajal nulliks.

public class LazySingleton { // initialize the instance as null. private static LazySingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private LazySingleton() { } // check if the instance is null, and if so, create the object. public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }

See lahendab ühe probleemi, kuid teine ​​on endiselt olemas. Mis siis, kui kaks erinevat klienti pääsevad Singletoni klassi korraga, millisekundini? Noh, nad kontrollivad, kas eksemplar on samal ajal tühine, ja leiavad, et see on tõsi, ning loovad kahe kliendi igale taotlusele kaks klassi eksemplari. Selle parandamiseks tuleb rakendada Thread Safe instantiation.

(Lõng) Ohutus on võti

Java-s kasutatakse sünkroniseeritud märksõna meetoditel või objektidel lõime ohutuse rakendamiseks, nii et ainult üks lõim pääseb korraga konkreetsele ressursile juurde. Klassi eksemplar paigutatakse sünkroonitud plokki, nii et meetodile pääseb kindlal ajal juurde vaid üks klient.

public class ThreadSafeSingleton { // initialize the instance as null. private static ThreadSafeSingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private ThreadSafeSingleton() { } // check if the instance is null, within a synchronized block. If so, create the object public static ThreadSafeSingleton getInstance() { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } return instance; } }

Sünkroniseeritud meetodi üldkulud on suured ja vähendavad kogu toimingu jõudlust.

Näiteks kui eksemplari muutuja on juba instantsitud, käivitatakse iga kord, kui mõni klient getInstance()meetodile juurde pääseb , synchronizedmeetod ja jõudlus langeb. See juhtub lihtsalt selleks, et kontrollida, kas instancemuutujate väärtus on null. Kui ta leiab, et on, jätab ta meetodi.

Selle üldkulude vähendamiseks kasutatakse topeltlukustust. Kontrolli kasutatakse ka enne synchronizedmeetodit ja kui väärtus on üksi null, kas synchronizedmeetod töötab.

// double locking is used to reduce the overhead of the synchronized method public static ThreadSafeSingleton getInstanceDoubleLocking() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }

Nüüd järgmisele liigitusele.

2. tüüp: struktuurne - dekoraatori kujundusmuster

Annan teile väikese stsenaariumi, et anda parem kontekst, miks ja kus peaksite dekoraatori mustrit kasutama.

Oletame, et teil on kohvik ja nagu iga algaja, alustate kõigest kahte sorti tavalist kohvi, maja segu ja tumedat rösti. Teie arveldussüsteemis oli erinevate kohvisegude jaoks üks klass, mis pärib jookide abstraktse klassi. Inimesed hakkavad tegelikult käima ja joovad teie imelist (ehkki kibedat?) Kohvi. Siis on kohvikohvikud, mis, hoidku jumal, tahavad suhkrut või piima. Selline kohvi travestia !! ??

Nüüd peavad teil olema ka need kaks lisandmoodulit, nii menüüs kui ka kahjuks arveldussüsteemis. Algselt valmistab teie IT-isik mõlemale kohvile alaklassi, millest üks sisaldab suhkrut, teine ​​piima. Kuna klientidel on alati õigus, öeldakse järgmisi kardetud sõnu:

"Kas ma saaksin piimakohvi suhkruga, palun?"

???

Seal läheb teie arveldussüsteem jälle teie nägu naerma. Noh, tagasi joonestuslaua juurde ...

Seejärel lisab IT-inimene igale vanema kohvikuklassile teise alaklassina suhkruga piimakohvi. Ülejäänud kuu on sujuv purjetamine, inimesed joovad teie kohvi jooma, te tegelikult teenite raha. ??

Aga oota, neid on veel!

Maailm on taas teie vastu. Konkurent avaneb üle tänava, kus pole mitte ainult 4 tüüpi kohvi, vaid ka üle 10 lisandmooduli! ?

Ostate kõik need ja palju muud, et ise paremat kohvi müüa, ja pidage lihtsalt meeles, et unustasite seda suurt arveldussüsteemi värskendada. Võimalik, et ei saa lõpmatu arvu alamklasside loomiseks kõigi lisandmoodulite kõigi kombinatsioonide jaoks, ka uute kohvisegudega. Rääkimata lõpliku süsteemi suurusest. ??

Aeg investeerida õigesse arveldussüsteemi. Leiate uue IT-personali, kes tegelikult teab, mida nad teevad, ja nad ütlevad;

"Miks, see on nii palju lihtsam ja väiksem, kui kasutati dekoraatori mustrit."

Mis see maa peal on?

Dekoraatori kujundusmuster kuulub struktuurikategooriasse, mis tegeleb klassi tegeliku struktuuriga, olgu see siis pärimise, koosseisu või mõlema järgi. Selle kujunduse eesmärk on modifitseerida objektide funktsionaalsust käitusajal. See on üks paljudest muudest kujundusmustritest, mis kasutavad soovitud tulemuse saamiseks abstraktseid klasse ja kompositsioonidega liideseid.

Anname Mathile võimaluse (väriseda?) See kõik perspektiivi tuua;

Võtke 4 kohvisegu ja 10 lisandit. Kui jäime kindlaks alamklasside genereerimise jaoks, mis on mõeldud igat tüüpi ühe kohvi kõigi lisandmoodulite erinevate kombinatsioonide jaoks. See on;

(10–1) ² = 9² = 81 alaklassi

We subtract 1 from the 10, as you cannot combine one add-on with another of the same type, sugar with sugar sounds stupid. And that’s for just one coffee blend. Multiply that 81 by 4 and you get a whopping 324 different subclasses! Talk about all that coding…

But with the decorator pattern will require only 16 classes in this scenario. Wanna bet?

If we map out our scenario according to the class diagram above, we get 4 classes for the 4 coffee blends, 10 for each add-on and 1 for the abstract component and 1 more for the abstract decorator. See! 16! Now hand over that $100.?? (jk, but it will not be refused if given… just saying)

As you can see from above, just as the concrete coffee blends are subclasses of the beverage abstract class, the AddOn abstract class also inherits its methods from it. The add-ons, that are its subclasses, in turn inherit any new methods to add functionality to the base object when needed.

Let’s get to coding, to see this pattern in use.

First to make the Abstract beverage class, that all the different coffee blends will inherit from:

public abstract class Beverage { private String description; public Beverage(String description) { super(); this.description = description; } public String getDescription() { return description; } public abstract double cost(); }

Then to add both the concrete coffee blend classes.

public class HouseBlend extends Beverage { public HouseBlend() { super(“House blend”); } @Override public double cost() { return 250; } } public class DarkRoast extends Beverage { public DarkRoast() { super(“Dark roast”); } @Override public double cost() { return 300; } }

The AddOn abstract class also inherits from the Beverage abstract class (more on this below).

public abstract class AddOn extends Beverage { protected Beverage beverage; public AddOn(String description, Beverage bev) { super(description); this.beverage = bev; } public abstract String getDescription(); }

And now the concrete implementations of this abstract class:

public class Sugar extends AddOn { public Sugar(Beverage bev) { super(“Sugar”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Mocha”; } @Override public double cost() { return beverage.cost() + 50; } } public class Milk extends AddOn { public Milk(Beverage bev) { super(“Milk”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Milk”; } @Override public double cost() { return beverage.cost() + 100; } }

As you can see above, we can pass any subclass of Beverage to any subclass of AddOn, and get the added cost as well as the updated description. And, since the AddOn class is essentially of type Beverage, we can pass an AddOn into another AddOn. This way, we can add any number of add-ons to a specific coffee blend.

Now to write some code to test this out.

public class CoffeeShop { public static void main(String[] args) { HouseBlend houseblend = new HouseBlend(); System.out.println(houseblend.getDescription() + “: “ + houseblend.cost()); Milk milkAddOn = new Milk(houseblend); System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost()); Sugar sugarAddOn = new Sugar(milkAddOn); System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost()); } }

The final result is:

It works! We were able to add more than one add-on to a coffee blend and successfully update its final cost and description, without the need to make infinite subclasses for each add-on combination for all coffee blends.

Finally, to the last category.

Type 3: Behavioral - The Command Design Pattern

A behavioral design pattern focuses on how classes and objects communicate with each other. The main focus of the command pattern is to inculcate a higher degree of loose coupling between involved parties (read: classes).

Uhhhh… What’s that?

Coupling is the way that two (or more) classes that interact with each other, well, interact. The ideal scenario when these classes interact is that they do not depend heavily on each other. That’s loose coupling. So, a better definition for loose coupling would be, classes that are interconnected, making the least use of each other.

The need for this pattern arose when requests needed to be sent without consciously knowing what you are asking for or who the receiver is.

In this pattern, the invoking class is decoupled from the class that actually performs an action. The invoker class only has the callable method execute, which runs the necessary command, when the client requests it.

Let’s take a basic real-world example, ordering a meal at a fancy restaurant. As the flow goes, you give your order (command) to the waiter (invoker), who then hands it over to the chef(receiver), so you can get food. Might sound simple… but a bit meh to code.

The idea is pretty simple, but the coding goes around the nose.

The flow of operation on the technical side is, you make a concrete command, which implements the Command interface, asking the receiver to complete an action, and send the command to the invoker. The invoker is the person that knows when to give this command. The chef is the only one who knows what to do when given the specific command/order. So, when the execute method of the invoker is run, it, in turn, causes the command objects’ execute method to run on the receiver, thus completing necessary actions.

What we need to implement is;

  1. An interface Command
  2. A class Order that implements Command interface
  3. A class Waiter (invoker)
  4. A class Chef (receiver)

So, the coding goes like this:

Chef, the receiver

public class Chef { public void cookPasta() { System.out.println(“Chef is cooking Chicken Alfredo…”); } public void bakeCake() { System.out.println(“Chef is baking Chocolate Fudge Cake…”); } }

Command, the interface

public interface Command { public abstract void execute(); }

Order, the concrete command

public class Order implements Command { private Chef chef; private String food; public Order(Chef chef, String food) { this.chef = chef; this.food = food; } @Override public void execute() { if (this.food.equals(“Pasta”)) { this.chef.cookPasta(); } else { this.chef.bakeCake(); } } }

Waiter, the invoker

public class Waiter { private Order order; public Waiter(Order ord) { this.order = ord; } public void execute() { this.order.execute(); } }

You, the client

public class Client { public static void main(String[] args) { Chef chef = new Chef(); Order order = new Order(chef, “Pasta”); Waiter waiter = new Waiter(order); waiter.execute(); order = new Order(chef, “Cake”); waiter = new Waiter(order); waiter.execute(); } }

Nagu eespool näha, teeb klient tellimuse ja määrab peakokaks vastuvõtja. Tellimus saadetakse kelnerile, kes saab teada, millal tellimus täita (st millal anda kokale kokkamise korraldus). Kui kutsutaja on täidetud, käivitatakse vastuvõtjal Orders'i käivitamismeetod (st peakokk annab käsu kas pastat küpsetada või kooki küpsetada?).

Kiire kokkuvõte

Selles postituses käisime läbi:

  1. Mis disainmuster tegelikult on,
  2. Kujundusmustrite erinevad tüübid ja miks need erinevad
  3. Iga tüübi jaoks üks põhiline või ühine kujundusmuster

Loodan, et sellest oli abi.  

Siit leiate postituse koodi repo.