3 vrste uzoraka dizajna koje bi svi programeri trebali znati (s primjerima koda za svaki)

Što je obrazac dizajna?

Uzorci dizajna rješenja su na razini dizajna za ponavljajuće probleme s kojima se mi softverski inženjeri često susrećemo. To nije kod - ponavljam,ŠIFRA . To je poput opisa kako se nositi s tim problemima i osmisliti rješenje.

Korištenje ovih uzoraka smatra se dobrom praksom, jer je dizajn rješenja prilično isproban i testiran, što rezultira većom čitljivošću konačnog koda. Uzorci dizajna su često stvoreni za i koriste ih OOP jezici, poput Jave, u kojima će biti napisana većina primjera od ovog trenutka.

Vrste uzoraka dizajna

Trenutno je otkriveno oko 26 uzoraka (teško da mislim da ću ih sve napraviti ...).

Njih 26 može se svrstati u 3 vrste:

1. Kreativno: Ovi su obrasci dizajnirani za instanciju klase. To mogu biti uzorci stvaranja klase ili uzorci kreacije predmeta.

2. Strukturni: Ovi su obrasci osmišljeni s obzirom na strukturu i sastav klase. Glavni cilj većine ovih uzoraka je povećati funkcionalnost klase (a), a da se pritom ne promijeni veći dio njezinog sastava.

3. Ponašanje: Ti se obrasci osmišljavaju ovisno o tome kako jedan razred komunicira s drugima.

U ovom ćemo postu proći kroz jedan osnovni obrazac dizajna za svaku klasificiranu vrstu.

Tip 1: kreativni - obrazac dizajna Singleton

Uzorak dizajna Singleton kreativni je obrazac čiji je cilj stvoriti samo jedan primjerak klase i pružiti samo jednu globalnu pristupnu točku tom objektu. Jedan od često korištenih primjera takve klase u Javi je Kalendar, gdje ne možete napraviti instancu te klase. Također koristi vlastitu getInstance()metodu kako bi objekt dobio na korištenje.

Predavanje koja koristi jednokračni dizajn uključuje,

  1. Privatna statička varijabla koja sadrži jedini primjerak klase.
  2. Privatni konstruktor, pa se ne može instancirati bilo gdje drugdje.
  3. Javna statička metoda za vraćanje pojedinačne instance klase.

Postoji mnogo različitih implementacija singleton dizajna. Danas ću proći kroz implementacije;

1. Željno instaliranje

2. Lijena instantacija

3. Instanciranje bez navoja

Nestrpljivi Dabar

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

Ova vrsta instancije događa se tijekom učitavanja klase, jer se instancacija varijable instance događa izvan bilo koje metode. To predstavlja pozamašan nedostatak ako se klijentska aplikacija uopće ne koristi ovom klasom. Ako se ova klasa ne koristi, plan za izvanredne situacije je Lazy Instantiation.

Lijeni dani

Nema velike razlike u odnosu na gornju provedbu. Glavne razlike su u tome što se statička varijabla u početku proglašava nulom i instancira se samo unutar getInstance()metode ako - i samo ako - varijabla instance ostane nula u vrijeme provjere.

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

Ovo rješava jedan problem, ali još uvijek postoji drugi. Što ako dva različita klijenta istovremeno pristupe klasi Singleton, točno u milisekundi? Pa, oni će istodobno provjeriti je li instanca ništavna i naći će je istinitom, i tako će stvoriti dvije instance klase za svaki zahtjev dvaju klijenata. Da bi se to popravilo, treba implementirati instancu Thread Safe.

(Tema) Sigurnost je ključna

U Javi se sinkronizirana ključna riječ koristi na metodama ili objektima za provedbu sigurnosti niti, tako da će samo jedna nit istovremeno pristupiti određenom resursu. Instancija klase stavlja se u sinkronizirani blok tako da metodi može pristupiti samo jedan klijent u određeno vrijeme.

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

Opći troškovi za sinkroniziranu metodu su visoki i smanjuju performanse cijele operacije.

