3-те типа дизайнерски модели, които всички разработчици трябва да знаят (с примери за кодове за всеки)

Какво е модел на дизайн?

Моделите на проектиране са решения на ниво дизайн за повтарящи се проблеми, които ние софтуерните инженери срещаме често. Това не е код - повтарям,КОД . Това е като описание на това как да се справим с тези проблеми и да проектираме решение.

Използването на тези модели се счита за добра практика, тъй като дизайнът на решението е доста изпитан и тестван, което води до по-висока четливост на крайния код. Моделите за проектиране доста често се създават и използват от OOP езици, като Java, в който повечето от примерите от тук нататък ще бъдат написани.

Видове дизайнерски модели

В момента са открити около 26 шаблона (едва ли мисля, че ще направя всички ...).

Тези 26 могат да бъдат класифицирани в 3 вида:

1. Създаване: Тези модели са предназначени за създаване на клас. Те могат да бъдат или модели за създаване на клас или модели за създаване на обекти.

2. Структурни: Тези модели са създадени по отношение на структурата и състава на класа. Основната цел на повечето от тези модели е да се увеличи функционалността на съответните класове, без да се променя голяма част от неговия състав.

3. Поведенчески: Тези модели са създадени в зависимост от това как един клас комуникира с други.

В тази публикация ще преминем през един основен модел на проектиране за всеки класифициран тип.

Тип 1: Творчески - Моделът на Singleton Design

Шаблонът за дизайн на Singleton е шаблон за създаване, чиято цел е да създаде само един екземпляр на клас и да предостави само една глобална точка за достъп до този обект. Един често използван пример за такъв клас в Java е Calendar, където не можете да направите екземпляр на този клас. Той също така използва свой собствен getInstance()метод, за да накара обекта да бъде използван.

Клас, използващ модела за единичен дизайн, ще включва,

  1. Частна статична променлива, съдържаща единствения екземпляр на класа.
  2. Частен конструктор, така че не може да бъде създаден никъде другаде.
  3. Публичен статичен метод за връщане на единичния екземпляр на класа.

Има много различни изпълнения на единичен дизайн. Днес ще прегледам внедряванията на;

1. Нетърпеливо инстанциране

2. Мързеливо инстанциране

3. Инстанциране, безопасно за нишките

Нетърпелив бобър

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

Този тип инстанциране се случва по време на зареждане на класа, тъй като инстанцирането на екземпляра на променливата се случва извън всеки метод. Това създава огромен недостатък, ако този клас изобщо не се използва от клиентското приложение. Планът за извънредни ситуации, ако този клас не се използва, е Мързеливото инстанциране.

Мързеливи дни

Няма голяма разлика от горното изпълнение. Основните разлики са, че статичната променлива първоначално се обявява за нула и се екземпляри само в getInstance()метода, ако - и само ако - променливата на екземпляра остава нула по време на проверката.

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

Това решава един проблем, но все още съществува друг. Какво ще стане, ако два различни клиента имат достъп до класа Singleton едновременно, точно до милисекундата? Е, те ще проверят дали екземплярът е нула едновременно и ще го намерят за истина и така ще създадат два екземпляра на класа за всяка заявка от двамата клиенти. За да се коригира това, трябва да се внедри екземпляр Thread Safe.

(Темата) Безопасността е ключова

В Java ключовата дума синхронизирана се използва за методи или обекти за внедряване на безопасност на нишките, така че само една нишка ще има достъп до определен ресурс наведнъж. Инстанцирането на класа се поставя в синхронизиран блок, така че методът може да бъде достъпен само от един клиент в даден момент.

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

Режийните разходи за синхронизирания метод са високи и намаляват производителността на цялата операция.

Например, ако променливата на екземпляра вече е създадена, тогава всеки клиент има достъп до getInstance()метода, synchronizedметодът се изпълнява и производителността спада. Това просто се случва, за да се провери дали instanceстойността на променливите е нула. Ако установи, че е, той напуска метода.

За да се намалят тези режийни разходи, се използва двойно заключване. Проверката се използва и преди synchronizedметода и ако стойността е само нула, synchronizedметодът се изпълнява.

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

Сега към следващата класификация.

Тип 2: Структурни - модел на декоратора

Ще ви дам малък сценарий, за да дадете по-добър контекст защо и къде трябва да използвате шаблона на декоратора.

Да предположим, че сте собственик на кафене и като всеки начинаещ, започвате само с два вида обикновено кафе, домашен бленд и тъмно печено. Във вашата система за фактуриране имаше един клас за различните смеси от кафе, който наследява абстрактния клас на напитката. Хората всъщност започват да идват и да пият вашето прекрасно (макар и горчиво?) Кафе. Тогава има и кафета, които, не дай боже, искат захар или мляко. Такава пародия за кафе !! ??

Сега трябва да имате и тези две добавки, както към менюто, така и за съжаление в системата за таксуване. Първоначално вашият ИТ специалист ще направи подклас и за двете кафета, едното включва захар, а другото мляко. След това, тъй като клиентите винаги са прави, човек казва тези страховити думи:

„Мога ли да си взема млечно кафе със захар, моля?“

???

Отива си системата за таксуване отново се смее в лицето ти. Е, обратно към чертожната дъска ...

След това ИТ специалистът добавя мляко кафе със захар като друг подклас към всеки клас кафе на родител. Останалата част от месеца е плавно плаване, хората се редят на опашка за вашето кафе, вие всъщност печелите пари. ??

Но почакайте, има още!

Светът отново е срещу теб. Конкурент се отваря отсреща, с не само 4 вида кафе, но и над 10 добавки! ?

Купувате всички тези и повече, за да продавате сами по-добро кафе и точно тогава не забравяйте, че сте забравили да актуализирате тази система за таксуване. Напълно вероятно не можете да направите безкраен брой подкласове за всякакви комбинации от всички добавки, с новите смеси от кафе също. Да не говорим, размерът на крайната система. ??

Време е всъщност да инвестирате в правилна система за таксуване. Намирате нов ИТ персонал, който всъщност знае какво правят и те казват;

„Защо това ще бъде много по-лесно и по-малко, ако се използваше декоративният модел.“

Какво по дяволите е това?

Схемата за дизайн на декоратора попада в структурната категория, която се занимава с действителната структура на клас, независимо дали е по наследство, композиция или и двете. Целта на този дизайн е да модифицира функционалността на обекти по време на изпълнение. Това е един от многото други дизайнерски модели, които използват абстрактни класове и интерфейси със композицията, за да получат желания резултат.

Нека да дадем шанс на математиката (потръпване?), За да приведе всичко това в перспектива;

Вземете 4 кафени смеси и 10 добавки. Ако се придържахме към генерирането на подкласове за всяка различна комбинация от всички добавки за един вид кафе. Това е;

(10–1) ² = 9² = 81 подкласа

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.