company_banner

Шаблоны проектирования с человеческим лицом

https://github.com/kamranahmedse/design-patterns-for-humans/blob/master/README.md
  • Перевод

image


Шаблоны проектирования — это способ решения периодически возникающих проблем. Точнее, это руководства по решению конкретных проблем. Это не классы, пакеты или библиотеки, которые вы можете вставить в своё приложение и ожидать волшебства.


Как сказано в Википедии:


В программной инженерии шаблон проектирования приложений — это многократно применяемое решение регулярно возникающей проблемы в рамках определённого контекста архитектуры приложения. Шаблон — это не законченное архитектурное решение, которое можно напрямую преобразовать в исходный или машинный код. Это описание подхода к решению проблемы, который можно применять в разных ситуациях.

image Будьте осторожны


  • Шаблоны проектирования — не «серебряная пуля».
  • Не пытайтесь внедрять их принудительно, последствия могут быть негативными. Помните, что шаблоны — это способы решения, а не поиска проблем. Так что не перемудрите.
  • Если применять их правильно и в нужных местах, они могут оказаться спасением. В противном случае у вас будет ещё больше проблем.

В статье приведены примеры на PHP 7, но пусть вас это не смущает, ведь заложенные в шаблонах принципы неизменны. Кроме того, внедряется поддержка других языков.


Виды шаблонов проектирования



Порождающие шаблоны проектирования


Вкратце


Порождающие шаблоны описывают создание (instantiate) объекта или группы связанных объектов.


Википедия


В программной инженерии порождающими называют шаблоны, которые используют механизмы создания объектов, чтобы создавать объекты подходящим для данной ситуации способом. Базовый способ создания может привести к проблемам в архитектуре или к её усложнению. Порождающие шаблоны пытаются решать эти проблемы, управляя способом создания объектов.


image Простая фабрика


Аналогия


Допустим, вы строите дом и вам нужны двери. Будет бардак, если каждый раз, когда вам требуется дверь, вы станете вооружаться инструментами и делать её на стройплощадке. Вместо этого вы закажете двери на фабрике.


Вкратце


Простая фабрика просто генерирует экземпляр для клиента без предоставления какой-либо логики экземпляра.


Википедия


В объектно ориентированном программировании фабрикой называется объект, создающий другие объекты. Формально фабрика — это функция или метод, возвращающая объекты разных прототипов или классов из вызова какого-то метода, который считается новым.

Пример


Для начала нам нужен интерфейс двери и его реализация.


interface Door
{
    public function getWidth(): float;
    public function getHeight(): float;
}

class WoodenDoor implements Door
{
    protected $width;
    protected $height;

    public function __construct(float $width, float $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function getWidth(): float
    {
        return $this->width;
    }

    public function getHeight(): float
    {
        return $this->height;
    }
}

Теперь соорудим фабрику дверей, которая создаёт и возвращает нам двери.


class DoorFactory
{
    public static function makeDoor($width, $height): Door
    {
        return new WoodenDoor($width, $height);
    }
}

Использование:


$door = DoorFactory:makeDoor(100, 200);
echo 'Width: ' . $door->getWidth();
echo 'Height: ' . $door->getHeight();

Когда использовать?


Когда создание объекта подразумевает какую-то логику, а не просто несколько присваиваний, то имеет смысл делегировать задачу выделенной фабрике, а не повторять повсюду один и тот же код.


image Фабричный метод


Аналогия


Одна кадровичка не в силах провести собеседования со всеми кандидатами на все должности. В зависимости от вакансии она может делегировать разные этапы собеседований разным сотрудникам.


Вкратце


Это способ делегирования логики создания объектов (instantiation logic) дочерним классам.


Википедия


В классо-ориентированном программировании (class-based programming) фабричным методом называют порождающий шаблон проектирования, использующий генерирующие методы (factory method) для решения проблемы создания объектов без указания для них конкретных классов. Объекты создаются посредством вызова не конструктора, а генерирующего метода, определённого в интерфейсе и реализованного дочерними классами либо реализованного в базовом классе и, опционально, переопределённого (overridden) производными классами (derived classes).

Пример


Сначала создадим интерфейс сотрудника, проводящего собеседование, и некоторые реализации для него.


interface Interviewer
{
    public function askQuestions();
}

class Developer implements Interviewer
{
    public function askQuestions()
    {
        echo 'Asking about design patterns!';
    }
}

class CommunityExecutive implements Interviewer
{
    public function askQuestions()
    {
        echo 'Asking about community building';
    }
}

Теперь создадим кадровичку HiringManager.


abstract class HiringManager
{

    // Фабричный метод
    abstract public function makeInterviewer(): Interviewer;

    public function takeInterview()
    {
        $interviewer = $this->makeInterviewer();
        $interviewer->askQuestions();
    }
}

Любой дочерний класс может расширять его и предоставлять нужного собеседующего:


class DevelopmentManager extends HiringManager
{
    public function makeInterviewer(): Interviewer
    {
        return new Developer();
    }
}

class MarketingManager extends HiringManager
{
    public function makeInterviewer(): Interviewer
    {
        return new CommunityExecutive();
    }
}

Использование:


$devManager = new DevelopmentManager();
$devManager->takeInterview(); // Output: Спрашивает о шаблонах проектирования.

$marketingManager = new MarketingManager();
$marketingManager->takeInterview(); // Output: Спрашивает о создании сообщества.

Когда использовать?


Этот шаблон полезен для каких-то общих обработок в классе, но требуемые подклассы динамически определяются в ходе выполнения (runtime). То есть когда клиент не знает, какой именно подкласс может ему понадобиться.


image Абстрактная фабрика


Аналогия


Вернёмся к примеру с дверями из «Простой фабрики». В зависимости от своих потребностей вы можете купить деревянную дверь в одном магазине, стальную — в другом, пластиковую — в третьем. Для монтажа вам понадобятся разные специалисты: деревянной двери нужен плотник, стальной — сварщик, пластиковой — спец по ПВХ-профилям.


Вкратце
Это фабрика фабрик. То есть фабрика, группирующая индивидуальные, но взаимосвязанные/взаимозависимые фабрики без указания для них конкретных классов.


Википедия


Шаблон «Абстрактная фабрика» описывает способ инкапсулирования группы индивидуальных фабрик, объединённых некой темой, без указания для них конкретных классов.

Пример


Создадим интерфейс Door и несколько реализаций для него.


interface Door
{
    public function getDescription();
}

class WoodenDoor implements Door
{
    public function getDescription()
    {
        echo 'I am a wooden door';
    }
}

class IronDoor implements Door
{
    public function getDescription()
    {
        echo 'I am an iron door';
    }
}

Теперь нам нужны специалисты по установке каждого вида дверей.


interface DoorFittingExpert
{
    public function getDescription();
}

class Welder implements DoorFittingExpert
{
    public function getDescription()
    {
        echo 'I can only fit iron doors';
    }
}

class Carpenter implements DoorFittingExpert
{
    public function getDescription()
    {
        echo 'I can only fit wooden doors';
    }
}

Мы получили абстрактную фабрику, которая позволяет создавать семейства объектов или взаимосвязанные объекты. То есть фабрика деревянных дверей создаст деревянную дверь и человека для её монтажа, фабрика стальных дверей — стальную дверь и соответствующего специалиста и т. д.


interface DoorFactory
{
    public function makeDoor(): Door;
    public function makeFittingExpert(): DoorFittingExpert;
}

// Фабрика деревянных дверей возвращает плотника и деревянную дверь
class WoodenDoorFactory implements DoorFactory
{
    public function makeDoor(): Door
    {
        return new WoodenDoor();
    }

    public function makeFittingExpert(): DoorFittingExpert
    {
        return new Carpenter();
    }
}

// Фабрика стальных дверей возвращает стальную дверь и сварщика
class IronDoorFactory implements DoorFactory
{
    public function makeDoor(): Door
    {
        return new IronDoor();
    }

    public function makeFittingExpert(): DoorFittingExpert
    {
        return new Welder();
    }
}

Использование:


$woodenFactory = new WoodenDoorFactory();

$door = $woodenFactory->makeDoor();
$expert = $woodenFactory->makeFittingExpert();

$door->getDescription();  // Output: Я деревянная дверь
$expert->getDescription(); // Output: Я могу устанавливать только деревянные двери

// Same for Iron Factory
$ironFactory = new IronDoorFactory();

$door = $ironFactory->makeDoor();
$expert = $ironFactory->makeFittingExpert();

$door->getDescription();  // Output: Я стальная дверь
$expert->getDescription(); // Output: Я могу устанавливать только стальные двери

Здесь фабрика деревянных дверей инкапсулировала carpenter и wooden door, фабрика стальных дверей — iron door and welder. То есть можно быть уверенными, что для каждой из созданных дверей мы получим правильного специалиста.


Когда использовать?


Когда у вас есть взаимосвязи с не самой простой логикой создания (creation logic).


image Строитель


Аналогия


Допустим, вы пришли в забегаловку, заказали бургер дня, и вам выдали его без вопросов. Это пример «Простой фабрики». Но иногда логика создания состоит из большего количества шагов. К примеру, при заказе бургера дня есть несколько вариантов хлеба, начинки, соусов, дополнительных ингредиентов. В таких ситуациях помогает шаблон «Строитель».


Вкратце


Шаблон позволяет создавать разные свойства объекта, избегая загрязнения конструктора (constructor pollution). Это полезно, когда у объекта может быть несколько свойств. Или когда создание объекта состоит из большого количества этапов.


Википедия


Шаблон «Строитель» предназначен для поиска решения проблемы антипаттерна Telescoping constructor.

Поясню, что такое антипаттерн Telescoping constructor. Каждый из нас когда-либо сталкивался с подобным конструктором:


public function __construct($size, $cheese = true, $pepperoni = true, $tomato = false, $lettuce = true)
{
}

Как видите, количество параметров может быстро разрастись, и станет трудно разобраться в их структуре. Кроме того, этот список параметров будет расти и дальше, если в будущем вы захотите добавить новые опции. Это и есть антипаттерн Telescoping constructor.


Пример


Разумная альтернатива — шаблон «Строитель». Сначала создадим бургер:


class Burger
{
    protected $size;

    protected $cheese = false;
    protected $pepperoni = false;
    protected $lettuce = false;
    protected $tomato = false;

    public function __construct(BurgerBuilder $builder)
    {
        $this->size = $builder->size;
        $this->cheese = $builder->cheese;
        $this->pepperoni = $builder->pepperoni;
        $this->lettuce = $builder->lettuce;
        $this->tomato = $builder->tomato;
    }
}

А затем добавим «строителя»:


class BurgerBuilder
{
    public $size;

