3 типи шаблонів дизайну, які повинні знати всі розробники (із прикладами коду кожного з них)

Що таке шаблон дизайну?

Шаблони проектування - це рішення рівня дизайну для повторюваних проблем, з якими ми, інженери-програмісти, стикаємось часто. Це не код - повторюю,КОД . Це як опис того, як вирішити ці проблеми та розробити рішення.

Використання цих шаблонів вважається гарною практикою, оскільки розробка рішення досить перевірена, що призводить до вищої читабельності остаточного коду. Шаблони дизайну досить часто створюються та використовуються мовами ООП, такими як Java, в яких буде написано більшість прикладів з цього моменту.

Види шаблонів дизайну

В даний час виявлено близько 26 шаблонів (навряд чи я думаю, що зроблю їх усі ...).

Ці 26 можна класифікувати на 3 типи:

1. Творчий: ці шаблони призначені для створення екземплярів класу. Це можуть бути як шаблони створення класу, так і об’єкти, що створюють об’єкти.

2. Структурні: ці структури розроблені з урахуванням структури та складу класу. Головною метою більшості цих зразків є підвищення функціональності задіяних класів, не змінюючи більшої частини його складу.

3. Поведінка: ці моделі розробляються залежно від того, як один клас спілкується з іншими.

У цій публікації ми розглянемо один основний шаблон дизайну для кожного класифікованого типу.

Тип 1: Творчий - шаблон дизайну Singleton

Шаблон проектування Singleton - це шаблон створення, метою якого є створення лише одного екземпляра класу та надання лише однієї глобальної точки доступу до цього об'єкта. Одним із загальновживаних прикладів такого класу в Java є Календар, де ви не можете зробити екземпляр цього класу. Він також використовує власний 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.