[Из песочницы] Порождающие паттерны. Ответственности и примеры
Если немного погуглить, то про паттерны можно найти довольно много информации. Например, на хабретут, тут, и тут, в большой советской энциклопедии вики и книжках GOF, Фаулер и Кериевски (последняя, кстати, наименее известная, но на мой взгляд самая интересная), … Зачем нужна еще одна статья?
В большинстве источников рассказывается какие есть паттерны и как их реализовывать, но после прочтения остается неясным когда и как их применять, что в итоге приводит к аду синглтонов неправильному их использованию.
Зачем нужны паттерны?
Согласно одному из определений «это лишь пример решения задачи, который можно использовать в различных ситуациях.» Неясно только какое решение каких задач и в каких ситуациях его использовать делаем все синглтонами!.
На мой взгляд, главная задача шаблонов проектирования — это определение и выделение ответственности. Для порождающих паттернов — это создание объектов. Таким образом, независимо от того, какая поставлена задача, если необходимо при ее решении создавать объекты, то можно применять порождающие паттерны.
Фабричный метод, фабрики, абстрактные фабрики
Самое четкое и понятное определение, которое я нашел, описано в книге Кириевски:
- Creation method (порождающий метод) — статический или нестатический метод, который создает и возвращает объекты.
- Factory method — это нестатический метод, возвращающий тип базового класса или интерфейса и реализованный в иерархии классов для обеспечения возможности полиморфного создания.
- Factory (Фабрика) — это класс, реализующий один или несколько методов создания.
- Abstract Factory (абстрактная фабрика) — это интерфейс для создания семейств связанных или зависимых объектов без указания их конкретных классов.
Все. Никаких диаграм и запутанных примеров.
- Если нам нужно создавать объект, то используем порождающий метод.
- Если нам нужно создавать другие объекты (реализующие тот же интерфейс или унаследованные от базового класса) или те же объекты, но другим способом в дочерних классах, то делаем метод виртуальным и переопределяем в дочерних классах (реализуем фабричный метод).
- Если нам нужно использовать порождающий метод из нескольких мест, то выделяем его в отдельный класс (фабрику).
- Если нам нужна возможность создавать объекты разными способами, то выделяем из фабрики интерфейс (абстрактная фабрика) и используем его.
Получилось немного утрированно. Есть более хитрые случаи использования этих паттернов и способы их реализации, но в большинстве случаев, особенно начинающим программистам, можно действовать по этой схеме.
Хочу еще отметить, что я видел как абстрактную фабрику иногда называют фабрикой фабрик, что неправильно. Абстрактная фабрика позволяет использовать интерфейс и не привязываться к конкретной реализации, а не создавать объект создающий объекты.
Порождающий метод
Все-таки на примерах понятнее.
Допустим, наше приложение позволяет генерировать сообщения, причем сообщения могут быть двух типов: текст и картинка с подписью. Для того, чтобы создавать сообщения этих двух типов, мы можем использовать два конструктора.
public class Message
{
public String Text{get; set;}
public Image Picture{get; set;}
public Message(String text)
{
Text = text;
}
public Message(String text, Image image)
{
Text = text;
Image = image;
}
}
У этого подхода есть масса недостатков. Из названия конструкторов непонятно чем они отличаются. Есть ли разница вызывать конструктор без картинки или передать null вместо картинки? Необходима дополнительная документация. Если мы захотим поменять поведение конструктора (например, добавить какой-нибудь префикс в сообщение) нам нужно менять либо все вызовы этого конструктора либо вводить дополнительные параметры, что усложняет понимание кода и повышает вероятность ошибок.
С другой стороны, у нас есть явная ответственность — «создание новых сообщений разных типов». Выделяем ее в отдельные методы.
public class Message
{
public String Text{get; set;}
public Image Picture{get; set;}
public Message(String text, Image image)
{
Text = text;
Image = image;
}
public static Message CreateTextMessage(String text)
{
return new Message(text, null);
}
public static Message CreateImageMessageWithSubtitle(String text, Image image)
{
return new Message(text, image);
}
}
Теперь из названия методов ясно для чего они нужны и какой из них использовать. Если мы захотим создавать сообщения с префиксом, то мы можем добавить новый метод.
Фабрика
Сразу видно, что ответственность создавать объекты никак не относится к основной ответственности класса Message. Если это не создает никаких трудностей, то можно оставить так, в противном случае выделяем эту ответственность в отдельный класс.
public class Message
{
public String Text{get; set;}
public Image Picture{get; set;}
public Message(String text, Image image)
{
Text = text;
Image = image;
}
}
static class MessageFactory
{
public static Message CreateTextMessage(String text)
{
return new Message(text, null);
}
public static Message CreateImageMessageWithSubtitle(String text, Image image)
{
return new Message(text, image);
}
}
Абстрактная Фабрика
У нас осталась жесткая зависимость на конкретную реализацию фабрики. Это вызовет затруднения, если мы захотим создавать сообщения каким-либо другим способом или подменять реализацию моками при тестировании.
Если нам важна сама ответственность создания объектов и мы не зависим от того, как эта ответственность реализована, используем абстрактную фабрику.
interface IMessageFactory
{
Message CreateMessage();
}
class TextMessageFactory: IMessageFactory
{
private String text;
public TextMessageFactory(String text)
{
this.text = text;
}
public Message CreateMessage()
{
return new Message(text, null);
}
}
class ImageWithSubtitleMessageFactory: IMessageFactory
{
private String text;
private Image image;
public TextMessageFactory(String text, Image image)
{
this.text = text;
this.image = image;
}
public static Message CreateMessage()
{
return new Message(text, image);
}
}
Пример с сообщениями не самый подходящий для абстрактной фабрики, но надеюсь, что он дает представление о том как она работает.
P.S. Это моя первая статья и она заняла больше времени, чем я рассчитывал. Поэтому я не включил в нее паттерны Builder и синглтон, которые собирался включить. Если статья окажется интересной, то напишу продолжение.