Шаблони проектування звичною мовою. Частина друга. Структурні шаблони.


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Розповідає Камран Ахмед


Шаблони проектування – це керівництво по рішенню проблем, які повторюються. Це не класи, пакети або бібліотеки, які можна було б підключити до вашого додатку і сидіти в очікуванні дива. Вони швидше є методиками рішення певних проблем в певних ситуаціях.

Вікіпедія описує їх таким чином:

Шаблон проектування, чи патерн, у розробці програмного забезпечення – повторювана архітектурна конструкція, що є вирішенням проблеми проектування, у рамках деякого часто виникаючого контексту.

Будьте обережні

  • шаблони проектування не є рішенням усіх ваших проблем;
  • не намагайтеся використовувати їх в обов’язковому порядку – це може привести до негативних наслідків. Шаблони – це підходи до рішення проблем, а не рішення для пошуку проблем;
  • якщо їх правильно використати в потрібних місцях, то вони можуть стати порятунком, а інакше можуть привести до жахливого безладу.

Також зверніть увагу, що приклади нижче написані на PHP 7. Але це не повинно вас зупиняти, адже принципи залишаються такими ж.

Типи шаблонів

Шаблони бувають наступних трьох видів:

  1. Що породжують.
  2. Структурні про них ми розповідаємо в цій статті.
  3. Поведінкові.

Простими словами: Структурні шаблони зазвичай пов’язані з композицією об’єктів, іншими словами, з тим, як сутності можуть використати один одного. Ще одним поясненням було б те, що вони допомагають відповісти на питання “Як створити програмний компонент”.

Вікіпедія свідчить:

Структурні шаблони – шаблони проектування, в яких розглядається питання про те, як з класів і об’єктів утворюються більші структури.

Список структурних шаблонів проектування :

Адаптер (Adapter)

Вікіпедія свідчить:

Адаптер – структурний шаблон проектування, призначений для організації використання функцій об’єкту, недоступного для модифікації, через спеціально створений інтерфейс.

Приклад з життя: Уявимо, що у вас на карті пам’яті є якісь зображення і вам потрібно перенести їх на ваш комп’ютер. Щоб це зробити, вам потрібний якийсь адаптер, який сумісний з портами вашого комп’ютера. В цьому випадку карт-рідер – це адаптер. Іншим прикладом буде блок живлення. Вилку з трьома ніжками не можна вставити в розетку з двома отворами. Для того, щоб вона підійшла, потрібно використати адаптер. Ще одним прикладом буде перекладач, який переводить слова однієї людини для іншого.

Простими словами: Шаблон дозволяє обернути несумісні об’єкти в адаптер, щоб зробити їх сумісними з іншим класом.

Звернемося до коду. Представимо гру, в якій мисливець полює на левів.

Спочатку у нас є інтерфейс Lion, який реалізує усіх левів:

interface Lion{ public function roar ();}class AfricanLion implements Lion{ public function roar (){ }}class AsianLion implements Lion{ public function roar (){ }}

І Hunter полює на будь-яку реалізацію інтерфейсу Lion:

class Hunter{ public function hunt (Lion $lion) { }}

Тепер уявимо, що нам потрібно додати WildDog у нашу гру, на яку наш Hunter також міг би полювати. Але ми не можемо зробити це безпосередньо, тому що у WildDog інший інтерфейс. Щоб зробити її сумісною з нашим Hunter, нам потрібно створити адаптер:

// Це потрібно додати в игруclass WildDog{ public function bark (){ }}
// Адаптер, щоб зробити WildDog сумісною з нашою грою class WildDogAdapter implements Lion { protected $dog; public function __construct (WildDog $dog) { $this ->dog = $dog; } public function roar (){ $this ->dog ->bark (); } }

Спосіб застосування :

$wildDog = new WildDog ();$wildDogAdapter = new WildDogAdapter ($wildDog);$hunter = new Hunter ();$hunter ->hunt ($wildDogAdapter);

Приклади на Java і Python.

Міст (Bridge)

Вікіпедія свідчить:

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

Приклад з життя: Уявимо, що у вас є сайт з різними сторінками, і вам потрібно дозволити користувачам міняти їх тему. Що ви робитимете? Створювати множинні копії кожної сторінки для кожної теми або просто окрему тему, яку користувач зможе вибрати сам? Шаблон міст дозволяє вам зробити друге.

