Паттерны проектирования

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

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

Книга называется Паттерны проектирования, автор Эрик Фримен, примеры там на JAVA, но мне они показались очень понятными, хоть я и веб-разработчик на PHP, но имел дело с Pascal, Delphi, C, LUA, Python и иногда внести правку в код программы на незнакомом языке достаточно просто, т.к. основы то везде одинаковые - условия, переменные, циклы, функции, ввод и вывод...

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


Наблюдатель

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

Главный класс, который управляет ситуацией, например курсы валют через него обновляются. И есть классы наблюдающие, например, у нас входные цены в баксах, а надо в рублях цену обновлять каждый день. И есть несколько таких классов, один в товарах обновляет, другой в XML выгружает. Классы наблюдатели сообщают "главному" классу что когда валюта поменяется - дерни меня, передавая ему например ссылку на свой метод.

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


Декоратор

Это какой-то замудренный паттерн, но я видел его применение во многих кодах на PHP, например в том же PHPExcel. И я тоже такое делаю иногда, просто не знал как это назвать.

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

<?php

//кофе
class Coffee
{
	public $parent_method;

	function __construct(&$m=false)
	{
		$this->method = $m;
	}

	//у кофе есть цена
	public function calc()
	{
		return 100;
	}

	public function descr()
	{
		return "Кофеек";	
	}
}

//сахар
class Sugar
{
	public $parent_method;

	function __construct(&$m=false)
	{
		$this->parent_method = $m;
	}

	//цена сахара + цена класса выше
	public function calc()
	{
		return 10+$this->parent_method->calc();
	}

	public function descr()
	{
		return $this->parent_method->descr().", Сахарок";	
	}
}

//молоко
class Milk
{
	public $parent_method;

	function __construct(&$m=false)
	{
		$this->parent_method = $m;
	}

	//цена молока + цена класса выше

	public function calc()
	{
		return 20+$this->parent_method->calc();
		
	}


	public function descr()
	{
		return $this->parent_method->descr().", Молоко";	
	}
}


$napitok = new Coffee(); //создаем напиток кофе
$napitok = new Sugar($napitok); //добавляем в него сахар
$napitok = new Milk($napitok); //добавляем молоко

echo $napitok->descr().PHP_EOL; //выводим название получившейся смеси
echo "Цена напитка: ".$napitok->calc().PHP_EOL; //выводим цену

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

~ php decorator.php 
Кофеек, Сахарок, Молоко
Цена напитка: 130

Прикол в том, что можно исключить строчку сахар

//$napitok = new Sugar($napitok); //добавляем в него сахар

И получим вот такой результат

~ php decorator.php 
Кофеек, Молоко
Цена напитка: 120

Этот пример достаточно абстрактный приведен, чтобы показать как можно перезадуйствовать в одном классе другой. Можно привести пример что вместо первого класса у нас будет класс для работы с базой, а второй для работы с данными и в такой цепочке можно первый класс не mysql, а mongodb подключить и следующий класс для работы с данными просто будет стандартные методы выполнять и не знать что у нас там за база, ну это тоже абстрактно очень, главное, чтобы вам было понятен такой фокус.

Если бы мне предложили считать кофе в таком элементарном коде и сделать это методом декоратор, то я бы сделал вообще вот так!
<?php


class Napitok
{
	public $parent_method,$description,$price;

	//конструктор, то что передается при создании класса
	function __construct(&$m=false,$d,$p)
	{
		$this->parent_method = $m;
		$this->description = $d;
		$this->price = $p;
	}

	public function calc()
	{
		if ($this->parent_method===false) 
			return $this->price;
		else
			return $this->price+$this->parent_method->calc();
	}

	public function descr()
	{
		if ($this->parent_method===false) 
			return $this->description;
		else
			return $this->parent_method->descr().', '.$this->description;	
	}
}





$start = false; //переменная с экземпляром по ссылке передается, в первом классе нет родителя
$napitok = new Napitok($start,'Кофе',100);
$napitok = new Napitok($napitok,'Молоко',20);
$napitok = new Napitok($napitok,'Сахар',10);


echo $napitok->descr().PHP_EOL;
echo "Цена напитка: ".$napitok->calc().PHP_EOL;

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


Фабрика и простая фабрика

Многие путают простую фабрику с фабрикой, в этом мы попробуем сегодня разобраться...

Простая фабрика выглядит примерно так

  1. Сперва некая простая фабрика
class SimpleFabrica 
{
	public function create_class($type)
	{
		if ($type===1) return new Class1;
		if ($type===2) return new Class1;
		...
	}


}

2. Потом класс, который ее использует

class Myclass
{
	protected $fabrica;

	function __construct($fabrica)
	{
		$this->fabrica = $fabrica;
	}


	public function prepare($type)
	{
		$newclass = $this->fabrica->create_class($type);
		$newclass->get();
		$newclass->prepare();
		return $newclass->get_result();
	}
}

А потом попробуем всё это задействовать

$fabrica = new SimpleFabrica;
$creator = new MyClass($fabrica);
$result = $creator->prepare(1);

В общем суть этого подхода в том, что есть отдельный класс - фабрика (простая), который в зависимости от типа объекта (переменная $type) создает подходящий класс, а дальше выполняет с ним определенную последовательность. Допустим, нам надо получить страницу или файл, мы передаем типа ftp или http или webdav, у нас создается нужный класс для работы с этим делом и над ним проводятся манипуляции - получить, обработать, вернуть результат...

Но это всё "простая фабрика", это не является полноценной фабрикой, это лишь подход к созданию объектов, он очень часто используется и является помощником паттернов...


Паттерн фабрика

Паттерну фабрика отведено 60 страниц в книге "Паттерны проектирования" серии Head First, чтобы вникнуть в это более глубоко, советую всё же прочитать книгу, это одно из фундаментальных знаний...

Насколько я понял, паттерн фабрика позволяет метод создания экземпляра класса вытащить в подклассы, для начала сделаем вот так

abstract class Myclass
{

	public function prepare($type)
	{
		$newclass = $this->create_class($type);
		$newclass->get();
		$newclass->prepare();
		return $newclass->get_result();
	}

	protected function create_class($type)
	{
		//это будет в субклассах
	}
}

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

class Subclass extends Myclass
{
      protected function create_class($type)
      {
        if ($type===1) return new Class1;
        if ($type===2) return new Class1;
        ...
      }
}

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


Паттерн одиночка

Этот паттерн создает экземпляр в единственном числе, если экземпляр объекта уже создан, то возвращается он, а не его копия, реализация данного механизма усложняется в многопоточном программировании, когда один поток создает, а второй обратился или оба обратились в один момент и для двоих создались разные экземпляры, в многопоточности для каждого языка программирования есть свои методы синхронизации событий. И поэтому, простая проверка на создание экземпляра превращается в некий отдельный механизм, который гарантирует 100% предотвращение дублирования.

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


Патерн команда

В простом понимании - основной класс имеет метод execute(), запускающий определенное действие, а дочерние классы этот метод переопределяют, выполняя свои дела.

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

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


Паттерн Адаптер и декоратор

Описывать подробную реализацию данных паттернов не буду, всё будет понятно и так... Представьте такой момент, когда вы провели массовый рефакторинг, выпилили кучу методов, добавили новые, переименовали другие методы более логично, ваш функционал оказался несовместимым с большей частью кода. Или, например, у вас была библиотека PHPExcel для чтения и генерации Excel-файлов, а потом разработчики решили закопать библиотеку и создали новую PhpSpreadsheet, но у вас так много импортеров из прайс-листов Excel и всяких генераторов Excel файлов с отчетами, накладными, товарными чеками и других документов, предположим, что новая библиотека имеет совсем другие методы для чтения и записи, для работы с оформлением ячеек, конечно, можем пройтись поиском по файлам, найти сотни или тысячи вызовов методов и переименовать их, более того, если в новых методах вместо передачи нескольких переменных нужно уже передавать одним массивом или порядок параметров функций поменялся, то это создает очень большую нагрузку на голову. А представьте что вам нужно в вашей системе поддерживать обратную совместимость со старыми модулями сторонних разработчиков. Тут и идет на помощь паттерн адаптер или фасад, создаем некое подобие старой библиотеки с методами и классами, но в этих методах ничего не делаем, просто вызываем методы новой библиотеки.

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

Паттерн Фасад

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

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

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

Если у вас есть что добавить по паттерну фабрика - пишите в комментариях.

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

Показать комментарии