Ответы пользователя по тегу Паттерны проектирования
  • Для чего использование Hydrator pattern вместо создания объекта?

    @MarkusD
    все время мелю чепуху :)
    Гидрация, или насыщение, как термин используется для обозначения процесса наполнения данными уже доступной пользователю сущности. Когда сущность уже создана и доступна для использования, но данными заполнена не до конца или еще вовсе не заполнена.
    Как правило, гидрация производится полностью в обход интерфейса сущности, предполагая изначальное соответствие неполной сущности инварианту своего типа. Этот процесс может напоминать сериализацию, но отличается от нее своей протяженностью во времени. В то время как сериализация предоставляет только полностью заполненную данными сущность, гидрация производится уже во время использования сущности пользователем.

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

    Декоратор для гидратора из приведенного по ссылке кода - это просто еще одна попытка использовать любимый термин. Гидратор в этом коде гидратором не является по всем своим признакам. Это - сериализатор.
    Ответ написан
    Комментировать
  • Как грамотно переписать фабрику?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Однажды я уже показывал как могла бы выглядеть абстрактная фабрика.
    Абстракция там достигается за счет реконфигурируемости самой фабрики.
    Показанная там реализация служит хорошим примером и позволяет понять общий подход, но не годится для использования в настоящем коде. Эту реализацию нужно дорабатывать уже под свои нужды.
    Ответ написан
    Комментировать
  • Как лучше реализовать архитектуру MVC/MVP?

    @MarkusD
    все время мелю чепуху :)
    Первым делом стоит обратиться к описанию[T] MVP от Мартина Фаулера.

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

    Что будет если сделать по-другому. Например, как описано у тебя в вопросе. Сейчас у себя ты потерял гибкость и буквально разрушил абстракцию презентера. Если презентер вдруг потребуется сделать композитным (например, выбирать видимые и доступные для ввода элементы вида), ты упрешься в рефакторинг и вида, и презентера. Это отсрочит реализацию композитности логики презентера. Если потребуется держать одновременно несколько видов для одной модели, тебе придется как-то внедрять Change Propagation. В рядовом MVP за это отвечает презентер, а у тебя выйдет что вид должен подписываться на уведомление, что характерно уже не для MVP, а для MVC.

    MVP, как и MVC, является архитектурным шаблоном. Такие шаблоны находятся на самом верхнем уровне пирамиды отношений шаблонов. Это говорит о том, что уже просто реализация MVC/MVP в лоб в коде является нежелательной. MVC/MVP задают для кода UI строгое разделение по функциональности: ввод данных, процессинг и вывод данных. Вот что должно явно присутствовать в твоем коде, вот что стоит реализовать с использованием шаблонов дизайна и идиом разработки. Например, презентер или контроллер может быть сформирован на базе Rx и быть полностью децентрализованным, но при этом качественно выполнять свои функции. А вид и вовсе может быть data-driven объектом, т.е. не иметь даже минимальной личной логики.
    Каждый архитектурный шаблон, помимо легкой поддержки, сформирован из расчета на изначальную простоту, возможность стыковки с другими архитектурными шаблонами и потенциальную расширяемость.
    В результате, заложив изначально слабую реализацию MVP, дальнейшими действиями ты рискуешь размыть границы элементов архитектуры, снизить прозрачность реализации для понимания другими людьми и усложнить поддержку этого кода.
    Ответ написан
    Комментировать
  • Что дает паттерн билдер по сравнению с обычными сеттерами?

    @MarkusD
    все время мелю чепуху :)
    Для начала стоит обратиться к какому-нибудь источнику информации об этом шаблоне. Например, к этому.
    Представьте сложный объект, требующий кропотливой пошаговой инициализации множества полей и вложенных объектов. Код инициализации таких объектов обычно спрятан внутри монструозного конструктора с десятком параметров. Либо ещё хуже — распылён по всему клиентскому коду.


    Иными словами, под монструозным конструктором понимается намеренное создание монолита. Монолит создается, как правило, один раз и потом тухнет до полного разложения его кода, когда пользоваться им уже невозможно в следствии объема его ошибок, но и отрефакторить тоже невозможно в следствии его монолитной организации.

    А под ещё хуже понимается именно вот этот метод с интерфейсом полным сеттеров, которые призваны инициализировать объект до его правильного состояния. Только вот в чем проблема: в каком состоянии будет находиться объект до вызова всех нужных сеттеров и завершения его инициализации? В неправильном состоянии, но объект уже будет и воспользоваться им в таком состоянии кто-то обязательно захочет. А ведь еще кто-то должен знать эту самую последовательность(последовательности?) вызова сеттеров чтобы завершить инициализацию такого объекта. Не многовато ли избыточной ответственности на клиентский код возлагается?

    Давай представим что нам надо выполнить рейс на МИ-8. Но чтобы автоматизированная система современного аэродрома дала нам разрешение на взлет, нам надо передать ей специальный хеш.
    Как это могло бы быть...
    МИ-8 - старая штука, на ее борту механический бортовой компьютер - что-то по аналогии с машиной Ч. Беббиджа, только поменьше. Современные машины формируют этот хеш сами из своей телеметрии процесса запуска и маршрута полета. У нас же с собой только мобильник с ПО для формирования этого хеша вручную и обширная приборная панель МИ-8.
    И вот, мы приступаем к запуску МИ-8: запускаем каждый механизм и вписываем показания приборов в ПО на мобильнике. Когда вертолет запущен и план полета установлен, мы жмем в ПО на мобильнике кнопку генерации хеша и получаем строчку этого хеша.
    В связи с тем, что старой техники со своими условностями море, а формат хеша единый для всех, ПО ручной генерации хеша для каждой модели техники сделали отдельно от ПО передачи хеша в диспетчерскую аэродрома. Да и вообще, ПО генерации хеша для старой техники пишется в КБ производителя этой техники. Поэтому нам выдали строковое представление хеша и теперь его надо скопировать из одного ПО и вставить в нужное поле для отправки в другом ПО...

    Так, стоп. Кажется хеш - это интерфейс, оператор ПО внезапно оказался посредником, а ПО для ручной генерации хеша - это билдер что ли? Все именно так.
    Билдер хорошо знает устройство того типа, который он строит. Но ни о самом этом типе, ни о его устройстве посреднику можно не знать. Посредник может знать только интерфейс (это важно, т.е. даже не сам тип билдера, а только его интерфейс) билдера и интерфейс создаваемого объекта. Посредник имеет право обойтись малыми знаниями, которых достаточно для того чтобы передать артефакт работы билдера потребителю.
    Билдер может строить объект совсем другого типа данных, отдает он всегда некоторый обобщенный интерфейс.
    Вдобавок, создаваемый нашим билдером из примера хеш является DTO - т.е. Data Transfer Object, среди прочих свойств которого можно обозначить иммутабильность.


    Иммутабильный объект можно получить через идиому RAII или фабричного метода. Но что если для создания иммутабильного объекта требуется очень много параметров? Например - 16 параметров. Или 23, как в одном случае. Или - 42 параметра.
    Что если часть этих 42 параметров можно вычислить из остальных, но вот иногда их нужно указывать явно?
    Что если всего параметров 42, но для конструирования требуется использовать лишь произвольное подмножество этих параметров? Припоминаются SQL-запросы, правда ведь?
    RAII в этом случае захлебывается и становится непонятным, а фабричных методов требуется столько, что ими становится тяжело управлять. Идиома фабричного метода в этом случае начинает проявлять свои негативные качества и тормозить разработку.
    А билдер со всеми такими случаями легко справляется. Напомню, что Immutable в принципе невозможно снабдить сеттерами, т.к. это нарушит иммутабильность. А если выйти из ситуации через преобразование одного Immutable в другой, то достичь таким способом получится лишь комбинаторного взрыва иммутабильных типов, похоронив тем самым дальнейшую разработку.

    Но давай представим другой пример. Допустим, система диспетчеризации аэродрома принимает сигнал о готовности взлета со стороны борта. Разрешать взлет или нет? Кажется, сперва надо то-то проверить перед резолюцией.
    Как это могло бы быть...
    Нужно осмотреть журналы движения судов, сверить полетные расписания, сделать сверку документов пилота на допуск к полетам сегодня и еще что-нибудь малозначительное, вроде проверки расписания уборки взлетного полотна, чтобы какой-нибудь уборщик не взлетел вместе с бортом на воздух.
    Все это - запросы к куче разных сервисов. Какой-то сервис может Быть слабонагруженным, а у какого-то могут быть перебои в работе. Система же распределенная и отказоустойчивая, в ней есть дублирующие узлы, на которые стоит отправить повтор запроса в случае отказа по запросу с основного узла.
    Все данные нужно собрать в экземпляре сложной аналитической системы, которая состоит из множества довольно сложных стратегий работы со своими данными. Это все значит, что объект, с которым должна работать система диспетчеризации, будет иметь крайне сложный инвариант своего типа. Такой инвариант невозможно собрать на сеттерах, объект просто не склеится. К тому же, зачем системе диспетчеризации знать все тонкости типа такой сложной аналитической системы, когда ей нужен просто ответ - можно вот этому борту взлетать или нет?
    Тут стоит отметить и то, что решение такой задачи в процедурном стиле приведет к все тому же комбинаторному взрыву и перегрузке по логике, потому что сервисный код запросов будет тесно переплетен с кодом логики принятия решений.
    Но система диспетчеризации может просто взять билдер и сформировать на его базе граф асинхронных задач запросов к внешним системам. Именно в топологии этого графа и отражаются зависимости запросов к резервным системам, а результаты запросов складываются в билдер в произвольном, полностью асинхронном порядке.
    Когда билдер возвращается к системе диспетчеризации из асинхронной среды, система диспетчеризации просто создает из него объект аналитической системы, с которой дальше и ведет все свои беседы по душам.


    Билдер призван решать вопросы перегрузки по знаниям типов, перегрузки по сохранению отказоустойчивости и перегрузки по сложности создания экземпляров. И использовать его стоит именно для решения таких задач.
    Еще билдер позволяет развести логику сеттеров отдельно от логики результирующего типа, это бывает очень полезно.
    И, да, важно отметить еще и то, что если инвариант типа допускает наличие сеттеров, если в том, что пользовательская система знает этот тип в лицо, нет проблемы, если сами сеттеры не перегружают логику типа, то билдер уже явно не нужен.
    Ответ написан
    1 комментарий
  • Большие шаблонные классы( реализуя CRTP). Все пихать в один .h файл?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Сегодня с помощью шаблонов делается очень много предварительных вычислений на этапе компиляции, чтобы не делать эти вычисления вручную и не проводить их на этапе исполнения. Не всегда такие шаблоны могут похвастаться компактностью кода.
    Прежде всего, шаблоны служат для того чтобы автоматизировать генерацию кода. Не всегда требующий генерации код будет именно компактным или простым. Любой умный указатель или стандартный контейнер легко выбьется за пределы обозначенных 400 строк лишь только своим телом. А если учесть еще и команду поддержки такого стандартного шаблона, то там счет пойдет уже на тысячи строк.

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

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

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

    По поводу организации кода.
    Код шаблонов точно так же можно организовать в наборы файлов. Для шаблонов точно так же разрешена и дружественность, и предварительное объявление, и определение не по месту объявления.
    Реализацию шаблонов можно развести между заголовками (.h-файлами) и файлами встраиваемых реализаций (.inl-файлами).
    При этом важным остается правило доступности шаблона из места его инстанцирования. Т.е. вся группа файлов с реализацией шаблона должна быть как-либо связана с заголовком объявления шаблона, который далее планируется предоставлять пользователю шаблона.
    Ответ написан
    2 комментария
  • Проектирование и архитектура приложений?

    @MarkusD
    все время мелю чепуху :)
    Самое первое и самое главное: клиентский код должен оставаться чистым, состоять как можно больше из кода реализации алгоритма и как можно меньше из кода обслуживающего эту реализацию.
    Клиентский код ничего не должен знать о том, как ты хочешь, например, оповещать пользователя об исключительных ситуациях. Ему просто незачем это знать, ему важно просто работать.

    Поэтому, from classes import MyTelegramm - думаю, не стоит. Мне там и try-catch тоже не нравится. Пусть пользователь блоки try-catch пишет только для себя и только тогда, когда ему это надо. Когда он точно может словить исключение и выкрутиться из ситуации. Про такие исключения тебе, по-хорошему, и знать-то не стоит, т.к. они лишь меняют ветвь исполнения, но не обрывают его.
    Клиентский код стоит выполнять в контролируемой среде, внутри специального защитного блока try-catch, в котором ты уже будешь ловить все доступные тебе исключения и производить их диспетчеризацию.

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

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

    Диспетчер начал бы свою работу с того, что завернул бы исключение в конверт (Envelope, [2], [3]). Конверт просто легче читать, чем исключение непредсказуемого типа. Заворачивание я бы сделал с помощью абстрактной фабрики (Abstract factory) конвертов, в которой исключение каждого типа и от каждого источника было бы завернуто в правильный конверт согласно конфигурации фабрики.

    Получив конверт, диспетчер посетил(Visitor) бы с этим конвертом почтовый ящик каждого адресата. Почтовый ящик адресата можно снабдить стратегией (Strategy) приема конвертов, согласно которой почтовый ящик или перепишет конверт к себе, или оставит без внимания, в зависимости от написанных на конверте данных. Управлять почтовыми ящиками может локальный реестр (Registry) диспетчера, в котором так же может быть реализована и функция обхода (Visit) для посещения почтовых ящиков.

    Почтовый ящик обслуживает адресата. Адресат может подписаться (Observer) на поступление письма, а может приходить к ящику за новыми письмами по расписанию и доставать только первые несколько писем, а не все. Как выгребать почтовый ящик - вопрос конфигурации адресата.
    Когда адресат забирает письма, он раздает их в конечные точки, в роли которых уже и будут выступать все эти MyTelegramm или MyLog, но строго сконфигурированные и принимающие уже только копию письма.

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

    Ну и, конечно же, любую часть всего этого безобразия можно отбросить и написать проще. :)
    На начальных этапах можно заглушить стратегии и свести конфигураторы к виду линейной функции.
    MVP тут, на мой взгляд, будет выглядеть как цельная система с гарантированной доставкой конверта с любым исключением во все созданные конечные точки, проводя их от защитного контура, через диспетчер и до адресата через почтовый ящик.
    Ответ написан
  • Как реализовать фабричный метод без switch?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Фабричный метод - это т.н. виртуальный конструктор. Внутри функции не должно быть конструкций выбора. Сам фабричный метод передается в другое место для порождения объектов некоторого класса с определенным интерфейсом.

    Конструкции выбора обычно присутствуют внутри абстрактных фабрик. В фабрику приходит некий идентификатор типа и (если идентификатор определен) фабрика создает объект некоторого класса с определенным интерфейсом.

    Абстрактную фабрику можно реализовать на основе контейнера фабричных методов. Шаблоны C++ и стандарты C++11/14 нам в этом только помогут. Самый простой код такой фабрики может выглядеть вот так:
    Пример фабрики
    template< typename TInterface, typename... TArguments >
    class AbstractFactory final
    {
    public:
    	// Produce the implementation, but return the pointer to interface.
    	inline std::shared_ptr<TInterface> Produce( const std::string& implementation_name, TArguments... arguments )
    	{
    		auto found_function = m_factory_functions.find( implementation_name );
    		return ( found_function == m_factory_functions.end() )? std::shared_ptr<TInterface>{} : found_function->second( std::forward<TArguments>( arguments )... );
    	};
    	
    	// Define the implementation.
    	template< typename TImplementation >
    	inline const bool DefineImplementation()
    	{
    		return DefineImplementation<TImplementation>( TImplementation::ClassName() );
    	};
    	
    	// Define the implementation.
    	template< typename TImplementation >
    	inline const bool DefineImplementation( const std::string& implementation_name )
    	{
    		// Abort the incorrect registration.
    		static_assert( std::is_base_of<TInterface, TImplementation>::value, "Implementation may only be derived from interface of Factory." );
    		
    		auto found_function = m_factory_functions.find( implementation_name );
    		if( found_function == m_factory_functions.end() )
    		{
    			m_factory_functions[ implementation_name ] = &AbstractFactory<TInterface, TArguments...>::template ConstructImplementation<TImplementation>;
    			return true;
    		};
    		
    		return false;
    	};
    	
    	// Check the implementation name is already defined.
    	inline const bool IsImplementationDefined( const std::string& implementation_name ) const
    	{
    		return m_factory_functions.find( implementation_name ) != m_factory_functions.end();
    	};
    	
    private:
    	// The factory function just produce implementation.
    	template< typename TImplementation >
    	static std::shared_ptr<TInterface> ConstructImplementation( TArguments... arguments )
    	{
    		return std::static_pointer_cast<TInterface>(
    			std::make_shared<TImplementation>( std::forward<TArguments>( arguments )... )
    		);
    	};
    
    private:
    	// Factory function produces the implementations of TInterface.
    	using FactoryFunction	= std::shared_ptr<TInterface> (*)( TArguments... arguments );
    	
    	std::unordered_map<std::string, FactoryFunction>	m_factory_functions;
    };


    Работает она примерно так:
    cpp.sh/93obm
    Ответ написан
    Комментировать
  • Для чего нужен singleton?

    @MarkusD
    все время мелю чепуху :)
    В буквальном смысле синглтон призван упростить владение, инициализацию, контроль времени жизни и доступ к элементам глобального состояния программы. Эта фраза является самой важной. А важнее всего - правильно понять ее.

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

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

    В качестве альтернативы синглтонам пропагандируются два других подхода: Registry / Service locator и Dependency injection.
    К слову, все альтернативы столь же активно и все так же незаслуженно обзываются антипаттернами. :)

    Теперь к примерам.
    - DirectX. Для работы с графикой тебе необходимо по одной инстанции интерфейсов IDirect3D и IDirect3DDevice. Эти две инстанции декларируют глобальное состояние программы. Инстанции всех буферов, текстур, шейдеров и поверхностей создаются с использованием этих инстанций. Разумным будет предоставить доступ к инстанциям DirectX через подход синглтона.
    - OpenGL старых добрых версий. Процедурный интерфейс OpenGL как бы намекает на отсутствие необходимости в глобальном состоянии. Но не тут то было. Для работы с OpenGL необходимо не просто создать контекст, но еще и помнить поток, в котором этот контекст связан с поверхностью вывода. В многопоточной среде контекстов может быть несколько для параллельной загрузки ресурсов. В этом случае помнить надо уже два потока и два контекста (минимум). Само собой, в синглтоне это глобальное состояние смотрится удобнее.
    - Sockets. Не важно какие. Когда твое приложение представляет собой MMO проект и у тебя гора подсистем, постоянно и обособленно общающихся с сервером, сетевое подключение разумно оформить в виде синглтона.
    - Assets/Resources - они бывают разные, кешируемые и нет, доступные из сети, с жесткого диска, из подсистемы пререндеринга. Опять же, я несколько раз видел боль и страдания от неоднородного контроля ресурсов без соответствующей подсистемы. А сама подсистема управления ресурсами всегда централизована и лучше всего реализуется именно на синглтоне.

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