    public $cheese = false;
    public $pepperoni = false;
    public $lettuce = false;
    public $tomato = false;

    public function __construct(int $size)
    {
        $this->size = $size;
    }

    public function addPepperoni()
    {
        $this->pepperoni = true;
        return $this;
    }

    public function addLettuce()
    {
        $this->lettuce = true;
        return $this;
    }

    public function addCheese()
    {
        $this->cheese = true;
        return $this;
    }

    public function addTomato()
    {
        $this->tomato = true;
        return $this;
    }

    public function build(): Burger
    {
        return new Burger($this);
    }
}

Использование:


$burger = (new BurgerBuilder(14))
                    ->addPepperoni()
                    ->addLettuce()
                    ->addTomato()
                    ->build();

Когда использовать?


Когда у объекта может быть несколько свойств и когда нужно избежать Telescoping constructor. Ключевое отличие от шаблона «Простая фабрика»: он используется в одноэтапном создании, а «Строитель» — в многоэтапном.


image Прототип


Аналогия


Помните клонированную овечку Долли? Так вот, этот шаблон проектирования как раз посвящён клонированию.


Вкратце


Объект создаётся посредством клонирования существующего объекта.


Википедия


Шаблон «Прототип» используется, когда типы создаваемых объектов определяются экземпляром-прототипом, клонированным для создания новых объектов.

То есть шаблон позволяет дублировать существующий объект и модифицировать копию в соответствии с потребностями. Без заморочек с созданием объекта с нуля и его настройкой.


Пример


В PHP это легко можно сделать с помощью clone:


class Sheep
{
    protected $name;
    protected $category;

    public function __construct(string $name, string $category = 'Mountain Sheep')
    {
        $this->name = $name;
        $this->category = $category;
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setCategory(string $category)
    {
        $this->category = $category;
    }

    public function getCategory()
    {
        return $this->category;
    }
}

Затем можно клонировать так:


$original = new Sheep('Jolly');
echo $original->getName(); // Джолли
echo $original->getCategory(); // Горная овечка

// Клонируйте и модифицируйте, что нужно
$cloned = clone $original;
$cloned->setName('Dolly');
echo $cloned->getName(); // Долли
echo $cloned->getCategory(); // Горная овечка

Также для модификации процедуры клонирования можно обратиться к магическому методу __clone.


Когда использовать?


Когда необходимый объект аналогичен уже существующему или когда создание с нуля дороже клонирования.


image Одиночка


Аналогия


У страны может быть только один президент. Он должен действовать, когда того требуют обстоятельства и долг. В данном случае президент — одиночка.


Вкратце


Шаблон позволяет удостовериться, что создаваемый объект — единственный в своём классе.


Википедия


Шаблон «Одиночка» позволяет ограничивать создание класса единственным объектом. Это удобно, когда для координации действий в рамках системы требуется, чтобы объект был единственным в своём классе.

На самом деле шаблон «Одиночка» считается антипаттерном, не следует им слишком увлекаться. Он необязательно плох и иногда бывает полезен. Но применяйте его с осторожностью, потому что «Одиночка» вносит в приложение глобальное состояние, так что изменение в одном месте может повлиять на все остальные случаи использования, а отлаживать такое — не самое простое занятие. Другие недостатки шаблона: он делает ваш код сильно связанным (tightly coupled), а создание прототипа (mocking) «Одиночки» может быть затруднено.


Пример


Сделайте конструктор приватным, отключите расширения и создайте статическую переменную для хранения экземпляра:


final class President
{
    private static $instance;

    private function __construct()
    {
        // Прячем конструктор
    }

    public static function getInstance(): President
    {
        if (!self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    private function __clone()
    {
        // Отключаем клонирование
    }

    private function __wakeup()
    {
        // Отключаем десериализацию
    }
}

Использование:


$president1 = President::getInstance();
$president2 = President::getInstance();

var_dump($president1 === $president2); // true

Структурные шаблоны проектирования


Вкратце


Эти шаблоны в основном посвящены компоновке объектов (object composition). То есть тому, как сущности могут друг друга использовать. Ещё одно объяснение: структурные шаблоны помогают ответить на вопрос «Как построить программный компонент?»


Википедия


Структурными называют шаблоны, которые облегчают проектирование, определяя простой способ реализации взаимоотношений между сущностями.


image Адаптер


Аналогия


Допустим, у вас на карте памяти есть какие-то картинки. Их нужно перенести на компьютер. Нужен адаптер, совместимый с входным портом компьютера, в который можно вставить карту памяти. В данном примере адаптер — это картридер. Ещё один пример: переходник, позволяющий использовать американский блок питания с российской розеткой. Третий пример: переводчик — это адаптер, соединяющий двух людей, говорящих на разных языках.


Вкратце


Шаблон «Адаптер» позволяет помещать несовместимый объект в обёртку, чтобы он оказался совместимым с другим классом.


Википедия


Шаблон проектирования «Адаптер» позволяет использовать интерфейс существующего класса как другой интерфейс. Этот шаблон часто применяется для обеспечения работы одних классов с другими без изменения их исходного кода.

Пример


Представим себе охотника на львов.


Создадим интерфейс Lion, который реализует все типы львов.


interface Lion
{
    public function roar();
}

class AfricanLion implements Lion
{
    public function roar()
    {
    }
}

class AsianLion implements Lion
{
    public function roar()
    {
    }
}

Охотник должен охотиться на все реализации интерфейса Lion.


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

Добавим теперь дикую собаку WildDog, на которую охотник тоже может охотиться. Но у нас не получится сделать это напрямую, потому что у собаки другой интерфейс. Чтобы она стала совместима с охотником, нужно создать подходящий адаптер.


// Это нужно добавить
class WildDog
{
    public function bark()
    {
    }
}

// Адаптер вокруг собаки сделает её совместимой с охотником
class WildDogAdapter implements Lion
{
    protected $dog;

    public function __construct(WildDog $dog)
    {
        $this->dog = $dog;
    }

    public function roar()
    {
        $this->dog->bark();
    }
}

Теперь WildDog может вступить в игру действие благодаря WildDogAdapter.


$wildDog = new WildDog();
$wildDogAdapter = new WildDogAdapter($wildDog);

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

image Мост


Аналогия


Допустим, у вас есть сайт с несколькими страницами. Вы хотите позволить пользователям менять темы оформления страниц. Как бы вы поступили? Создали множественные копии каждой страницы для каждой темы или просто сделали отдельные темы и подгружали их в соответствии с настройками пользователей? Шаблон «Мост» позволяет реализовать второй подход.


image


Вкратце


Шаблон «Мост» — это предпочтение компоновки наследованию. Подробности реализации передаются из одной иерархии другому объекту с отдельной иерархией.


Википедия


Шаблон «Мост» означает отделение абстракции от реализации, чтобы их обе можно было изменять независимо друг от друга.

Пример


Реализуем вышеописанный пример с веб-страницами. Сделаем иерархию 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 "About page in " . $this->theme->getColor();
    }
}

class Careers implements WebPage
{
    protected $theme;

    public function __construct(Theme $theme)
    {
        $this->theme = $theme;
    }

    public function getContent()
    {
        return "Careers page in " . $this->theme->getColor();
    }
}

Отделим иерархию тем:


interface Theme
{
    public function getColor();
}

class DarkTheme implements Theme
{
    public function getColor()
    {
        return 'Dark Black';
    }
}
class LightTheme implements Theme
{
    public function getColor()
    {
        return 'Off white';
    }
}
class AquaTheme implements Theme
{
    public function getColor()
    {
        return 'Light blue';
    }
}

Обе иерархии:


$darkTheme = new DarkTheme();

$about = new About($darkTheme);
$careers = new Careers($darkTheme);

echo $about->getContent(); // "About page in Dark Black";
echo $careers->getContent(); // "Careers page in Dark Black";

image Компоновщик


Аналогия


Каждая компания состоит из сотрудников. У каждого сотрудника есть одни и те же свойства: зарплата, обязанности, отчётность перед кем-то, субординация...


Вкратце


Шаблон «Компоновщик» позволяет клиентам обрабатывать отдельные объекты в едином порядке.


Википедия


Шаблон «Компоновщик» описывает общий порядок обработки группы объектов, словно это одиночный экземпляр объекта. Суть шаблона — компонование объектов в древовидную структуру для представления иерархии от частного к целому. Шаблон позволяет клиентам одинаково обращаться к отдельным объектам и к группам объектов.

Пример
Вот разные типы сотрудников:


interface Employee
{
    public function __construct(string $name, float $salary);
    public function getName(): string;
    public function setSalary(float $salary);
    public function getSalary(): float;
    public function getRoles(): array;
}

class Developer implements Employee
{
    protected $salary;
    protected $name;

    public function __construct(string $name, float $salary)
    {
        $this->name = $name;
        $this->salary = $salary;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setSalary(float $salary)
    {
        $this->salary = $salary;
    }

    public function getSalary(): float
    {
        return $this->salary;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }
}

class Designer implements Employee
{
    protected $salary;
    protected $name;

    public function __construct(string $name, float $salary)
    {
        $this->name = $name;
        $this->salary = $salary;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setSalary(float $salary)
    {
        $this->salary = $salary;
    }

    public function getSalary(): float
    {
        return $this->salary;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }
}

А вот компания, которая состоит из сотрудников разных типов:


class Organization
{
    protected $employees;

    public function addEmployee(Employee $employee)
    {
        $this->employees[] = $employee;
    }

    public function getNetSalaries(): float
    {
        $netSalary = 0;

        foreach ($this->employees as $employee) {
            $netSalary += $employee->getSalary();
        }

        return $netSalary;
    }
}

Использование:


// Подготовка сотрудников
$john = new Developer('John Doe', 12000);
$jane = new Designer('Jane Doe', 15000);

// Включение их в штат
$organization = new Organization();
$organization->addEmployee($john);
$organization->addEmployee($jane);

echo "Net salaries: " . $organization->getNetSalaries(); // Net Salaries: 27000

image Декоратор


Аналогия


Допустим, у вас свой автосервис, оказывающий различные услуги. Как выставлять клиентам счёт? Добавлять последовательно услуги и их стоимость — и в конце концов получится итоговая сумма к оплате. Здесь каждый тип услуги — это «декоратор».


Вкратце


Шаблон «Декоратор» позволяет во время выполнения динамически изменять поведение объекта, обёртывая его в объект класса «декоратора».


Википедия


Шаблон «Декоратор» позволяет подключать к объекту дополнительное поведение (статически или динамически), не влияя на поведение других объектов того же класса. Шаблон часто используется для соблюдения принципа единственной обязанности (Single Responsibility Principle), поскольку позволяет разделить функциональность между классами для решения конкретных задач.

Пример


Возьмём в качестве примера кофе. Сначала просто реализуем интерфейс:


interface Coffee
{
    public function getCost();
    public function getDescription();
}

class SimpleCoffee implements Coffee
{
    public function getCost()
    {
        return 10;
    }

