De 3 typer designmønstre, som alle udviklere bør kende (med kodeeksempler på hver)

Hvad er et designmønster?

Designmønstre er løsninger på designniveau til tilbagevendende problemer, som vi softwareingeniører ofte støder på. Det er ikke kode - jeg gentager,KODE . Det er som en beskrivelse af, hvordan man tackler disse problemer og designer en løsning.

Brug af disse mønstre betragtes som god praksis, da løsningen er ret prøvet og testet, hvilket resulterer i højere læsbarhed af den endelige kode. Designmønstre oprettes ofte til og bruges af OOP-sprog, som Java, hvor de fleste eksempler herfra vil blive skrevet.

Typer af designmønstre

Der er i øjeblikket omkring 26 mønstre opdaget (jeg tror næppe jeg vil gøre dem alle ...).

Disse 26 kan klassificeres i 3 typer:

1. Creational: Disse mønstre er designet til klasse instantiering. De kan være enten klasseskabelsesmønstre eller objektskabelsesmønstre.

2. Strukturel: Disse mønstre er designet med hensyn til klassens struktur og sammensætning. Hovedmålet med de fleste af disse mønstre er at øge funktionaliteten i den / de involverede klasse (r) uden at ændre meget af dens sammensætning.

3. Adfærdsmæssige: Disse mønstre er designet afhængigt af, hvordan en klasse kommunikerer med andre.

I dette indlæg vil vi gennemgå et grundlæggende designmønster for hver klassificeret type.

Type 1: Creational - Singleton-designmønsteret

Singleton-designmønsteret er et skabelsesmønster, hvis mål er at oprette kun en forekomst af en klasse og kun give et globalt adgangspunkt til det objekt. Et almindeligt anvendt eksempel på en sådan klasse i Java er Kalender, hvor du ikke kan lave en forekomst af denne klasse. Det bruger også sin egen getInstance()metode til at få objektet til at blive brugt.

En klasse, der bruger designmønsteret for singleton, inkluderer,

  1. En privat statisk variabel, der indeholder den eneste forekomst af klassen.
  2. En privat konstruktør, så den kan ikke instantieres andre steder.
  3. En offentlig statisk metode til at returnere den enkelte forekomst af klassen.

Der er mange forskellige implementeringer af singleton-design. I dag vil jeg gennemgå implementeringerne af;

1. Ivrig instantiering

2. Lazy Instantiation

3. Trådsikker instantiering

Ivrige bæver

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; } }

Denne type instantiering sker under klasseindlæsning, da instantiering af den variable forekomst sker uden for enhver metode. Dette udgør en stor ulempe, hvis denne klasse overhovedet ikke bruges af klientapplikationen. Beredskabsplanen, hvis denne klasse ikke bruges, er Lazy Instantiation.

Dovne dage

Der er ikke meget forskel fra ovenstående implementering. De væsentligste forskelle er, at den statiske variabel oprindeligt erklæres ugyldig og kun instantieres inden for getInstance()metoden, hvis - og kun hvis - forekomstvariablen forbliver nul på tidspunktet for kontrollen.

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; } }

Dette løser et problem, men et andet eksisterer stadig. Hvad hvis to forskellige klienter får adgang til Singleton-klassen på samme tid lige i millisekunden? Nå, de vil kontrollere, om forekomsten er nul på samme tid, og finder den sand, og det vil således skabe to forekomster af klassen for hver anmodning fra de to klienter. For at løse dette skal trådsikker instantiering implementeres.

(Tråd) Sikkerhed er nøglen

I Java bruges nøgleordet synkroniseret på metoder eller objekter til at implementere trådsikkerhed, så kun en tråd får adgang til en bestemt ressource ad gangen. Klasse-instantiering placeres i en synkroniseret blok, så metoden kun kan tilgås af en klient på et givet tidspunkt.

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; } }

Overhead for den synkroniserede metode er høj og reducerer ydeevnen for hele operationen.