Na primjer, ako je varijabla instance već instancirana, tada se svaki put kada bilo koji klijent pristupi getInstance()metodi, synchronizedmetoda se pokrene i izvedba padne. To se događa samo kako bi se provjerilo je li vrijednost instancevarijabli null. Ako utvrdi da jest, napušta metodu.

Da bi se smanjili ovi režijski troškovi, koristi se dvostruko zaključavanje. Provjera se koristi i prije synchronizedmetode, a ako je vrijednost samo nula, synchronizedizvršava li se metoda.

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

Sada na sljedeću klasifikaciju.

Tip 2: Strukturni - uzorak dizajnera dizajnera

Dat ću vam mali scenarij kako biste dali bolji kontekst zašto i gdje biste trebali koristiti uzorak dekoratora.

Recimo da ste vlasnik kafića i da kao i svaki novi početnik započinjete sa samo dvije vrste obične kave, kućnom mješavinom i tamnom pečenkom. U vašem sustavu naplate postojala je jedna klasa za različite mješavine kave koja nasljeđuje klasu apstraktnih napitaka. Ljudi zapravo počnu navratiti i popiti vašu divnu (iako gorku?) Kavu. Zatim tu su kava novorođenčad koja, ne daj Bože, želi šećer ili mlijeko. Kakva parodija za kavu !! ??

Sada morate imati i ta dva dodatka, i na izborniku i nažalost u sustavu naplate. Izvorno će vaša IT osoba pripremiti podrazred za obje kave, jednu uključujući šećer, a drugu mlijeko. Zatim, budući da su kupci uvijek u pravu, netko kaže ove strašne riječi:

"Mogu li dobiti mliječnu kavu sa šećerom, molim?"

???

Ponovo vam se smije vaš sustav naplate. Pa, vratimo se na ploču za crtanje ...

IT-ovac zatim dodaje mliječnu kavu sa šećerom kao još jedan podrazred svakoj roditeljskoj klasi kave. Ostatak mjeseca glatko plovi, ljudi se poredaju za vašu kavu, vi zapravo zarađujete. ??

Ali čekajte, ima još!

Svijet je još jednom protiv vas. Natjecatelj se otvara preko puta, ne samo 4 vrste kave, već i više od 10 dodataka! ?

Kupite sve te i više da biste sami prodali bolju kavu i tada se sjetite da ste zaboravili ažurirati taj napušteni sustav naplate. Vrlo vjerojatno ne možete napraviti beskonačan broj potklasa za bilo koju kombinaciju svih dodataka, ni s novim mješavinama kave. Da ne spominjem, veličinu konačnog sustava. ??

Vrijeme je za stvarno ulaganje u pravilan sustav naplate. Pronaći ćete novo IT osoblje, koje zapravo zna što radi i kaže;

"Pa, ovo će biti puno lakše i manje ako se koristi uzorak dekoratora."

Što je zaboga?

Uzorak dizajna dekoratera spada u strukturnu kategoriju koja se bavi stvarnom strukturom klase, bilo nasljeđivanjem, kompozicijom ili oboje. Cilj ovog dizajna je izmijeniti funkcionalnost objekata tijekom izvođenja. Ovo je jedan od mnogih drugih dizajnerskih obrazaca koji koriste apstraktne klase i sučelja sa kompozicijom kako bi dobili željeni rezultat.

Dajmo Matematiku priliku (zadrhtati?) Da sve ovo iznese u perspektivu;

Uzmite 4 mješavine kave i 10 dodataka. Ako bismo se držali generacije podrazreda za svaku različitu kombinaciju svih dodataka za jednu vrstu kave. To je;

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

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

As you can see above, the Client makes an Order and sets the Receiver as the Chef. The Order is sent to the Waiter, who will know when to execute the Order (i.e. when to give the chef the order to cook). When the invoker is executed, the Orders’ execute method is run on the receiver (i.e. the chef is given the command to either cook pasta ? or bake cake?).

Quick recap

In this post we went through:

  1. What a design pattern really is,
  2. The different types of design patterns and why they are different
  3. One basic or common design pattern for each type

I hope this was helpful.  

Find the code repo for the post, here.