    public function getDescription()
    {
        return 'Simple coffee';
    }
}

Можно сделать код расширяемым, чтобы при необходимости вносить модификации. Добавим «декораторы»:


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() . ', milk';
    }
}

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() . ', whip';
    }
}

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() . ', vanilla';
    }
}

Теперь приготовим кофе:


$someCoffee = new SimpleCoffee();
echo $someCoffee->getCost(); // 10
echo $someCoffee->getDescription(); // Simple Coffee

$someCoffee = new MilkCoffee($someCoffee);
echo $someCoffee->getCost(); // 12
echo $someCoffee->getDescription(); // Simple Coffee, milk

$someCoffee = new WhipCoffee($someCoffee);
echo $someCoffee->getCost(); // 17
echo $someCoffee->getDescription(); // Simple Coffee, milk, whip

$someCoffee = new VanillaCoffee($someCoffee);
echo $someCoffee->getCost(); // 20
echo $someCoffee->getDescription(); // Simple Coffee, milk, whip, vanilla

image Фасад


Аналогия


Как включить компьютер? Вы скажете: «Нажать кнопку включения». Это потому, что вы используете простой интерфейс, предоставляемый компьютером наружу. А внутри него происходит очень много процессов. Простой интерфейс для сложной подсистемы — это фасад.


Вкратце


Шаблон «Фасад» предоставляет упрощённый интерфейс для сложной подсистемы.


Википедия


«Фасад» — это объект, предоставляющий упрощённый интерфейс для более крупного тела кода, например библиотеки классов.

Пример


Создадим класс computer:


class Computer
{
    public function getElectricShock()
    {
        echo "Ouch!";
    }

    public function makeSound()
    {
        echo "Beep beep!";
    }

    public function showLoadingScreen()
    {
        echo "Loading..";
    }

    public function bam()
    {
        echo "Ready to be used!";
    }

    public function closeEverything()
    {
        echo "Bup bup bup buzzzz!";
    }

    public function sooth()
    {
        echo "Zzzzz";
    }

    public function pullCurrent()
    {
        echo "Haaah!";
    }
}

Теперь «фасад»:


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(); // Ouch! Beep beep! Loading.. Ready to be used!
$computer->turnOff(); // Bup bup buzzz! Haah! Zzzzz

image Приспособленец


Аналогия


Обычно в заведениях общепита чай заваривают не отдельно для каждого клиента, а сразу в некой крупной ёмкости. Это позволяет экономить ресурсы: газ/электричество, время и т. д. Шаблон «Приспособленец» как раз посвящён общему использованию (sharing).


Вкратце


Шаблон применяется для минимизирования использования памяти или вычислительной стоимости за счёт общего использования как можно большего количества одинаковых объектов.


Википедия


«Приспособленец» — это объект, минимизирующий использование памяти за счёт общего с другими, такими же объектами использования как можно большего объёма данных. Это способ применения многочисленных объектов, когда простое повторяющееся представление приведёт к неприемлемому потреблению памяти.

Пример


Сделаем типы чая и чайника.


// Приспособленец — то, что будет закешировано.
// Типы чая здесь — приспособленцы.
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('less sugar', 1);
$shop->takeOrder('more milk', 2);
$shop->takeOrder('without sugar', 5);

$shop->serve();
// Serving tea to table# 1
// Serving tea to table# 2
// Serving tea to table# 5

image Заместитель


Аналогия


Открыть дверь с электронным замком можно с помощью карточки доступа (access card) или кнопки для обхода системы безопасности. То есть основная функциональность двери — открыться, а поверх неё может быть ещё какая-то функциональность — «заместитель».


Вкратце


С помощью шаблона «Заместитель» класс представляет функциональность другого класса.


Википедия


В наиболее общей форме «Заместитель» — это класс, функционирующий как интерфейс к чему-либо. Это оболочка или объект-агент, вызываемый клиентом для получения доступа к другому, «настоящему» объекту. «Заместитель» может просто переадресовывать запросы настоящему объекту, а может предоставлять дополнительную логику: кеширование данных при интенсивном выполнении операций или потреблении ресурсов настоящим объектом; проверка предварительных условий (preconditions) до вызова выполнения операций настоящим объектом.

Пример


Реализуем интерфейс двери и саму дверь:


interface Door
{
    public function open();
    public function close();
}

class LabDoor implements Door
{
    public function open()
    {
        echo "Opening lab door";
    }

    public function close()
    {
        echo "Closing the lab door";
    }
}

Сделаем «заместителя», чтобы дверь могла выполнять защитную функцию:


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 "Big no! It ain't possible.";
        }
    }

    public function authenticate($password)
    {
        return $password === '$ecr@t';
    }

    public function close()
    {
        $this->door->close();
    }
}

Использование:


$door = new Security(new LabDoor());
$door->open('invalid'); // Big no! It ain't possible.

$door->open('$ecr@t'); // Opening lab door
$door->close(); // Closing lab door

Ещё один пример связан с реализацией преобразователя данных (data-mapper). С помощью этого шаблона я недавно сделал ODM (Object Data Mapper) для MongoDB. Я написал «заместителя» вокруг mongo-классов, воспользовавшись волшебным методом __call(). Все вызовы методов проходили к оригинальным mongo-классам через «заместителя», а извлечённые результаты возвращались как есть. Только в случае с find или findOne данные преобразовывались в объекты требуемого класса, которые возвращались вместо Cursor.


Поведенческие шаблоны проектирования


Вкратце


Они связаны с присвоением обязанностей (responsibilities) объектам. От структурных шаблонов они отличаются тем, что не просто описывают структуру, но и очерчивают шаблоны передачи данных, обеспечения взаимодействия. То есть поведенческие шаблоны позволяют ответить на вопрос «Как реализовать поведение в программном компоненте?»


Википедия


Поведенческие шаблоны проектирования определяют алгоритмы и способы реализации взаимодействия различных объектов и классов. Они обеспечивают гибкость взаимодействия между объектами.


image Цепочка ответственности


Аналогия


Допустим, для вашего банковского счёта доступны три способа оплаты (A, B и C). Каждый подразумевает разные доступные суммы денег: A — 100 долларов, B — 300, C — 1000. Приоритетность способов при оплате: А, затем В, затем С. Вы пытаетесь купить что-то за 210 долларов. На основании «цепочки ответственности» система попытается оплатить способом А. Если денег хватает — то оплата проходит, а цепочка прерывается. Если денег не хватает — то система переходит к способу В, и т. д.


Вкратце


Шаблон «Цепочка ответственности» позволяет создавать цепочки объектов. Запрос входит с одного конца цепочки и движется от объекта к объекту, пока не будет найден подходящий обработчик.


Википедия


Шаблон «Цепочка ответственности» содержит исходный управляющий объект и ряд обрабатывающих объектов. Каждый обрабатывающий объект содержит логику, определяющую типы командных объектов, которые он может обрабатывать, а остальные передаются по цепочке следующему обрабатывающему объекту.

Пример


Создадим основной банковский счёт, содержащий логику связывания счетов в цепочки, и сами счета.


abstract class Account
{
    protected $successor;
    protected $balance;

    public function setNext(Account $account)
    {
        $this->successor = $account;
    }

    public function pay(float $amountToPay)
    {
        if ($this->canPay($amountToPay)) {
            echo sprintf('Paid %s using %s' . PHP_EOL, $amountToPay, get_called_class());
        } elseif ($this->successor) {
            echo sprintf('Cannot pay using %s. Proceeding ..' . PHP_EOL, get_called_class());
            $this->successor->pay($amountToPay);
        } else {
            throw new Exception('None of the accounts have enough balance');
        }
    }

    public function canPay($amount): bool
    {
        return $this->balance >= $amount;
    }
}

class Bank extends Account
{
    protected $balance;

    public function __construct(float $balance)
    {
        $this->balance = $balance;
    }
}

class Paypal extends Account
{
    protected $balance;

    public function __construct(float $balance)
    {
        $this->balance = $balance;
    }
}

class Bitcoin extends Account
{
    protected $balance;

    public function __construct(float $balance)
    {
        $this->balance = $balance;
    }
}

Теперь с помощью определённых выше линков (Bank, Paypal, Bitcoin) подготовим цепочку:


// Сделаем такую цепочку
//      $bank->$paypal->$bitcoin
//
// Приоритет у банка
//      Если банк не может оплатить, переходим к Paypal
//      Если Paypal не может, переходим к Bitcoin

$bank = new Bank(100);          // У банка баланс 100
$paypal = new Paypal(200);      // У Paypal баланс 200
$bitcoin = new Bitcoin(300);    // У Bitcoin баланс 300

$bank->setNext($paypal);
$paypal->setNext($bitcoin);

// Начнём с банка
$bank->pay(259);

// Выходной вид
// ==============
// Нельзя оплатить с помощью банка. Обрабатываю...
// Нельзя оплатить с помощью Paypal. Обрабатываю...
// Оплачено 259 с помощью Bitcoin!

image Команда


Аналогия


Вы пришли в ресторан. Вы (Client) просите официанта (Invoker) принести блюда (Command). Официант перенаправляет запрос шеф-повару (Receiver), который знает, что и как готовить. Другой пример: вы (Client) включаете (Command) телевизор (Receiver) с помощью пульта (Invoker).


Вкратце


Шаблон «Команда» позволяет инкапсулировать действия в объекты. Ключевая идея — предоставить средства отделения клиента от получателя.


Википедия


В шаблоне «Команда» объект используется для инкапсуляции всей информации, необходимой для выполнения действия либо для его инициирования позднее. Информация включает в себя имя метода; объект, владеющий методом; значения параметров метода.

Пример


Сначала сделаем получателя, содержащего реализации каждого действия, которое может быть выполнено.


// Receiver
class Bulb
{
    public function turnOn()
    {
        echo "Bulb has been lit";
    }

    public function turnOff()
    {
        echo "Darkness!";
    }
}

Теперь сделаем интерфейс, который будет реализовывать каждая команда. Также сделаем набор команд.


interface Command
{
    public function execute();
    public function undo();
    public function redo();
}

// Command
class TurnOn implements Command
{
    protected $bulb;