For eksempel, hvis forekomstvariablen allerede er instantificeret, køres getInstance()metoden, hver gang en klient får adgang til metoden, synchronizedog ydeevnen falder. Dette sker bare for at kontrollere, om instancevariablernes værdi er nul. Hvis det finder ud af, at det er det, forlader det metoden.

For at reducere denne omkostning anvendes dobbeltlåsning. Kontrollen bruges også før synchronizedmetoden, og hvis værdien er nul alene, kører synchronizedmetoden.

// 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; }

Nu til næste klassifikation.

Type 2: Strukturel - Dekoratørdesignmønsteret

Jeg vil give dig et lille scenarie for at give en bedre kontekst til, hvorfor og hvor du skal bruge dekoratørmønsteret.

Sig, at du ejer en kaffebar, og som enhver nybegynder starter du med kun to typer almindelig kaffe, husblandingen og mørk stege. I dit faktureringssystem var der en klasse for de forskellige kaffeblandinger, som arver drikkevareabstraktklassen. Folk begynder faktisk at komme forbi og få din vidunderlige (omend bitre?) Kaffe. Så er der de nye kaffe, der, forbyder Gud, sukker eller mælk. Sådan en travesty for kaffe !! ??

Nu skal du også have disse to tilføjelser, både til menuen og desværre på faktureringssystemet. Oprindeligt laver din it-person en underklasse for begge kaffe, den ene inklusive sukker, den anden mælk. Da kunderne altid har ret, siger man disse frygtede ord:

"Kan jeg få en mælkekaffe med sukker, tak?"

???

Der går dit faktureringssystem griner i dit ansigt igen. Nå tilbage til tegnebrættet….

IT-personen tilføjer derefter mælkekaffe med sukker som en anden underklasse til hver forældrekaffe. Resten af ​​måneden er glat sejlads, folk står i kø for at få din kaffe, du tjener faktisk penge. ??

Men vent, der er mere!

Verden er igen imod dig. En konkurrent åbner sig over gaden med ikke kun 4 typer kaffe, men også mere end 10 tilføjelser! ?

Du køber alt det og mere for at sælge bedre kaffe selv, og husk bare da, at du har glemt at opdatere det drottede faktureringssystem. Du kan muligvis ikke lave det uendelige antal underklasser for alle kombinationer af alle tilføjelser med de nye kaffeblandinger også. For ikke at nævne størrelsen på det endelige system. ??

Tid til faktisk at investere i et ordentligt faktureringssystem. Du finder nyt it-personale, der faktisk ved, hvad de laver, og de siger;

"Hvorfor, dette bliver så meget lettere og mindre, hvis det bruger dekoratørmønsteret."

Hvad i alverden er det?

Dekoratørdesignmønsteret falder ind i den strukturelle kategori, der beskæftiger sig med klassens faktiske struktur, hvad enten det er ved arv, komposition eller begge dele. Målet med dette design er at ændre et objekters funktionalitet ved kørsel. Dette er et af de mange andre designmønstre, der bruger abstrakte klasser og grænseflader med komposition for at få det ønskede resultat.

Lad os give matematik en chance (ryster?) For at bringe alt dette i perspektiv;

Tag 4 kaffeblandinger og 10 tilføjelsesprogrammer. Hvis vi holdt fast ved genereringen af ​​underklasser for hver anden kombination af alle tilføjelser til en type kaffe. Det er;

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

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(); } }

Som du kan se ovenfor, foretager klienten en ordre og sætter modtageren som kokken. Ordren sendes til tjeneren, som ved, hvornår ordren skal udføres (dvs. hvornår han skal give kokken ordren til at lave mad). Når invoker udføres, køres ordrenes eksekveringsmetode på modtageren (dvs. kokken får kommandoen til enten at lave mad? Eller bage kage?).

Hurtig resumé

I dette indlæg gik vi igennem:

  1. Hvad et designmønster virkelig er,
  2. De forskellige typer designmønstre, og hvorfor de er forskellige
  3. Et grundlæggende eller fælles designmønster for hver type

Jeg håber, det var nyttigt.  

Find koden repo for indlægget her.