Тема шаблонов проектирования достаточно популярна. По ней снято много роликов и написано статей. Объединяет все эти материалы «анти-паттерн» Ненужная сложность (Accidental complexity). В результате примеры заумные, описание запутанное, как применять не понятно. Да и главная задача шаблонов проектирования – упрощение (кода, и работы в целом) не достигается. Ведь применение шаблона требует дополнительных усилий. Примерно так же, как и Unit тестирование.


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


К порождающим шаблонам можно отнести шесть:


  • Прототип (Prototype),
  • Абстрактная фабрика (Abstract Factory),
  • Фабричный метод (Factory Method),
  • Строитель (Builder),
  • Одиночка (Singleton),
  • Ленивая инициализация (Lazy initialization).

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


Порождающие шаблоны можно разделить на три группы, по вопросу на который они отвечают. Итак, три вопроса:


  • Где?
  • Как?
  • Когда?

Где?


На этот вопрос отвечают три шаблона: «Прототип», «Абстрактная фабрика» и «Фабричный метод».


Немного о терминах

В рамках концепции ООП есть только три места где теоретически можно породить новый экземпляр (instance):


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


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


Иерархия порождающих шаблонов

В своей реализации «Фабричный метод» может делегировать порождение экземпляра либо имеющейся «Фабрике», либо «Прототипу». «Прототип» же, не должен ни от кого зависеть и делать все сам. Теперь подробнее.


«Прототип»


Данный шаблон соответствует месту «Продукт», по сути является конструктором класса. Соответственно, всегда порождается экземпляр конкретного (заранее известного) класса.
В рамках данного шаблона конструктор знает только непосредственно переданные ему параметры (количество параметров стремится к числу полей класса). Разумеется, есть полный доступ ко всем полям и свойствам создаваемого класса.


Правильно реализованные методы «Прототипа» позволяют избавиться от дополнительных методов инициализации в 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 в потомке Фабрики.

Комментарии (10)


  1. andi123
    18.09.2019 10:35

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


    1. keksmen
      18.09.2019 11:27

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


      1. andi123
        18.09.2019 11:52
        +2

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


        1. VolCh
          18.09.2019 23:47

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


        1. Alado
          19.09.2019 11:40

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


    1. MTemp123 Автор
      18.09.2019 15:22

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


      1. andi123
        18.09.2019 15:29

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

        В общем, есть мнение, что идею авторов шаблонов поняли совершенно наоборот. Но я не настаиваю.


        1. MTemp123 Автор
          18.09.2019 16:31

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

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

          Насчет правильно ли их поняли, не знаю. Думаю, что правильно. Просто прошло уже 25 лет, языки сильно изменились, а шаблоны учат как будто на дворе 94-й.


          1. UnclShura
            18.09.2019 18:42
            +1

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


            1. Spunreal
              19.09.2019 09:33

              del