    public function __construct(Bulb $bulb)
    {
        $this->bulb = $bulb;
    }

    public function execute()
    {
        $this->bulb->turnOn();
    }

    public function undo()
    {
        $this->bulb->turnOff();
    }

    public function redo()
    {
        $this->execute();
    }
}

class TurnOff implements Command
{
    protected $bulb;

    public function __construct(Bulb $bulb)
    {
        $this->bulb = $bulb;
    }

    public function execute()
    {
        $this->bulb->turnOff();
    }

    public function undo()
    {
        $this->bulb->turnOn();
    }

    public function redo()
    {
        $this->execute();
    }
}

Теперь сделаем вызывающего Invoker, с которым будет взаимодействовать клиент для обработки команд.


// Invoker
class RemoteControl
{
    public function submit(Command $command)
    {
        $command->execute();
    }
}

Посмотрим, как всё это может использовать клиент:


$bulb = new Bulb();

$turnOn = new TurnOn($bulb);
$turnOff = new TurnOff($bulb);

$remote = new RemoteControl();
$remote->submit($turnOn); // Лампочка зажглась!
$remote->submit($turnOff); // Темнота!

Шаблон «Команда» можно использовать и для реализации системы на основе транзакций. То есть системы, в которой вы сохраняете историю команд по мере их выполнения. Если последняя команда выполнена успешно, то всё хорошо. В противном случае система итерирует по истории и делает undo для всех выполненных команд.


image Итератор


Аналогия


Хороший пример — радиоприёмник. Вы начинаете с какой-то радиостанции, а затем перемещаетесь по станциям вперёд/назад. То есть устройство предоставляет интерфейс для итерирования по каналам.


Вкратце


Шаблон — это способ доступа к элементам объекта без раскрытия базового представления.


Википедия


В этом шаблоне итератор используется для перемещения по контейнеру и обеспечения доступа к элементам контейнера. Шаблон подразумевает отделение алгоритмов от контейнера. В каких-то случаях алгоритмы, специфичные для этого контейнера, не могут быть отделены.

Пример


В PHP довольно легко реализовать этот шаблон с помощью стандартной библиотеки PHP. Сначала создадим радиостанцию RadioStation.


class RadioStation
{
    protected $frequency;

    public function __construct(float $frequency)
    {
        $this->frequency = $frequency;
    }

    public function getFrequency(): float
    {
        return $this->frequency;
    }
}

Теперь создадим итератор:


use Countable;
use Iterator;

class StationList implements Countable, Iterator
{
    /** @var RadioStation[] $stations */
    protected $stations = [];

    /** @var int $counter */
    protected $counter;

    public function addStation(RadioStation $station)
    {
        $this->stations[] = $station;
    }

    public function removeStation(RadioStation $toRemove)
    {
        $toRemoveFrequency = $toRemove->getFrequency();
        $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) {
            return $station->getFrequency() !== $toRemoveFrequency;
        });
    }

    public function count(): int
    {
        return count($this->stations);
    }

    public function current(): RadioStation
    {
        return $this->stations[$this->counter];
    }

    public function key()
    {
        return $this->counter;
    }

    public function next()
    {
        $this->counter++;
    }

    public function rewind()
    {
        $this->counter = 0;
    }

    public function valid(): bool
    {
        return isset($this->stations[$this->counter]);
    }
}

Использование:


$stationList = new StationList();

$stationList->addStation(new RadioStation(89));
$stationList->addStation(new RadioStation(101));
$stationList->addStation(new RadioStation(102));
$stationList->addStation(new RadioStation(103.2));

foreach($stationList as $station) {
    echo $station->getFrequency() . PHP_EOL;
}

$stationList->removeStation(new RadioStation(89)); // Will remove station 89

image Посредник


Аналогия


Когда вы говорите с кем-то по мобильнику, то между вами и собеседником находится мобильный оператор. То есть сигнал передаётся через него, а не напрямую. В данном примере оператор — посредник.


Вкратце


Шаблон «Посредник» подразумевает добавление стороннего объекта («посредника») для управления взаимодействием между двумя объектами («коллегами»). Шаблон помогает уменьшить связанность (coupling) классов, общающихся друг с другом, ведь теперь они не должны знать о реализациях своих собеседников.


Википедия


Шаблон определяет объект, который инкапсулирует способ взаимодействия набора объектов.

Пример


Простейший пример: чат («посредник»), в котором пользователи («коллеги») отправляют друг другу сообщения.


Создадим «посредника»:


interface ChatRoomMediator 
{
    public function showMessage(User $user, string $message);
}

// Посредник
class ChatRoom implements ChatRoomMediator
{
    public function showMessage(User $user, string $message)
    {
        $time = date('M d, y H:i');
        $sender = $user->getName();

        echo $time . '[' . $sender . ']:' . $message;
    }
}

Теперь создадим «коллег»:


class User {
    protected $name;
    protected $chatMediator;

    public function __construct(string $name, ChatRoomMediator $chatMediator) {
        $this->name = $name;
        $this->chatMediator = $chatMediator;
    }

    public function getName() {
        return $this->name;
    }

    public function send($message) {
        $this->chatMediator->showMessage($this, $message);
    }
}

Использование:


$mediator = new ChatRoom();

$john = new User('John Doe', $mediator);
$jane = new User('Jane Doe', $mediator);

$john->send('Hi there!');
$jane->send('Hey!');

// Выходной вид
// Feb 14, 10:58 [John]: Hi there!
// Feb 14, 10:58 [Jane]: Hey!

image Хранитель


Аналогия


В качестве примера можно привести калькулятор («создатель»), у которого любая последняя выполненная операция сохраняется в памяти («хранитель»), чтобы вы могли снова вызвать её с помощью каких-то кнопок («опекун»).


Вкратце


Шаблон «Хранитель» фиксирует и хранит текущее состояние объекта, чтобы оно легко восстанавливалось.


Википедия


Шаблон «Хранитель» позволяет восстанавливать объект в его предыдущем состоянии (отмена через откат — undo via rollback).

Обычно шаблон применяется, когда нужно реализовать функциональность отмены операции.


Пример


Текстовый редактор время от времени сохраняет своё состояние, чтобы можно было восстановить текст в каком-то прошлом виде.


Сначала создадим объект «хранитель», в котором можно сохранять состояние редактора.


class EditorMemento
{
    protected $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getContent()
    {
        return $this->content;
    }
}

Теперь сделаем редактор («создатель»), который будет использовать объект «хранитель».


class Editor
{
    protected $content = '';

    public function type(string $words)
    {
        $this->content = $this->content . ' ' . $words;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function save()
    {
        return new EditorMemento($this->content);
    }

    public function restore(EditorMemento $memento)
    {
        $this->content = $memento->getContent();
    }
}

Использование:


$editor = new Editor();

// Пишем что-нибудь
$editor->type('This is the first sentence.');
$editor->type('This is second.');

// Сохранение состояния в: This is the first sentence. This is second.
$saved = $editor->save();

// Пишем ещё
$editor->type('And this is third.');

// Output: Содержимое до сохранения
echo $editor->getContent(); // This is the first sentence. This is second. And this is third.

// Восстанавливаем последнее сохранённое состояние
$editor->restore($saved);

$editor->getContent(); // This is the first sentence. This is second.

image Наблюдатель


Аналогия


Хороший пример: люди, ищущие работу, подписываются на публикации на сайтах вакансий и получают уведомления, когда появляются вакансии, подходящие по параметрам.


Вкратце


Шаблон определяет зависимость между объектами, чтобы при изменении состояния одного из них его «подчинённые» узнавали об этом.


Википедия


В шаблоне «Наблюдатель» есть объект («субъект»), ведущий список своих «подчинённых» («наблюдателей») и автоматически уведомляющий их о любом изменении своего состояния, обычно с помощью вызова одного из их методов.

Пример


Сначала реализуем людей, ищущих работу, которых нужно уведомлять о появлении вакансий.


class JobPost
{
    protected $title;

    public function __construct(string $title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }
}

class JobSeeker implements Observer
{
    protected $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function onJobPosted(JobPost $job)
    {
        // Do something with the job posting
        echo 'Hi ' . $this->name . '! New job posted: '. $job->getTitle();
    }
}

Теперь реализуем публикации вакансий, на которые люди будут подписываться.


class JobPostings implements Observable
{
    protected $observers = [];

    protected function notify(JobPost $jobPosting)
    {
        foreach ($this->observers as $observer) {
            $observer->onJobPosted($jobPosting);
        }
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function addJob(JobPost $jobPosting)
    {
        $this->notify($jobPosting);
    }
}

Использование:


// Создаём подписчиков
$johnDoe = new JobSeeker('John Doe');
$janeDoe = new JobSeeker('Jane Doe');

// Создаём публикатора и прикрепляем подписчиков
$jobPostings = new JobPostings();
$jobPostings->attach($johnDoe);
$jobPostings->attach($janeDoe);

// Добавляем новую вакансию и смотрим, будут ли уведомлены подписчики
$jobPostings->addJob(new JobPost('Software Engineer'));

// Output
// Hi John Doe! New job posted: Software Engineer
// Hi Jane Doe! New job posted: Software Engineer

image Посетитель


Аналогия


Туристы собрались в Дубай. Сначала им нужен способ попасть туда (виза). После прибытия они будут посещать любую часть города, не спрашивая разрешения, ходить где вздумается. Просто скажите им о каком-нибудь месте — и туристы могут там побывать. Шаблон «Посетитель» помогает добавлять места для посещения.


Вкратце


Шаблон «Посетитель» позволяет добавлять будущие операции для объектов без их модифицирования.


Википедия


Шаблон «Посетитель» — это способ отделения алгоритма от структуры объекта, в которой он оперирует. Результат отделения — возможность добавлять новые операции в существующие структуры объектов без их модифицирования. Это один из способов соблюдения принципа открытости/закрытости (open/closed principle).

Пример


Возьмём зоопарк: у нас есть несколько видов животных, и нам нужно послушать издаваемые ими звуки.


// Место посещения
interface Animal
{
    public function accept(AnimalOperation $operation);
}

// Посетитель
interface AnimalOperation
{
    public function visitMonkey(Monkey $monkey);
    public function visitLion(Lion $lion);
    public function visitDolphin(Dolphin $dolphin);
}

Реализуем животных:


class Monkey implements Animal
{
    public function shout()
    {
        echo 'Ooh oo aa aa!';
    }

    public function accept(AnimalOperation $operation)
    {
        $operation->visitMonkey($this);
    }
}

class Lion implements Animal
{
    public function roar()
    {
        echo 'Roaaar!';
    }