Простими словами: Шаблон міст – ця перевага композиції над спадкоємством. Деталі реалізації передаються з однієї ієрархії в інший об’єкт з окремою ієрархією.

Звернемося приміром в коді. Візьмемо приклад з нашими сторінками. У нас є ієрархія WebPage:

interface WebPage{ public function __construct (Theme $theme); public function getContent ();}
class About implements WebPage{ protected $theme; public function __construct (Theme $theme) { $this ->theme = $theme; } public function getContent (){ return "Сторінка з інформацією в ". $this ->theme ->getColor (); }}class Careers implements WebPage{ protected $theme; public function __construct (Theme $theme) { $this ->theme = $theme; } public function getContent (){ return "Сторінка кар'єри в ". $this ->theme ->getColor (); }}

І окрема ієрархія Theme:

interface Theme{ public function getColor ();}class DarkTheme implements Theme{ public function getColor (){ return 'темній темі'; }}class LightTheme implements Theme{ public function getColor (){ return 'світлій темі'; }}
class AquaTheme implements Theme{ public function getColor (){ return 'блакитній темі'; }}

Застосування в коді:

$darkTheme = new DarkTheme ();$about = new About ($darkTheme);$careers = new Careers ($darkTheme);echo $about ->getContent (); // "Сторінка інформації в темній темі";echo $careers ->getContent (); // "Сторінка кар'єри в темній темі";

Приклади на Java і Python.

Компонувальник (Composite)

Вікіпедія свідчить:

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

Приклад з життя: Кожна організація скомпонована із співробітників. У кожного співробітника є однакові властивості, такі як зарплата, обов’язки, звітність і так далі.

Простими словами: Шаблон-компонувальник дозволяє клієнтам працювати з індивідуальними об’єктами в єдиному стилі.

Звернемося до коду. Візьмемо наш приклад з робітниками. У нас є Employee різних типів:

interface Assignee { public function canHandleTask ($task): bool; public function takeTask ($task);}class Employee implements Assignee { // реалізуємо методи інтерфейсу}class Team implements Assignee { /** @var Assignee[] */ private $assignees; // допоміжні методи для управління композитом: public function add ($assignee); public function remove ($assignee); // методу інтерфейсу Employee
public function canHandleTask ($task): bool { foreach ($this ->assignees as $assignee) if ($assignee ->canHandleTask ($task)) return true; return false; } public function takeTask ($task) { // може бути різна імплементація - припустимо, деякі завдання вимагають кількох чоловік з команди одночасно // в простому випадку беремо першого незайнятого працівника серед this ->assignees $assignee =...; $assignee ->takeTask ($task); }}

Тепер у нас є TaskManager:

class TaskManager { private $assignees; public function performTask ($task) { foreach ($this ->assignees as $assignee) { if ($assignee ->canHandleTask ($task)) { $assignee ->takeTask ($task); return; } } throw new Exception ('Cannot handle the task - please hire more people');
}}

Спосіб застосування :

$employee1 = new Employee ();$employee2 = new Employee ();$employee3 = new Employee ();$employee4 = new Employee ();$team1 = new Team ([$employee3, $employee4);// УВАГА: передаємо команду в taskManager як єдиний композит.// Сам taskManager не знає, що це команда і працює з нею без модифікації своєї логіки.$taskManager = new TaskManager ([$employee1, $employee2, $team1]);$taskManager ->preformTask ($task);

Приклади на Java і Python.

Декоратор (Decorator)

Вікіпедія свідчить:

Декоратор – структурний шаблон проектування, призначений для динамічного підключення додаткової поведінки до об’єкту. Шаблон-декоратор надає гнучку альтернативу практиці створення підкласів з метою розширення функціональності.

Приклад з життя: Уявимо, що у вас є свій автосервіс. Як ви розраховуватимете суму в рахунку за послуги? Ви вибираєте одну послугу і динамічно додаєте до неї ціни на послуги, що надаються, поки не отримаєте остаточну вартість. Тут кожен тип сервісу є декоратором.

Простими словами: Шаблон декоратор дозволяє вам динамічно змінювати поведінку об’єкту під час роботи, обертаючи його в об’єкт класу декоратора.

Перейдемо до коду. Візьмемо приклад з кавою. Спочатку у нас є простий Coffee і інтерфейс, що реалізовує його :

interface Coffee{ public function getCost (); public function getDescription ();}class SimpleCoffee implements Coffee{ public function getCost (){ return 10; } public function getDescription (){ return 'Проста кава'; }}

Ми хочемо зробити код розширюваним, щоб при необхідності можна було змінювати його. Давайте зробимо деякі доповнення (декоратори) :

class MilkCoffee implements Coffee{ protected $coffee; public function __construct (Coffee $coffee) { $this ->coffee = $coffee; } public function getCost (){ return $this ->coffee ->getCost () + 2; }
public function getDescription (){ return $this ->coffee ->getDescription (). ', молоко'; }}class WhipCoffee implements Coffee{ protected $coffee; public function __construct (Coffee $coffee) { $this ->coffee = $coffee; } public function getCost (){ return $this ->coffee ->getCost () + 5; } public function getDescription (){ return $this ->coffee ->getDescription (). ', вершки'; }}class VanillaCoffee implements Coffee{ protected $coffee; public function __construct (Coffee $coffee) { $this ->coffee = $coffee; } public function getCost (){ return $this ->coffee ->getCost () + 3; } public function getDescription (){ return $this ->coffee ->getDescription (). ', ваніль'; }}

А тепер приготуємо Coffee:

$someCoffee = new SimpleCoffee ();echo $someCoffee ->getCost (); // 10echo $someCoffee ->getDescription (); // Проста кава$someCoffee = new MilkCoffee ($someCoffee);echo $someCoffee ->getCost (); // 12echo $someCoffee ->getDescription (); // Проста кава, молоко$someCoffee = new WhipCoffee ($someCoffee);echo $someCoffee ->getCost (); // 17echo $someCoffee ->getDescription (); // Проста кава, молоко, вершки$someCoffee = new VanillaCoffee ($someCoffee);echo $someCoffee ->getCost (); // 20echo $someCoffee ->getDescription (); // Проста кава, молоко, вершки, ваніль

Приклади на Java і Python.

Фасад (Facade)

Вікіпедія свідчить:

Фасад – структурний шаблон проектування, що дозволяє приховати складність системи шляхом зведення усіх можливих зовнішніх викликів до одного об’єкту, що делегує їх відповідним об’єктам системи.

Приклад з життя: Як ви включаєте комп’ютер? Натискаю на кнопку включення, скажете ви. Це те, в що ви вірите, тому що ви використовуєте простий інтерфейс, який комп’ютер надає для доступу зовні. Усередині ж повинно статися значно більше. Цей простий інтерфейс для складної підсистеми називається фасадом.

Простими словами: Шаблон фасад надає спрощений інтерфейс для складної системи.

Перейдемо до прикладів в коді. Візьмемо приклад з комп’ютером. Спочатку у нас є клас Computer:

class Computer{
public function getElectricShock (){ echo " Ай"!; } public function makeSound (){ echo "Бип-бип"!; } public function showLoadingScreen (){ echo "Завантаження.".; } public function bam (){ echo "Готовий до використання"!; } public function closeEverything (){ echo "Буп-буп-буп-бззз"!; } public function sooth (){ echo " Zzzzz"; } public function pullCurrent (){ echo " Аах"!; }}

Потім у нас є фасад:

class ComputerFacade{ protected $computer; public function __construct (Computer $computer) { $this ->computer = $computer; } public function turnOn (){ $this ->computer ->getElectricShock (); $this ->computer ->makeSound ();
$this ->computer ->showLoadingScreen (); $this ->computer ->bam (); } public function turnOff (){ $this ->computer ->closeEverything (); $this ->computer ->pullCurrent (); $this ->computer ->sooth (); }}

Приклад використання :

$computer = new ComputerFacade (new Computer());$computer ->turnOn (); // Ай! Бип-бип! Завантаження. Готовий до використання!$computer ->turnOff (); // Буп-буп-буп-бззз! Аах! Zzzzz

Приклади на Java і Python.

Пристосованець (Flyweight)

Вікіпедія свідчить:

Пристосованець – структурний шаблон проектування, при якому об’єкт, що представляє себе як унікальний екземпляр в різних місцях програми, по факту не є таким.

Приклад з життя: Ви коли-небудь замовляли чай у вуличному ларьку? Там готують не одну чашку, яку ви замовили, а набагато більшу місткість. Це робиться для того, щоб економити ресурси (газ/електрика). Газ/електрика в даному прикладі і є пристосованцями, ресурси яких діляться (sharing).

Простими словами: Пристосованець використовується для мінімізації використання пам’яті або обчислювальної вартості шляхом розділення ресурсів з найбільшою кількістю схожих об’єктів.

Перейдемо до прикладів в коді. Візьмемо наш приклад з чаєм. Спочатку у нас є різні види Tea і TeaMaker:

// Все, що буде закешировано, є пристосованцем.// Типи чаю тут будуть пристосованцями.class KarakTea{}//Поводиться як фабрика і зберігає чайclass TeaMaker{ protected $availableTea =[]; public function make ($preference) { if (empty ($this ->availableTea[$preference])) { $this ->availableTea[$preference] = new KarakTea (); } return $this ->availableTea[$preference]; }}

Тепер у нас є TeaShop, який приймає замовлення і виконує їх:

class TeaShop{ protected $orders; protected $teaMaker; public function __construct (TeaMaker $teaMaker) {
$this ->teaMaker = $teaMaker; } public function takeOrder (string $teaType, int $table) { $this ->orders[$table] = $this ->teaMaker ->make ($teaType); } public function serve (){ foreach ($this ->orders as $table => $tea) { echo "Serving tea to table# ". $table; } }}

Приклад використання :

$teaMaker = new TeaMaker ();$shop = new TeaShop ($teaMaker);$shop ->takeOrder ('менше цукру', 1);$shop ->takeOrder ('більше молока', 2);$shop ->takeOrder ('без цукру', 5);$shop ->serve ();// Подаємо чай на перший стіл// Подаємо чай на другий стіл// Подаємо чай на п'ятий стіл

Приклади на Java і Python.

Заступник (Proxy)

Вікіпедія свідчить:

Заступник – структурний шаблон проектування, який надає об’єкт, який контролює доступ до іншого об’єкту, перехоплюючи усі виклики (виконує функцію контейнера).

Приклад з життя: Ви коли-небудь використали карту доступу, щоб пройти через двері? Є декілька способів відкрити двері: наприклад, вона може бути відкрита за допомогою карти доступу або натиснення кнопки, яка обходить захист. Основна функціональність дверей – це відкриття, але заступник, доданий поверх цього, додає функціональність. Але краще я поясню це на прикладі коду трохи нижче.

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

Перейдемо до коду. Візьмемо наш приклад з безпекою. Спочатку у нас є інтерфейс Door і його реалізація:

interface Door{ public function open (); public function close ();}class LabDoor implements Door{ public function open (){ echo "Відкриття двері лабораторії"; }
public function close (){ echo "Закриття дверей лабораторії"; }}

Потім у нас є заступник Security для захисту будь-яких наших дверей :

class Security{ protected $door; public function __construct (Door $door) { $this ->door = $door; } public function open ($password) { if ($this ->authenticate ($password)) { $this ->door ->open (); } else { echo "Немає! Це неможливо".; } } public function authenticate ($password) { return $password === '[email protected]'; } public function close (){ $this ->door ->close (); }}

Приклад використання :

$door = new Security (new LabDoor());$door ->open ('invalid'); // Ні! Це неможливо.
$door ->open('[email protected]'); // Відкриття дверей лабораторії$door ->close (); // Закриття дверей лабораторії

Іншим прикладом буде реалізація мапінга даних. Наприклад, нещодавно я створив ODM (Object Data Mapper) для MongoDB, використовуючи цей шаблон, де я написав заступник навколо класів mongo і використав магічний метод __call (). Усі виклики методів були заміщені оригінальним класом mongo, і отриманий результат повертався без змін, але у випадку find чи findOne дані зіставлялися необхідному класу, і поверталися в об’єкт замість Cursor.

Приклади на Java і Python.

Переклад статті “Design Patterns for Humans”

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *