Тема шаблонов проектирования достаточно популярна. По ней снято много роликов и написано статей. Объединяет все эти материалы «анти-паттерн» Ненужная сложность (Accidental complexity). В результате примеры заумные, описание запутанное, как применять не понятно. Да и главная задача шаблонов проектирования – упрощение (кода, и работы в целом) не достигается. Ведь применение шаблона требует дополнительных усилий. Примерно так же, как и Unit тестирование.
Попробую изложить шаблоны проектирования с точки зрения, как их применять, где и почему.
К порождающим шаблонам можно отнести шесть:
- Прототип (Prototype),
- Абстрактная фабрика (Abstract Factory),
- Фабричный метод (Factory Method),
- Строитель (Builder),
- Одиночка (Singleton),
- Ленивая инициализация (Lazy initialization).
Все остальные шаблоны, которые относят к порождающим это частный случай применения и нет смысла на них останавливаться.
Порождающие шаблоны можно разделить на три группы, по вопросу на который они отвечают. Итак, три вопроса:
- Где?
- Как?
- Когда?
Где?
На этот вопрос отвечают три шаблона: «Прототип», «Абстрактная фабрика» и «Фабричный метод».
В рамках концепции ООП есть только три места где теоретически можно породить новый экземпляр (instance):
- Продукт – класс, экземпляр которого создается.
- Клиент – класс, который будет использовать созданный экземпляр.
- Партнер – любой третий класс в области видимости Клиента.
Собственно, эти шаблоны и определяют место порождения. Кроме этого они связаны иерархически и имеют различную область видимости. Связь показана на рисунке, стрелки определяют направление вызовов.
![Иерархия порождающих шаблонов](https://github.com/mtemp123/Design-Patterns/raw/master/img/Creational_patterns1.png)
В своей реализации «Фабричный метод» может делегировать порождение экземпляра либо имеющейся «Фабрике», либо «Прототипу». «Прототип» же, не должен ни от кого зависеть и делать все сам. Теперь подробнее.
«Прототип»
Данный шаблон соответствует месту «Продукт», по сути является конструктором класса. Соответственно, всегда порождается экземпляр конкретного (заранее известного) класса.
В рамках данного шаблона конструктор знает только непосредственно переданные ему параметры (количество параметров стремится к числу полей класса). Разумеется, есть полный доступ ко всем полям и свойствам создаваемого класса.
Правильно реализованные методы «Прототипа» позволяют избавиться от дополнительных методов инициализации в public. В свою очередь внешний интерфейс класса становится проще и меньше соблазна применять класс не по назначению.
Что нам дает этот шаблон:
- Низкая связанность – класс знает только себя, не зависит от внешних данных;
- Расширяемость – конструкторы могут быть переопределены или добавлены в потомках;
Из минусов:
- Для сложных классов может потребоваться передавать много параметров для инициализации. Хотя есть тривиальное решение.
- Прямое использование в Клиенте может ухудшить читаемость и практически закрывает возможность переопределения типа порождаемого экземпляра в потомке Клиента.
Самый популярный шаблон. Все его используют, но мало кто знает, что использует. Хорош до получения первой рабочей модели, пока не до конца определены классы и их отношения. После чего обязательно переработка и повышение абстракции.
«Абстрактная фабрика»
Некий класс Партнер. Может быть специализированным, либо «совмещать». Может быть статическим (без экземпляра). Примером «совместительства» может быть класс настроек (configuration). Также может быть скрыта за «Фасадом» (Facade).
«Фабрика» как правило видит все глобальные настройки приложения (либо отдельной подсистемы). Непосредственное порождение может быть делегировано «Прототипу». При этом количество входящих параметров в методе Фабрики будет меньше, чем в аналогичном конструкторе Прототипа. Фабрика не принимает решение «кого создавать» на основании входящих параметров.
Данный шаблон весьма удобен и прост в реализации, но требует предварительного проектирования. Если создавать фабрики для всего, это приведет к усложнению кода. По сути мы получим аналог Прототипа, но вынесенный в сторонний класс.
Из плюсов:
- Хорошая переопределение в потомках
- Упрощенный вызов
- На базе Фабрики легко реализовать подмену (шаблон Состояние (State))
Но есть и минусы:
- Требует проектирования, особенно для универсальных Фабрик (которые используются во многих проектах). Другими словами, с ходу получить хорошую фабрику не просто.
- Очень легко загадить код, тут есть два основных направления:
- Сползание в сторону Прототипа, но в стороннем классе. Методы перегружены параметрами, самих методов много. В результате наследование затруднено, как в самой Фабрике, так и в Клиенте.
- Фабрика с универсальным методом. Этот метод возвращает любой экземпляр в зависимости от переданных параметров. Результат, как и в первом случае.
Очень популярен. Этот шаблон используют те, кто прослушал курс GoF. Как правило код становится еще хуже, чем «до применения шаблонов».
Разумно, когда Фабрики появляются во время первой переработки кода. На этом этапе уже известны комбинации параметров для создаваемых экземпляров, и не составит труда написать обобщенные методы Фабрики. В результате вызовы в Клиенте упростятся.
В ряде случаев удобно прятать Фабрики за Фасадом. Например, в приложении есть десяток своих фабрик, и десяток из библиотек. Для них можно построить Фасад. Это позволит не линковать библиотеки к каждому модулю, а также легко подменять одну фабрику на другую.
«Фабричный метод»
Вершина абстракции в порождающих шаблонах. Место порождения Клиент. Класс, в котором каждое порождение вынесено в «Фабричный метод» имеет все шансы на долгую жизнь. Если же без фанатизма, то предполагаемая ось развития обязательно должна основываться на данном шаблоне.
Фабричный метод не видит дальше своего класса. Количество непосредственно передаваемых параметров должно быть минимальным (в пределе ноль). Сам метод необходимо строит с учетом возможности перекрытия в потомке.
Частой ошибкой является сложная инициализация в одном методе. Например, при порождении сложного экземпляра (шаблон Строитель (Builder)) в один метод помещают создание всех частей будущего объекта. В результате такой метод сложно перекрывать в потомке.
Из плюсов:
- Легко будет соответствовать шаблону «Шаблонный метод» (Template method)
- Получаем лаконичный код, в котором явно видна логика (ее не нужно высматривать среди нагромождения методов и параметров)
Минусов по сути нет.
Этот шаблон практически не используется. Как правило, его можно увидеть только в проектах с глубокой предварительной проработкой. Идеальный вариант, когда Фабричный метод делегирует порождение «Фабрике» или «Прототипу».
Маленький пример
У нас есть класс для протоколирования в файл на жестком диске. Вот так могут выглядеть порождающие методы в рамках шаблонов «Где?»:
Прототип:
constructor Create(aFilename: string; aLogLevel: TLogLevel);
Все что должен знать конструктор передается ему в виде параметров.
Фабрика:
function GetLogger(aLogLevel: TLogLevel): ILogger;
Фабрика знает в какой файл необходимо писать, так как это указано в настройках приложения.
Фабричный метод:
function NewLogger: ILogger;
В классе Клиенте, известно с какой деталировкой производить протоколирование.
В данной конструкции для подмены класса протоколирования на заглушку достаточно переопределить NewLogger в потомке Клиента. Это полезно при проведении Unit тестов.
Что бы производить протоколирование в базу данных, достаточно переопределить метод GetLogger в потомке Фабрики.