    public function accept(AnimalOperation $operation)
    {
        $operation->visitLion($this);
    }
}

class Dolphin implements Animal
{
    public function speak()
    {
        echo 'Tuut tuttu tuutt!';
    }

    public function accept(AnimalOperation $operation)
    {
        $operation->visitDolphin($this);
    }
}

Реализуем посетителя:


class Speak implements AnimalOperation
{
    public function visitMonkey(Monkey $monkey)
    {
        $monkey->shout();
    }

    public function visitLion(Lion $lion)
    {
        $lion->roar();
    }

    public function visitDolphin(Dolphin $dolphin)
    {
        $dolphin->speak();
    }
}

Использование:


$monkey = new Monkey();
$lion = new Lion();
$dolphin = new Dolphin();

$speak = new Speak();

$monkey->accept($speak);    // Уа-уа-уааааа!    
$lion->accept($speak);      // Ррррррррр!
$dolphin->accept($speak);   // Туут тутт туутт!

Это можно было сделать просто с помощью иерархии наследования, но тогда пришлось бы модифицировать животных при каждом добавлении к ним новых действий. А здесь менять их не нужно. Например, мы можем добавить животным прыжки, просто создав нового посетителя:


class Jump implements AnimalOperation
{
    public function visitMonkey(Monkey $monkey)
    {
        echo 'Jumped 20 feet high! on to the tree!';
    }

    public function visitLion(Lion $lion)
    {
        echo 'Jumped 7 feet! Back on the ground!';
    }

    public function visitDolphin(Dolphin $dolphin)
    {
        echo 'Walked on water a little and disappeared';
    }
}

Использование:


$jump = new Jump();

$monkey->accept($speak);   // Ooh oo aa aa!
$monkey->accept($jump);    // Jumped 20 feet high! on to the tree!

$lion->accept($speak);     // Roaaar!
$lion->accept($jump);      // Jumped 7 feet! Back on the ground!

$dolphin->accept($speak);  // Tuut tutt tuutt!
$dolphin->accept($jump);   // Walked on water a little and disappeared

image Стратегия


Аналогия


Возьмём пример с пузырьковой сортировкой. Мы её реализовали, но с ростом объёмов данных сортировка стала выполняться очень медленно. Тогда мы сделали быструю сортировку (Quick sort). Алгоритм работает быстрее на больших объёмах, но на маленьких он очень медленный. Тогда мы реализовали стратегию, при которой для маленьких объёмов данных используется пузырьковая сортировка, а для больших — быстрая.


Вкратце


Шаблон «Стратегия» позволяет переключаться между алгоритмами или стратегиями в зависимости от ситуации.


Википедия


Шаблон «Стратегия» позволяет при выполнении выбирать поведение алгоритма.

Пример


Возьмём вышеописанный пример. Сначала сделаем интерфейс стратегии и реализации самих стратегий.


interface SortStrategy
{
    public function sort(array $dataset): array;
}

class BubbleSortStrategy implements SortStrategy
{
    public function sort(array $dataset): array
    {
        echo "Sorting using bubble sort";

        // Do sorting
        return $dataset;
    }
}

class QuickSortStrategy implements SortStrategy
{
    public function sort(array $dataset): array
    {
        echo "Sorting using quick sort";

        // Do sorting
        return $dataset;
    }
}

Теперь реализуем клиента, который будет использовать нашу стратегию.


class Sorter
{
    protected $sorter;

    public function __construct(SortStrategy $sorter)
    {
        $this->sorter = $sorter;
    }

    public function sort(array $dataset): array
    {
        return $this->sorter->sort($dataset);
    }
}

Использование:


$dataset = [1, 5, 4, 3, 2, 8];

$sorter = new Sorter(new BubbleSortStrategy());
$sorter->sort($dataset); // Output : Пузырьковая сортировка

$sorter = new Sorter(new QuickSortStrategy());
$sorter->sort($dataset); // Output : Быстрая сортировка

image Состояние


Аналогия


Допустим, в графическом редакторе вы выбрали инструмент «Кисть». Она меняет своё поведение в зависимости от настройки цвета: т. е. рисует линию выбранного цвета.


Вкратце


Шаблон позволяет менять поведение класса при изменении состояния.


Википедия


Шаблон «Состояние» реализует машину состояний объектно ориентированным способом. Это достигается с помощью:
  • реализации каждого состояния в виде производного класса интерфейса шаблона «Состояние»,
  • реализации переходов состояний (state transitions) посредством вызова методов, определённых вышестоящим классом (superclass).


Шаблон «Состояние» — это в некотором плане шаблон «Стратегия», при котором возможно переключение текущей стратегии с помощью вызова методов, определённых в интерфейсе шаблона.

Пример


Текстовый редактор меняет состояние текста, который вы печатаете, т. е. если выбрано полужирное начертание — то редактор печатает полужирным и т. д.


Сначала сделаем интерфейс состояний и сами состояния:


interface WritingState
{
    public function write(string $words);
}

class UpperCase implements WritingState
{
    public function write(string $words)
    {
        echo strtoupper($words);
    }
}

class LowerCase implements WritingState
{
    public function write(string $words)
    {
        echo strtolower($words);
    }
}

class Default implements WritingState
{
    public function write(string $words)
    {
        echo $words;
    }
}

Сделаем редактор:


class TextEditor
{
    protected $state;

    public function __construct(WritingState $state)
    {
        $this->state = $state;
    }

    public function setState(WritingState $state)
    {
        $this->state = $state;
    }

    public function type(string $words)
    {
        $this->state->write($words);
    }
}

Использование:


$editor = new TextEditor(new Default());

$editor->type('First line');

$editor->setState(new UpperCase());

$editor->type('Second line');
$editor->type('Third line');

$editor->setState(new LowerCase());

$editor->type('Fourth line');
$editor->type('Fifth line');

// Output:
// First line
// SECOND LINE
// THIRD LINE
// fourth line
// fifth line

image Шаблонный метод


Аналогия


Допустим, вы собрались строить дома. Этапы будут такими:


  • Подготовка фундамента.
  • Возведение стен.
  • Настил крыши.
  • Настил перекрытий.

Порядок этапов никогда не меняется. Вы не настелите крышу до возведения стен — и т. д. Но каждый этап модифицируется: стены, например, можно возвести из дерева, кирпича или газобетона.


Вкратце


«Шаблонный метод» определяет каркас выполнения определённого алгоритма, но реализацию самих этапов делегирует дочерним классам.


Википедия


«Шаблонный метод» — это поведенческий шаблон, определяющий основу алгоритма и позволяющий наследникам переопределять некоторые шаги алгоритма, не изменяя его структуру в целом.

Пример


Допустим, у нас есть программный инструмент, позволяющий тестировать, проводить контроль качества кода (lint), выполнять сборку, генерировать отчёты сборки (отчёты о покрытии кода, о качестве кода и т. д.), а также развёртывать приложение на тестовом сервере.


Сначала наш базовый класс определяет каркас алгоритма сборки.


abstract class Builder
{

    // Шаблонный метод
    final public function build()
    {
        $this->test();
        $this->lint();
        $this->assemble();
        $this->deploy();
    }

    abstract public function test();
    abstract public function lint();
    abstract public function assemble();
    abstract public function deploy();
}

Теперь создаём реализации:


class AndroidBuilder extends Builder
{
    public function test()
    {
        echo 'Running android tests';
    }

    public function lint()
    {
        echo 'Linting the android code';
    }

    public function assemble()
    {
        echo 'Assembling the android build';
    }

    public function deploy()
    {
        echo 'Deploying android build to server';
    }
}

class IosBuilder extends Builder
{
    public function test()
    {
        echo 'Running ios tests';
    }

    public function lint()
    {
        echo 'Linting the ios code';
    }

    public function assemble()
    {
        echo 'Assembling the ios build';
    }

    public function deploy()
    {
        echo 'Deploying ios build to server';
    }
}

Использование:


$androidBuilder = new AndroidBuilder();
$androidBuilder->build();

// Output:
// Выполнение Android-тестов
// Линтинг Android-кода
// Создание Android-сборки
// Развёртывание Android-сборки на сервере

$iosBuilder = new IosBuilder();
$iosBuilder->build();

// Output:
// Выполнение iOS-тестов
// Линтинг iOS-кода
// Создание iOS-сборки
// Развёртывание iOS-сборки на сервере

image Закругляемся


На этом обзор закончен. Я продолжу его улучшать и планирую написать главы про архитектурные шаблоны. Так что периодически заглядывайте.

Mail.Ru Group 768,16
Строим Интернет
Поделиться публикацией
Похожие публикации
Комментарии 92
  • +19
    mail.ru радуют подобными статями. карму отмывают)
    • 0
      Плюсую вообще; а статься действительно интересная, я сейчас студента своего заставлю читать:)
    • –12
      Если это перевод, где ссылка на первоисточник?
      • +6
        Внизу, там где указан автор.
        • +1
          вот так спрашиваешь без задних мыслей, а тебе карму спускают… разве это честно? не понимаю я людей, как буд-то обидел кого-то
          • +2
            В 90% переводов найдется человек, попросивший ссылку на первоисточник.
            А однообразные комментарии раздражают.
      • +1
        Есть отличная книга по паттернам с отличным описанием и аналогиями. Не сочтите за рекламу.

        Совсем недавно по мотивам этой книги создал репозиторий с имплементацией базовых паттернов (не все, что описаны в книге, только на сколько хватило энтузиазма) на php. Не сочтите за рекламу 2.
        • +1
          Раз уж Вы затронули тему книжек по паттернам, нельзя не упомянуть библию: Design Patterns: Elements of Reusable Object-Oriented Software.
          • 0
            Если уж речь идет о паттернах с человеческим лицом, то «человечнее» лица, чем у Фрименов, сложно придумать. Труд Банды Четырех — это все же больше справочник.
          • 0
            А вот же репозиторий с паттернами на PHP https://github.com/domnikl/DesignPatternsPHP
          • 0
            а почему Interpreter пропустили?
            • 0
              Пример на Python
              """
              Define a represention for a grammar of the given language along with an
              interpreter that uses the representation to interpret sentences in the
              language.
              """
              
              import abc
              
              
              class AbstractExpression(metaclass=abc.ABCMeta):
                  """
                  Declare an abstract Interpret operation that is common to all nodes
                  in the abstract syntax tree.
                  """
              
                  @abc.abstractmethod
                  def interpret(self):
                      pass
              
              
              class NonterminalExpression(AbstractExpression):
                  """
                  Implement an Interpret operation for nonterminal symbols in the grammar.
                  """
              
                  def __init__(self, expression):
                      self._expression = expression
              
                  def interpret(self):
                      self._expression.interpret()
              
              
              class TerminalExpression(AbstractExpression):
                  """
                  Implement an Interpret operation associated with terminal symbols in
                  the grammar.
                  """
              
                  def interpret(self):
                      pass
              
              
              def main():
                  abstract_syntax_tree = NonterminalExpression(TerminalExpression())
                  abstract_syntax_tree.interpret()
              
              
              if __name__ == "__main__":
                  main()
              
              


              • 0
                при чем тут ваш пример на Python?
              • 0
                Шаблоны проектирования также заслуживающие внимания:


                • +1

                  Ну первое это вообще больше техника рефакторинга. Нет?

                  • 0
                    да, шаблонов заслуживающих внимания много, но здесь речь идет о шаблонах GOF. Я не считаю важным паттерн Interpreter посто он входит в список от GOF. Поэтому суть вопроса в том, что если описывают шаблоны GOF тогда почему пропустили Interpreter?
                • +1
                  А почему именно PHP? Хочу такую же для Python c использованием особенностей языка)
                • +2
                  Добавим теперь дикую собаку WildDog, на которую охотник тоже может охотиться. Но у нас не получится сделать это напрямую, потому что у собаки другой интерфейс. Чтобы она стала совместима с охотником, нужно создать подходящий адаптер.
                  // Адаптер вокруг собаки сделает её совместимой с охотником

                  Адаптер для собаки..) Прям очень улыбнуло)
                  • +13
                    Было бы прекрасно добавить еще и английские(оригинальные) варианты названий шаблонов проектирования, чтобы при чтении зарубежной литературы и статей не гадать, что к чему. Не всегда они переводятся однозначно с русского языка на английский, что может вызвать диссонанс. Например «Flyweight» — так сразу и не скажешь, что это «Приспособленец».
                    • 0
                      Во-во, я тоже долго пытался понять, что за «приспособленец» такой. Что за надмозг переводил этот термин (претензия не к переводчику статьи, в вики то же самое)? К чему этот приспособленец присособляется, если это вообще неизменяемый (immutable) объёкт? Почему нельзя было взять прямой и адекватный перевод «легковес»? Flyweight — это название весовой категории в боксе, а как паттерн означает именно максимально облегчённый и дешёвый объект.
                    • +2
                      В дополнение к статье – https://github.com/domnikl/DesignPatternsPHP. Отличный сборник шаблонов с примерами, тестами и иллюстрациями.
                      • +1
                        Сергей Тепляков, Паттерны проектирования на платформе .NET

                        Современно, понятно, разобраны плюсы и минусы каждого паттерна, плюс прикладное использование. Подходит для любого языка.
                        • 0
                          Хоть эта фраза про «любой язык» даже в предисловии упоминается, но, надо признать, примеры слишком специфичны для .NET. Особенно лирические отступления. Потому что книга Теплякова (а она хороша, спору нет) — это не обучающий материал и даже не справочник, а дискуссия на тему особенностей реализации классических паттернов на C#, что весьма ограничивает аудиторию.
                        • +2

                          Шаблоны мне не нравятся тем, что их всегда подают под неправильным соусом. Лично я не согласен с:


                          Шаблоны проектирования — это способ решения периодически возникающих проблем. Точнее, это руководства по решению конкретных проблем

                          Шаблоны проектирования — это не способ решения проблем, это способ записи решения проблемы на объектно-ориентированном языке программирования в некотором устоявшемся виде. Оформление кода в соответствии с шаблоном, а не использование велосипеда, делает код более структурированным и читаемым, только и всего.

                          • 0

                            Ну почему же. Просто возникли они в ООП среде и исторически большинство каноничных примеров на соответствующих языках. Но концептуально-то, если есть задача что-то, например, порождать, фабрику можно реализовать и в виде функции. Если воспринимать шаблоны, как концепции решения абстрактных задач программирования, то не так уж они и привязаны к языкам и парадигмам. Шаблоны, это скорее про разделение ответственности между подпрограммами, ради более прозрачной и гибкой архитектуры. Но к этому стремились и до, и после появления ООП языков.

                            • +5

                              Паттерны настолько элементарны, что любой программист, не знающий их, все равно будет их использовать. Знание паттернов просто позволяет оформлять код в универсальном виде.

                              • 0
                                А как вам мода — напишите на листочке реализацию фасада, на собеседованиях? Повсемастная истерика с паттернами уже порядком поднадоела.
                                • 0

                                  А что в данном случае значит "написать реализацию фасада"? Самому придумать пару классов и завернуть их в класс-фасад? Или что-то другое?

                                  • 0
                                    Да, именно то, что вы подумали.
                                    • 0

                                      Прикольно, и ведь выбрали один из самых абстрактных шаблонов. Видно совсем у народа вера в кандидатов подточена.

                                  • +3
                                    А как вам мода — напишите на листочке реализацию фасада, на собеседованиях

                                    А это результат того, что в резюме пишут: «знаю Design Patterns», а потом не могут назвать ни одного примера (хоть название)
                                    • 0
                                      Мы рассматриваем разные крайности одной сущности, знать паттерны и понимать не значит наизусть помнить реализацию их.
                                      • +2

                                        Реализацию шаблона нельзя помнить наизусть, на то это и шаблон. Вы либо понимаете что это за шаблон и можете написать в нужном месте конкретную реализацию этого шаблона, либо не понимаете и не можете написать. Запоминать конкретные реализации — это все равно что запоминать конкретную программу или скрипт — нафиг никому не нужно.

                                        • 0
                                          Примерно то же самое я и имел ввиду, с поправкой на то, что для меня — это не более чем общие названия для макро конструкций и ОДИН ИЗ способов решать проблемы.
                                        • 0

                                          Паттерны на то и паттерны, что у них нет образцовой реализации, это просто шаблоны по которым должны строиться конкретные решения конкретных задач, чтобы иметь право называться реализациями того или иного паттерна. Соответствует решение шаблону (прежде всего по списку участников и типов взаимодействия между ними) — решение реализует данный паттерн. Не соответствует — нешаблонное решение или реализует другой паттерн, например вместо фасада реализует адаптер или прокси.

                                      • +2
                                        напишите на листочке реализацию фасада, на собеседованиях?

                                        сразу уточню что подразумевается. Вдруг интервьювер слишком много писал на ларавели и у нас различается понимание этого слова.


                                        А так — я чаще просто спрашиваю чем адаптер от декоратора отличается. Бывает весьма интересно послушать. В целом мало кто пытается строить связь даже между названием паттерна и его предназначением… просто заучивают. Грустно. При том что пользы от этого ноль.

                                        • 0
                                          Я не пишу на PHP. Unity, .NET — мой стэк
                                      • +1

                                        Насколько я помню, идея GoF была в том, чтобы создать глоссарий для облегчения коммуникации между программистами… а не в том, чтобы кого-то учить программировать.

                                        • 0
                                          Спасибо, я наконец понял, зачем про это что-то пишут вообще.
                                        • +1
                                          Просто возникли они в ООП среде и исторически большинство каноничных примеров на соответствующих языках.

                                          Многие паттерны созданы для преодоления проблем ООП, которых в, например, ФП просто нет. Более того, некоторые паттерны созданы для решения проблем исключительно статически типизируемых ООП языков и в языках типа PHP, Ruby, Python и JS решаются нативно в рантайме.

                                          • 0

                                            Но суть то в том что есть проблемы, не важно где. и есть типичные решения. А паттерны — это лишь названия этих типичных решений типичных проблем.

                                      • +10

                                        Про Компоновщик неверно.
                                        Суть данного шаблона в том, что и одиночные объекты, и композит (компоновщик) реализуют один и тот же интерфейс. Именно это позволяет клиенту работать с группой объектов (инкапсулированной в компоновщике) точно так же, как и с одиночным объектом.


                                        В примере же, приведенном в статье, класс Organization не реализует интерфейс 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);
                                          }
                                        }
                                        
                                        // Использование:
                                        
                                        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);
                                        • +1
                                          Тоже заметил данную неточность. Еще по моему мнению пример декоратора также не верен, так как не показывает основное отличие декоратора и прокси, а именно добавление нового поведения/функционал к объекту. В примере показан обычный прокси. Классический пример декоратора div/table renderer для элементов формы имеет более «человеческое» лицо
                                          • +1
                                            кстати хороший пример composite это symfony form компонент, там как раз отдельный элемент формы и форма реализуют один интерфейс (методы setData, submit etc.)
                                            • +2
                                              Аналогично с состоянием. Вместо этого у автора пример стратегии. ИМХО, в качестве хорошего примера подойдет что-то детерминированное. Например, нам в университете замечательно иллюстрировали состояние на примере телефона:

                                              // интерфейс-состояние
                                              interface IPhoneState {
                                                  pickUp():IPhoneState;
                                                  hangUp():IPhoneState;
                                                  dial():IPhoneState;
                                              }
                                              
                                              // несколько реализаций
                                              class PhoneStateIdle implements IPhoneState {
                                                  pickUp():IPhoneState {
                                                      return new PhoneStatePickedUp();
                                                  }
                                                  hangUp():IPhoneState {
                                                      throw new Exception("already idle");
                                                  }
                                                  dial():IPhoneState {
                                                      throw new Exception("unable to dial in idle state");
                                                  }
                                              }
                                              
                                              class PhoneStatePickedUp implements IPhoneState {
                                                  pickUp():IPhoneState {
                                                      throw new Exception("already picked up");
                                                  }
                                                  hangUp():IPhoneState {
                                                      return new PhoneStateIdle();
                                                  }
                                                  dial():IPhoneState {
                                                      return new PhoneStateCalling();
                                                  }
                                              }
                                              
                                              class PhoneStateCalling implements IPhoneState {
                                                  pickUp():IPhoneState {
                                                      throw new Exception("already picked up");
                                                  }
                                                  hangUp():IPhoneState {
                                                      return new PhoneStateIdle();
                                                  }
                                                  dial():IPhoneState {
                                                      throw new Exception("already dialing");
                                                  }
                                              }
                                              
                                              // автомат
                                              class Phone {
                                                  private IPhoneState state;
                                              
                                                  constructor() {
                                                      this.state = new PhoneStateIdle();
                                                  }
                                                  pickUp():void {
                                                      this.state = this.state.pickUp();
                                                  }
                                                  hangUp():void {
                                                      this.state = this.state.hangUp();
                                                  }
                                                  dial():void {
                                                      this.state = this.state.dial();
                                                  }
                                              }
                                              


                                              Лично меня особенно восхищает этот шаблон за его умение организовать ветвящуюся логику без if'ов.
                                            • 0
                                              Для .NET разработчиков рекомендую отличную книгу Design Patterns via C#. Научит правильно подбирать и использовать шаблоны проектирования, описанные в классическом труде «Приемы объектно-ориентированного проектирования, авторами которого являются Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес переписанную для платформы .Net.
                                              • +2

                                                Что, опять? Банды четырёх мало что ли?

                                                • +4

                                                  Вобщем-то да. Это далеко не лучшее чтиво на данную тему на сегодняшний день.

                                                  • +5

                                                    А почему бы и нет?
                                                    Посмотрите на статистику этой статьи.
                                                    Пипл до сих пор хавает пересказ книги начала нулевых с ухудшениями от Рабиновичей, а мыло готово кормить этим досыта.
                                                    И пофиг, что синглтон — антипаттерн, что создающие паттерны давным-давно реализованы в DI-контейнерах, что State — это тихий ужас, что компоновщик в статье перевран и т.д. и т.п.

                                                    • 0

                                                      Интересно, а почему это синглтон внезапно антипаттерн? Вы ведь наверняка не про обращение в глобальном контексте (это его использование, которые вполне возможно через DI), а уверенность автора класса в том, что больше одного инстанса пользователь не создаст?

                                                      • +1

                                                        Синглтон — антипаттерн по построению.
                                                        Он имеет две ответственности: делает что-то полезное и контролирует количество собственных экземпляров.
                                                        Это нарушение SRP с особым цинизмом.
                                                        На практике это выливается в многослойный геморрой:


                                                        1. Приходится наследоваться от специального класса или копипастить
                                                        2. Тестирование синглтона затруднено
                                                        3. Тестирование синглтона с зависимостями от других синглтонов еще сложнее
                                                        4. Когда становится нужно иметь два экземпляра класса-синглтона — у программиста большие проблемы

                                                        Самое забавное, что правильное решение тривиально — достаточно, чтобы количество экземпляров контролировал отдельный объект. Причем его даже не надо писать самому — полно готовых реализаций, от Lazy до DI-контейнеров.
                                                        А синглтону место в параде антипаттернов, как безусловно вредному, но крайне распространенному и живучему решению.

                                                        • –2
                                                          Когда становится нужно иметь два экземпляра класса-синглтона — у программиста большие проблемы

                                                          зачем иметь два экземпляра класса СИНГЛтона? это не проблема паттерна, это проблема того, кто хочет «два экземпляра класса-синглтона». Это тоже самое, если бы Вы купили шоколадное мороженое, а потом сказали бы «ну блин, я же хочу ванильное. с этим мороженым что-то не так»
                                                          • +1

                                                            Это проблема антипаттерна "синглтон".
                                                            Требования имеют свойство меняться и более простой класс, не занимающийся контролем числа собственных экземпляров, оказывается заведомо более удобным и гибким, чем синглтон.

                                                            • 0
                                                              Никто не спорит, что требования меняются. Повторюсь, в Вашем примере проблема не в паттерне, а в том, что Вы его применили не там, где надо. Требования поменялись и он Вам больше не подходит. Но это Ваша проблема, так как Вы не подумали наперед и применили неподходящий паттерн. Если Вам завтра скажут «перепишите все с С++ на Rust», Вы будете говорить, что С++ плохой и вообще антиязык?

                                                              Я не спорю, что синглтон атипаттерн и что его функционал (слежение за количеством экземпляров) реализован в DI-контейнерах, но Вы приводите смешные примеры.
                                                              • 0

                                                                Я не применяю антипаттерн "синглтон" — это исключительно ваши собственные догадки.
                                                                В том числе потому, что не хочу заранее жестко ограничивать возможность иметь более одного экземпляра класса.

                                                            • 0

                                                              Это проблема нарушения SRP паттерном. Никому (наверное) не нужно два синглтона, но бывает становится нужно два и более соединения к СУБД или иному серверу, два и более экземпляра конфигов, два и более экземпляра профиля пользователя и т. п., но по коду уже везде разбросаны вызовы SomeSingleton::getInstance() даже не из требования строго ограничить количество экземпляров одним, а просто потому что так удобно и экономятся ресурсы.

                                                              • 0
                                                                SomeSingleton::getInstance() даже не из требования строго ограничить количество экземпляров одним, а просто потому что так удобно и экономятся ресурсы.


                                                                Ну адекватная IDE должна помочь с рефакторингом. Не такая уж это и проблема. Проблема в том где вызывается это гет инстанс. Eсли гдето в дебрях бизнес логики потому что так быстро и удобно, то это банальный говнокод и неявные зависимости. Я не защищаю синглтон, но в конце концов, это просто паттерн. Его можно использовать там, где это к месту.
                                                                • 0

                                                                  Так внедрение неявных зависимостей с тяжелой инициализацией и есть основной способ применения синглтона де-факто. Просто замена многочисленных new Class() на Class::getInstance. Если бы его применяли где-то в самом начале стэка вызовов, а потом передавали параметрами, то может он бы антипаттерном и не считался, вот только смысла бы в нём было мало, а так смысл в том, что он доступен глобально.

                                                                  • 0
                                                                    Смысл в нем то, что будет 1) гарантировано будет создан только один объект. 2) создан он будет лениво в момент первого обращения. То что он доступен глобально лишь побочный ефект. В абстрактном языке без статических методов тоже можно реализовать синглтон, но он не будет доступен глобально.
                                                                    • 0

                                                                      Глобальная доступность — свойство синглтона по определению.

                                                                      • 0
                                                                        Да ну? И где это написано? Он вносит глобальное состояние в приложение, это да. Но требование глобальной доступности везде — это я впервые слышу. Основное требование — это создание одного инстанса. Именно поэтому он синглтон, а не глобалтон какойто
                                                                        • +2

                                                                          Ну открываем определение сингелтона и читаем требования к реализации:


                                                                          An implementation of the singleton pattern must:
                                                                          • ensure that only one instance of the singleton class ever exists; and
                                                                          • provide global access to that instance.

                                                                          последний пункт как раз про это


                                                                          Именно поэтому он синглтон, а не глобалтон какойто

                                                                          глобальный доступ это не цель, это деталь реализации.

                                                            • 0

                                                              Класс может таким образом защищаться от неправильного использования. С тем же успехом можно и конструктор называть "второй ответственностью", что же, у класс и себя конфигурирует, и http-запросы посылает? Вот негодяй! Синглтон это такой подвид конструктора, который не принимает параметров. Мокать его как правило тоже не нужно, потому что мокать нужно интерфейс, который этот синглтон реализует. А параметров у синглотнов быть не должно, иначе это запах и использование паттерна не по-назначению.


                                                              Если вдруг нужно 2 инстанса — не вопрос, убираем синглтон и делаем какую-то обертку, но ведь рефакторинг под изменившиеся требования это ок. Или вы всегда предусматриваете ВСЕ возможные изменения в будущем во ВСЕХ местах? Звучит как оверинжинеринг.

                                                              • 0
                                                                и себя конфигурирует, и http-запросы посылает? Вот негодяй!

                                                                поэтому придумали фабрики.


                                                                Хотя в целом я с вами согласен.

                                                                • 0
                                                                  Класс может таким образом защищаться от неправильного использования.

                                                                  Может. Но реальному использованию класса такой способ защиты скорее вреден, чем бесполезен.


                                                                  С тем же успехом можно и конструктор называть "второй ответственностью", что же, у класс и себя конфигурирует, и http-запросы посылает?

                                                                  Конструктор ничего не конфигурирует — он отвечает за создание полноценного рабочего экземпляра объекта. Все что нужно для этого должно передаваться в параметрах.


                                                                  Синглтон это такой подвид конструктора, который не принимает параметров.

                                                                  Это прямо противоречит определению от "банды четырех".


                                                                  Если вдруг нужно 2 инстанса — не вопрос, убираем синглтон и делаем какую-то обертку, но ведь рефакторинг под изменившиеся требования это ок.

                                                                  Рефакторинг, который появляется исключительно от использования синглтона — это совсем другое слово: "Антипаттерн".


                                                                  Звучит как оверинжинеринг.

                                                                  С точностью до наоборот. Перепроектирование — это добавление к классу вредного кода, контролирующего число его экземпляров.
                                                                  Альтернатива — одна строка кода (при использовании Lazy) или один дополнительный вызов (при использовании DI-контейнера) или вообще бесплатно (если регистрация в контейнере ограничивает число экземпляров по умолчанию).

                                                                  • 0

                                                                    Представьте что вам нужно написать библиотеку для работы с COM портом. Доступ к нему должен быть строго у одного инстанса на процесс. Предложите способ организовать это так, что бы клиентскому коду небыло проблем? Я вижу один — сингелтон. Просто и железобетонно. А недостатки вроде "глобального доступа" можно скрыть с клиентской стороны добавив в тот же контейнер. Но зато мы с нимем с разработчика, который будет использовать нашу библиотеку риски того, что он "случайно" может сделать что-то не то.


                                                                    Но таких кейсов 1 на тысячу. В большинстве же случаев сингелтон лепят просто так.

                                                                    • 0

                                                                      Ровно один ком-порт на машину? Ну-ну.
                                                                      Добавив контейнер, синглтон можно выкинуть сразу.

                                                                      • 0

                                                                        Это для примера. То есть вы обязанность вашей библиотеки будете перекидывать на клиента (или заставлять его использовать ажно целый контейнер, причем именно тот который вам захотелось).

                                                                        • 0

                                                                          Нет. Я полагаю, что библиотека не должна принуждать клиента к определенной композиции.
                                                                          Что характерно, даже умозрительного примера пользы синглтона с ходу привести не удалось.

                                                                          • 0
                                                                            библиотека не должна принуждать клиента к определенной композиции.

                                                                            В тоже время вы именно этот вариант и предлагаете.


                                                                            Я не пытаюсь доказать что "сингелтоны полезны", для меня этот паттерн это как "множественное наследование" например. То есть за время коммерческой разработки может быть один раз и оправдано.

                                                                    • 0
                                                                      Может. Но реальному использованию класса такой способ защиты скорее вреден, чем бесполезен.

                                                                      А у меня с точностью до наоборот мнение.


                                                                      Рефакторинг, который появляется исключительно от использования синглтона — это совсем другое слово: "Антипаттерн".

                                                                      Рефакторинг, который появляется от изменения требований.


                                                                      С точностью до наоборот. Перепроектирование — это добавление к классу вредного кода, контролирующего число его экземпляров.
                                                                      Альтернатива — одна строка кода (при использовании Lazy) или один дополнительный вызов (при использовании DI-контейнера) или вообще бесплатно (если регистрация в контейнере ограничивает число экземпляров по умолчанию).

                                                                      Надо полагать, что если DI в проекте, нет, то авторы — лохи, которым нужно ткнуть в нос SOLID и пинать, пока они не поймут, что без IoC жизни нет.

                                                                      • 0
                                                                        А у меня с точностью до наоборот мнение.

                                                                        Отсюда и понятие "антипаттерн": несмотря на объективный вред, у многих другое субъективное мнение.


                                                                        Рефакторинг, который появляется от изменения требований.

                                                                        Без синглтона этот рефакторинг не нужен и после изменения требований.


                                                                        Надо полагать, что если DI в проекте, нет, то авторы — лохи, которым нужно ткнуть в нос SOLID и пинать, пока они не поймут, что без IoC жизни нет.

                                                                        Проект от среднего размера без DI (не контейнера, а паттерна) — либо легаси, либо авторы таки да.

                                                                        • 0
                                                                          либо легаси, либо авторы таки да.

                                                                          ушел жалеть бедалаг которые страдают с python/javascript/ruby… Или вы все же имели ввиду IoC и не паттерн на принцип. Ну там всякие don't call us, we call you.

                                                                          • 0

                                                                            DI как паттерн это всего лишь замена композиции агрегацией. В питоне, яваскрипте и руби все это есть.

                                                          • 0
                                                            https://github.com/domnikl/DesignPatternsPHP/
                                                            • –4

                                                              Теги: "Никто не читает теги" — порадовало
                                                              А статья — весьма монументальна, приятно видеть

                                                              • 0
                                                                кому они нужны после прочтения статьи? (а до прочтения их хрен найдёшь)
                                                                • 0
                                                                  Вот, кстати, вопрос: зачем вообще читать теги кому-то, кроме автора? Они же не для почитать, а для поиска и упорядочивания по ключевым словам. Совершенно логично, что «никто не читает теги». Зато по этому тегу можно найти сразу весь список «оригинально» пошутивших авторов ;)
                                                                • 0
                                                                  • 0

                                                                    В PHP с версии 5.4 имеются первоклассные функции, почему они не применяются здесь?

                                                                    • 0
                                                                      Спасибо за статью. Проще, чем описаны паттерны тут refactoring.guru еще не видел. К тому же, примеры на псевдокоде + диаграммы, на русском.
                                                                      • +2
                                                                        Чем объясняется повсеместное применение реализаций интерфейсов вместо наследования? Взять тот же пример шаблона «мост»:
                                                                        interface WebPage
                                                                        {
                                                                            public function __construct(Theme $theme);
                                                                            public function getContent();
                                                                        }
                                                                        

                                                                        Конструктор как элемент интерфейса, серьёзно? И почему бы не использовать наследование от абстрактного класса, например:

                                                                        abstract class AbstractWebPage
                                                                        {
                                                                            protected $theme;
                                                                        
                                                                            public function __construct(Theme $theme)
                                                                            {
                                                                                $this->theme = $theme;
                                                                            }
                                                                        
                                                                            abstract public function getContent();
                                                                        }
                                                                        
                                                                        
                                                                        class About extends AbstractWebPage
                                                                        {
                                                                            public function getContent()
                                                                            {
                                                                                return "About page in " . $this->theme->getColor();
                                                                            }
                                                                        }
                                                                        
                                                                        class Careers extends AbstractWebPage
                                                                        {
                                                                            public function getContent()
                                                                            {
                                                                                return "Careers page in " . $this->theme->getColor();
                                                                            }
                                                                        }
                                                                        
                                                                        • +1
                                                                          Причем в данном случае как раз About и Careers «is-a» а не «has-a» WebPage. Потому extends вполне подходит
                                                                          • 0
                                                                            Чем объясняется повсеместное применение реализаций интерфейсов вместо наследования?

                                                                            Видимо, следованием принципов SOLID. Но конкретный пример действительно ужасен: если уж следовать паттернам и делать по-человечески, то интерфейс должен называться IWebContentProvider, а не WebPage, а конструирование вынесено в отдельную фабрику.


                                                                            Но опять же, плодить сущности только ради того, чтобы заюзать какой-нибудь паттерн, плохо. Здесь действительно можно обойтись абстрактным классом.

                                                                            • +1
                                                                              Видимо, следованием принципов SOLID

                                                                              ни один из принципов SOLID никак не влияет на выбор между implements и extends. Они немного подсказывают что интерфейсы чаще лучше для организации иерархии типов, но конкретно в этом случае наличие конструктора в интерфейсе явный признак того что что-то пошло не так.


                                                                              Вообще на тему implemets vs extends есть неплохая статья: Why extends is evil


                                                                              то интерфейс должен называться IWebContentProvider, а не WebPage

                                                                              скорее просто ContentProvider. Нам же в целом только это важно.

                                                                            • +2
                                                                              Чем объясняется повсеместное применение реализаций интерфейсов вместо наследования?

                                                                              1. Строка public function __construct(Theme $theme); в интерфейсе, еще не повод ставить под сомнение повсеместное применение интерфейсов. Это все равно, если запретить спички, только потому, что один ребенок обжег ими палец. Или поставить под вопрос использование автомобилей, ведь за их рулем часто оказываются нетрезвые люди. Просто, нужно правильно использовать возможности языка, а не писать определение конструктора в интерфейсе, только лишь потому, что язык этого не запрещает.

                                                                              2. Почему же их все-таки нужно использовать и желательно почаще?
                                                                              Не будем далеко ходить и возьмем ваш пример. Мы имеем один абстрактный класс и два класса реализующие его. Допустим, у нас есть некий клиентский класс, который обращается к getContent().
                                                                              class Client 
                                                                              {
                                                                                  private $page;
                                                                              
                                                                                  /**
                                                                                   * Client constructor.
                                                                                   *
                                                                                   * @param $page
                                                                                   */
                                                                                  public function __construct(AbstractWebPage $page)
                                                                                  {
                                                                                      $this->page = $page;
                                                                                  }
                                                                              
                                                                                  public function showPage()
                                                                                  {
                                                                                      return $this->page->getContent();
                                                                                  }
                                                                              }
                                                                              

                                                                              Таким образом мы научили наш клиент работать только с AbstractWebPage. Что, если мы хотим добиться от клиента работы не только с AbstractWebPage типом но и другими похожими типами? Стоит отметить, клиенту важно только одно, это метод getContent(), все что он должен знать о типе AbstractWebPage.
                                                                              Тут вспоминается пример из жизни, когда обезьянку научили приносить воду в ведре. Та ходила по искусственной дорожке, где в конце был небольшой резервуар с водой. Позже, к этим условиям добавили бассейн с водой, однако обезьянка продолжила ходить за водой по привычному маршруту, тогда как бассейн с водой был намного ближе.
                                                                              Также и тут, в нашем примере, мы словно учим обезьяну. Мы говорим ей вместо «воды» «принести», «воооон там, есть дорога, ты должна пройти по ней и только по ней, а в конце дороги будет резервуар и из него набрать воды» «принести»

                                                                              Возвращаясь к нашему примеру. С помощью интерфейса мы можем определить более абстрактно тип страницы PageInterface. И любой класс реализующий данный тип будет обязан описать логику для метода getContent()

                                                                              3. Почему не использовать в нашем примере абстрактный класс вместо интерфейса? — это неправильный вопрос. Почему мы можем использовать их вместе? — а вот это уже правильный.
                                                                              Если мы будем использовать и то и другое, то все сразу встанет на свои места. Интерфейс даст возможность определить корневой тип, суперабстракцию, а абстрактный класс сможет ее уточнить/конкретизировать до необходимой степени. Таким образом мы правильно распределим ответственность за степень абстракции между классами и сделаем более понятной их иерархию.

                                                                              В итоге у нас может быть такое решение, когда типов страниц может быть больше одного, а клиент не хочет брать на себя ответственность за их обработку. Он знает только о getContent и что на входе у него уже есть какая то реализация типа PageInterface.
                                                                              abstract class AbstractWebPage implements PageInterface
                                                                              {
                                                                                  protected $theme;
                                                                              
                                                                                  public function __construct(WebTheme $theme)
                                                                                  {
                                                                                      $this->theme = $theme;
                                                                                  }
                                                                              }
                                                                              
                                                                              
                                                                              class About extends AbstractWebPage
                                                                              {
                                                                                  public function getContent()
                                                                                  {
                                                                                      return "About page in ".$this->theme->getStyle();
                                                                                  }
                                                                              }
                                                                              
                                                                              abstract class AbstractPdfPage implements PageInterface
                                                                              {
                                                                                  protected $theme;
                                                                              
                                                                                  public function __construct(PdfTheme $theme)
                                                                                  {
                                                                                      $this->theme = $theme;
                                                                                  }
                                                                              }
                                                                              
                                                                              class Cv extends AbstractPdfPage
                                                                              {
                                                                                  public function getContent()
                                                                                  {
                                                                                      return "Cv page in ".$this->theme->getStyle();
                                                                                  }
                                                                              }
                                                                              
                                                                              class Client 
                                                                              {
                                                                                  private $page;
                                                                              
                                                                                  /**
                                                                                   * Client constructor.
                                                                                   *
                                                                                   * @param $page
                                                                                   */
                                                                                  public function __construct(PageInterface $page)
                                                                                  {
                                                                                      $this->page = $page;
                                                                                  }
                                                                              
                                                                                  /**
                                                                                   * @return PageInterface
                                                                                   */
                                                                                  public function showPage(): PageInterface
                                                                                  {
                                                                                      return $this->page->getContent();
                                                                                  }
                                                                              }
                                                                              

                                                                              • 0
                                                                                Полностью согласен с вами. Более того, изначально хотел дописать, что при необходимости дополнительной абстракции, можно вынести метод getContent() в интерфейс и указать его реализацию в абстрактном классе (как у вас в примере).
                                                                                Сложилось впечатление, что в понимании автора (оригинальной статьи) интерфейс — это представление некой абстрактной сущности, а не её поведения (даже судя просто по названиям интерфейсов).
                                                                            • 0

                                                                              Забыли реализовать интерфейс Observer в примере с наблюдателем

                                                                              • 0
                                                                                У меня в закладках есть папочка «on hand», эта статья должна лежать именно там. Спасибо!
                                                                                • 0
                                                                                  Вот ещё, прикольно оформленная книга (англ.): https://sourcemaking.com/design_patterns
                                                                                  Правда, мне примеры не всегда нравятся.

                                                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                                  Самое читаемое