structural design patterns
TRANSCRIPT
ВАМ ПРИВЕТ
Advanced OOP : Структурные паттерныYou are Ready?
Alexander Babenko (HuktoDev)iOS DeveloperImprove Digital
Адаптер (Adapter)
Alternate Name : (Wrapper)
• Нужно использовать класс (например, из другой подсистемы), интерфейс которого несовместим
• Мы создали несколько реализаций с общим интерфейсом, и хотим сделать еще одну, но есть уже классное стороннее решение, и мы хотели бы его использовать без изменения интерфейсов
• Мы хотим сделать более подходящий и удобный интерфейс для объекта без изменения исходного объекта (например, он уже везде используется с исходным интерфейсом).
• Мы хотим отойти к более подходящей терминологии. Или добавить дополнительные контракты на входах и выходах. Или скрыть какие-то технологии. В общем, добавить какой-то дополнительный код
Мотивация
• Классическая реализация предлагает композицию
UML-диаграмма
• Разница с декоратором :• 1) Декоратор предлагает новый декорирующий функционал • 2) Адаптер именно адаптирует
• Адаптер обычно реализуется очень просто• Объем работ по адаптации может быть
разным, в зависимости от степени несовместимости интерфейсов
• Если изменяется адаптируемый класс - изменения нужно будет внести почти наверняка только в адаптер
• Есть еще 2 типа адаптеров :
Результаты и нюансы использования :
Сменный адаптер (динамический)Двусторонний адаптер
• По-другому можно назвать “Каркас для плагинов”• Задача в том, чтобы использовать один и тот же объект для
адаптации разных технологий или реализаций• Адаптер без привязки к конкретному адаптируемому объекту.• Адаптер привязывается только к конкретному протоколу
адаптируемого объекта• Обычно используется в крупных системах, или фреймворках,
когда другому разработчику предоставляется возможность написать плагин (сделать инъекцию собственного кода для исполнения)
Сменный адаптер
Адаптируемый (Плагин)
Ядро плагина (Плагин Handler)
CustomPluginProtocol
PrivatePluggableAdapterProtocol
Плагины :• Есть несколько способов реализации сменных
адаптеров (каркас + набор плагинов)
• Реализация через композицию - это, фактически, то же самое делегирование. Например, кастомный DataSource или Delegate для TableView можно считать плагином
• Можно параметризовать блоками с исполнительным кодом. В этом случае у нас отсутствует адаптируемый объект, мы просто наполняем каркас кодом
• Использование базового класса с абстрактными операциями. Например, мы делаем подкласс UIViewController и переопределяем методы LifeCycle
Пример :
Мост (Bridge)Alternate Name :
(Handle/Body)
• Нужно, чтобы абстракция и реализация были независимы, обладали бы минимальными сведениями друг о друге (чтобы их можно было изменять независимо). Даже в отдельных проектах. Например Core -> Custom Targets
Суть проблемы :
Абстракция Реализация
Некорректное состояние дел :
• Трудно расширять• Появляются баги, так как имеется высокая связность
• В интерфейсах всплывают особенности реализации, из-за чего может быть проблема с переиспользованием, или если детачить какую-то технологию
• Отделяем реализацию от абстракции с помощью паттерна Мост
• Делаем, чтобы реализации зависели только от абстракции в одностороннем порядке (абстракция ничего не знает о реализациях)
Способ решения :
Spoiler:Я выделил 2 различных типа Моста :Мост по типу : Абстракция -> Реализация
Мост по типу : Публичная абстракция -> Приватная абстракцияMARK: Второй вариант - более общий и гибкий случай, но применимыйтолько в самых “запущеных” вариантах!
Абстракция -> Реализация :Конкретная технология
моста
Реализация 1
Реализация 2
Реализация 3
Единая абстракция
Базовый класс семейства приватных объектов
Публичная абстракция (Window на экране)
Конкретный интерфейс абстракции
Базовый/Абстрактный класс
Конкретный подкласс
Публичная абстракция -> Приватная абстракция :
Приватная абстракция (Window на экране iPhone)
Конкретный интерфейс абстракции
Реализация конкретного подкласса в семействе
<WindowProtocol>
Window
AlertWindow
<iOSWindowProtocol>
iOSWindow / tvOSWindow / AndroidWindow
iOSAlertWindow
Конкретная технология
моста
Window
AlertWindow
OverlayWindow
View
• Публичный интерфейс (мост) должен быть максимально узким
Особенности моста :• Интерфейс не должен иметь методов, привязанных к конкретным
технологиям или иным зависимостям
• Мы сообщаем об использовании ViewController-а Presenter-уПримеры плохих интерфейсов :
• Мы конфигурируем какую-то кастомную вьюшку напрямую из Presenter-а• Мы высовываем ReactiveCocoa в открытый интерфейс
• Только одна из реализаций ViewController-а для Login-экрана содержит метод для отображения алерта после неудачного логина
MARK: С помощью протоколов это можно обойти : такие методы можно объявлять, как optional (необязательная имплементация)
• Мост - это компиляция концепций абстракции, параллельных иерархий и адаптера
• Это паттерн, в котором очень легко запутаться, и который в разных местах по-разному трактуется. Очень часто путается со «Стратегией». Паттерн в трактовании вызвавший наибольшую головную боль!
• Еще одна задача Моста - обеспечить связь “1 ко многим” (1 ко многим реализациям, или 1 ко многим семействам объектов)
• Можно делать абстракцию и реализации даже в разных проектах. Они настолько отвязаны, что :
• Абстракция самодостаточна сама по себе• Для конкретной реализации нужно только наличие
набора интерфейсов, или базовой соединительной иерархии (подключаем проект или ядро с ней). В некоторых случаях даже они не нужны
Еще некоторые преимущества
• 1) Классический подход с помощью композиции (с использованием адаптера)
• 2) Подход с помощью абстрактного класса
• 3) Подход с помощью протоколов• 4) Подход с помощью isa-swizzling
Способы реализации моста :
Классическая реализация моста
• Применение паттерна “Адаптер” к семейству объектов. • Используется композиция (публичный
объект содержит приватный объект)• Позволяет динамическую подмену
реализации
• Наименее гибкий• Трудно создавать полностью независимые
абстракции• Реализация должна полностью повторять
и реализовывать интерфейс базового класса (интерфейсы идентичны)
Подход с помощью абстрактных классов :
• Не задает жесткой абстракции (имеет опциональные методы)
• Можно дополнить новый любой другой класс протоколом, как новым признаком и поместить в семейство реализаций
• Только для простого моста
Подход с помощью протоколов :
• Использование возможностей Obj-c Runtime. Подмена isa-инварианта класса
• Позволяет динамическую подмену реализации во время выполнения
• Интерфейсы обоих классов могут быть не идентичны, но оба класса должны реализовывать публичный интерфейс. С использованием рантайма можно обойти и этот изъян
• Оба класса обязаны иметь одинаковый Size (кол-во и типы ivar-ов должны быть одинаковы)
Подход с помощью isa-Swizzling :
• Увы, я действительно убедился, что для создания Моста между абстракциями нужен либо крышесносный изврат, либо классическая реализация.
• Только через адаптер можно избежать независимости абстракций
Параллельные иерархии
View
Label
ImageView
VideoView
iOSView
iOSLabel
iOSImageView
iOSVideoView
Параллельные композицииPresenter
View
Interactor
Router
Assembly
ViewInput/ViewOutput
InteractorInput/InteractorOutput
ModuleInput/ModuleOutput
RouterInput
AssemblyProtocol
Presenter
View
Interactor
Router
Assembly
ViewInput/ViewOutput
InteractorInput/InteractorOutput
ModuleInput/ModuleOutput
RouterInput
AssemblyProtocol
Presenter
View
Interactor
Router
Assembly
ViewInput/ViewOutput
InteractorInput/InteractorOutput
ModuleInput/ModuleOutput
RouterInput
AssemblyProtocol
BaseCommon module
Login module
StartGuest module
• Параллельные иерархии - это что-то вроде “двумерного” наследования. Часто используется создание параллельной иерархии через наследование. В этом случае получаем все преимущества наследования.
• Возможно наследование только от одного базового класса, и использование Моста между иерархиями
• Без иного соотнесения - бессмысленно.
• Параллельные композиции имеют смысл - только при полном наследовании целой композиции
• В таком случае мы получаем переиспользуемое взаимодействие между объектами целой подсистемы
Использование параллельности
MARK: Для крупных параллельных подсистем - выгодно использовать кодогенераторы. Generamba for Example
MARK: Для создания юнитов(модулей) параллельных подсистем хорошо используется Абстрактная фабрика
• Параллельные подсистемы могут быть полезны при должном использовании
• Нужно понимать “Нафига”• При добавлении нового уровня параллельности
количество классов и интерфейсов плодится экспоненционально
• Количество классов и без того растет очень шустро, что усложняет работу
• Появляется множество пустых, “зарезервированных” классов
Предостережение
Компоновщик (Composite)
• Мы хотели бы, чтобы один объект мог содержать себе подобные объекты
Мотивация :
• Мы хотели бы детализировать что-либо, атомизировать, и сгруппировать в иерархическую древовидную структуру
• Мы хотели бы иметь способ для представления и сборки составных объектов
• Нужно, чтобы способ взаимодействия с простым объектом, и сколь угодно сложным составным объектом был одинаков
• Иерархичность и совокупность - одни из базовых концепций мироздания, без которых был бы невозможен переход от любых простых форм к более сложным
Отступление про иерархии :
• Компоновщик является одним из центральных паттернов, он помогает строить иерархические структуры. Компоновка основана на базовой концепции Целое-частное.• В реальной жизни объекты компонуются по особенностям строения и назначения, а при разработке - по зонам ответственности
• Без компоновки мы бы работали с ужасными монстрообразными объектами, и тонули бы в сложности и ошибках
UML-диаграмма Компоновщика
• Компоновка - способ составления иерархий• Иерархия - отличный способ контроля над сложностью
• Главное - наличие общего способа взаимодействия - общего интерфейса компоновки
• Иерархии UIView, CALayer-ов, UIViewController-ов
Известные примеры компоновки :• Любые иерархии классов/интерфейсов (у нас единый
способ создания и работы с ними)• Текстовые иерархии (буквы -> слова -> предложения -
> абзацы -> страницы -> статья)• Иерархии выражений : ((a+b) * c) + d . Или например,
иерархии инструкций, операций и операндов в исполнительных файлах• Иерархии сотрудников в различных организациях (сотрудник —> отдел -> организация -> холдинг)
• Храним ли ссылку на родителя? Дочерний объект может как знать, так и не знать о своем родителе. Стоит делать ссылку для сохранения обратной связи, и возможности двустороннего обхода иерархии (не только от общего к частному). Упрощает работу с иерархической структурой
Нюансы реализации:
По сути есть только 3 варианта :
• Только родитель знает о том, что эти объекты - его сыновья• Только сыновья знают о том, кто их родитель
• Все знают обо всех• Например, в случае hit-test-а нужно идти от корневой вьюшки,
и искать наиболее подходящую. Потом все родительские вьюшки, если есть super-вызов оповещаются в touch Began/Moved/End методах
• Иногда у компонента может быть более 1го родителя (например, если использовать ссылки (использовать паттерн Приспособленец)). Например, можно взять одну вьюшку и добавить ее в качестве subview в 2 разных вьюшки
Нюансы реализации:
• Иногда операции для примитивов и составных объектов могут конфликтовать. В общем, на практике приходится либо в базовый класс выносить методы для составных объектов, или использовать проверки на класс (isKindOfClass:)
• Мы должны четко выбрать - прозрачность или безопасность (либо у всех объектов общий и небезопасный интерфейс, либо безопасные, но различные интерфейсы)
• Можно выбирать разные способы упорядочивания потомков (важен ли порядок, например Array или Set)
• С протокол-ориентированной разработкой можно уйти от общего предка, и сделать возможность создания даже нескольких различных вариантов примитивов + набор необязательных методов для составных объектов
• Первым признаком использования паттерну Компоновщик - является использование префиксов parent/child, super/sub
• Для обхода иерархий лучше всего использовать рекурсивные алгоритмы вместе с Итераторами
• Для следования по иерархии вверх следует использовать паттерн Chain of Responsibility (Цепочка обязанностей)
Пример реализации:• Реализуем часть каркаса для событийно-ориентированной
архитектуры. А именно - простое событие и составное событие. Представим, что каждое событие может иметь набор связанных событий, и из них будет составляться иерархическая структура
Пример реализации:• Примеры событий :• Interface Drag&Drop event = Drag event + change Location events +
Drop event• Target-action event = Interface event + Transaction(Action) Events +
Callback event
Пример реализации:
Фасад (Facade)
• Мы бы хотели абстрагироваться от особенностей реализации системы, и иметь возможность управлять ей максимально упрощенно
Мотивация :
• Нам нахрен не надо знать о тысяче внутренних объектов, о нюансах использования, мы просто хотим написать 1-2 строчки / сделать простое действие, и чтобы это заработало!• Так же, как мы не хотим знать о внутренних особенностях класса, так же мы не хотим знать и о внутренних особенностях подсистемы
• Нам нужно минимизировать количество сущностей, которые мы держим в нашей голове. Для огромной системы мы хотели бы мыслить полностью в терминологии ее пакетных подсистем.
Результат :• Паттерн Фасад - унифицированный интерфейс, вместо набора
интерфейсов подсистемы, снижает связность системы, централизует взаимодействие (единая точка входа в систему)
• Фасад хорошо объяснять на примерах из реальной жизни :
• Начальнику не надо знать нюансов работы уборщицы его 5го заместителя
• Мы воспринимаем автомобиль, как почти что цельный объект и в повседневной жизни взаимодействуем с ним, как с транспортным средством. Нам не надо знать об особенностях двигателя и тормозной системы, и нас это очень огорчает, когда нам приходится воспринимать автомобиль, как сложный структурный объект• Мы установили фреймворк Fabric через менеджер зависимостей , и подключили CrashlyticsKit в делегате приложения одной строчкой, и скорее всего, все сразу стали счастливы =)
• Скорее всего, те фреймворки и инструменты, работа с которыми проста, и приносит вам наслаждение - они просто используют принцип фасадности
• Фасад - это та же инкапсуляция, применительно к подсистеме
About Facade :
• Часто оставляют обратную совместимость, и нам можно работать, как с фасадом подсистемы, так и с отдельными модулями подсистемы (например, мы просто подключаем специальный набор библиотек, и работаем уже с внутренностями)
• Фасад - это выделенный интерфейс подсистемы• Фасад - это специальный отдельный интерфейс для
пакета модулей• Если еще точнее - Фасад - это отдельный объект,
который реализует наиболее часто используемые взаимодействия с подсистемой. Он - тот, кто знает, когда и к кому обратиться по адресу
• Резюмируя : Фасад призван упростить нам жизнь
• Можно оставить обратную совместимость, и дать будущее пользователю выбор между простотой и общностью
Нюансы использования и реализации :
• Вместо единого фасада - можно сделать фасадный протокол, и сделать сколько угодно реализаций подсистемы, с полностью разным набором пакетных модулей. Например, мы можем работать на верхнем уровне с MVVM и VIPER-модулями через единый интерфейс, и нам даже не нужно знать, какого типа модуль перед нами
• Мы можем изменять подсистему полностью независимо, главное чтобы у нас был функционирующий фасад.
• Нужно выбирать, какие части подсистемы делать публичными, а какие - приватными. Обычно есть публичный хедер, и остальные модули можно подключить вручную
• Фасад - это не обязательно единый объект. Например, 100 объектов подсистемы могут представляться 3-4мя публичными объектами
• По сути фреймворки по типу Magical Record, Rest Kit - тоже являются фасадами над технологиями более низкого уровня.
• ServiceLocator-объект обеспечивает фасадность над сервисами (это не совсем антипаттерн, просто есть паттерн лучше)
• UIKit тоже основан на CoreFoundation и наборе фреймворков по типу Core Graphics, Core Animation. Он тоже обеспечивает фасадность.• Хороший пример использования - ViperMcFlurry фабрики с методами переходов open чего-то там. В них мы используем в лучшем случае Input/Output протоколы модулей, и не думаем, как устроен конкретный Interactor или View
Примеры использования :
Связи с другими паттернами :• Внутри выгодно использовать абстрактную фабрику для
порождения объектов подсистемы. Абстрактная фабрика может быть альтернативой фасаду
• Похож на медиатора(посредника). Только посредник связывает объекты внутри подсистемы, а фасад обеспечивает способ взаимодействия с системами извне
• В Typhoon - это классы TyphoonAssembly и TyphoonDefinition
• Существует способ создания фасада с помощью множественного наследования.
• Он обладает рядом недостатков, но все-же иногда применим• Фактически, создание объекта, физически объединяющего
интерфейсы нескольких классов в одном - это нарушение SOLID. • Но…
Альтернативный фасад
• Если у нас имеется набор совсем мелких классов с 1-2мя методами в интерфейсе, и у нас есть хороший объединяющий (склеивающий) признак. Например, если у меня user registration service, user authorization service и user token service - я могу из них составить фасад UserService, объединив 3 класса в один. Плюс такого подхода - упрощение работы, и уменьшение количества объектов в системе.
• см. IDCommonFacade
• Это сильно упрощенная фасадность. Ведь у нас нет взаимодействия между объектами подсистемы, и мы не создаем упрощенный интерфейс
Заместитель (Proxy)Alternate Names :
(Surrogate / Promise)
• Для некоторого набора задач мы бы хотели иметь объект, который бы скрывал или заменял реальный объект.
Мотивация :
• Пока объект не готов - вернуть временные заменяющие данные (Placeholder, Dummy Object, Fake data)• Объект в другом адресном пространстве. Например, какой-то объект на удаленном сервере (посол)
• Защищающий заместитель, решающий, предоставлять права пользования объектом, или нет
• Виртуальный заместитель (создающий объект по требованию)
• Экранирующий заместитель (защищает объект от опасных взаимодействий, клиентов)
• Необходимо иметь доступ к объекту через другой объект, который может иметь дополнительную функциональность. При этом извне, чтобы клиенты не знали о существовании объекта-заместителяПримеры использования :
• Различные контейнеры • Другие варианты
UML-диаграмма Proxy
Сравнение Proxy со схожими шаблонами :
• Адаптер обеспечивает отличающийся интерфейс к объекту.
• Прокси обеспечивает тот же самый интерфейс.• Декоратор обеспечивает расширенный интерфейс.
Примеры использования в iOS :• Typhoon - TyphoonReferenceDefinition-объекты, которые
обозначают определения, еще не заполненные инъекциями• Foundation - PlaceholderArray
• ViperMcFlurry - RamblerViperOpenModulePromise - переходные объекты-модули, которые еще не заполнены всеми требуемыми значениями
• В ReactiveCocoa есть Proxy-объекты• В Foundation есть возможность загружать Asset-ы
приложения через Bundle по требованию (On Demand)• В Foundation представляется базовый класс NSProxy
для создания прокси-объектов• OCMock-фреймворк для Unit-тестирования широко
использует концепцию проксирования
Технологии для проксирования в obj-c• NSProxy - облегченная версия NSObject-а, с основным
функционалом для форвардинга (перенаправление сообщения объекта, или подмена реализации)
• Можно переопределить вручную, если переопределить все методы, и направить на соответствующий объект, но это небезопасно. Форвардинг помогает автоматизировать этот процесс.
• В качестве Proxy можно без проблем использовать и любой NSObject-объект
• - (void)forwardInvocation:(NSInvocation *)invocation;
Можно изменить вызов, подделать любой параметр, выполнить произвольный код, в т.ч. перенаправить Invocation на другой объект, и даже подменить селектор!Срабатывает в том случае, если метод не был найден ни в классе, ни в одном из суперклассов
• - (id)forwardingTargetForSelector:(SEL)aSelector
Достаточный метод для простого перенаправления на подходящий таргет (реальный объект. ). Срабатывает, если в классе и суперклассах не реализован метод по заданному селектору. Можно указать таргет, у которого поискать селектор
Приспособленец (Flyweight)
• Объекты не всегда должны знать все, что им нужно для работы
Мотивация :• Мы хотим разграничить внутренние знания и умения объекта с
существующим контекстом (средой деятельности объекта)• Мы не хотим делать всемогущие и всезнающие объекты• Мы хотим иметь возможность использовать один объект в
разных контекстах, или даже в нескольких контекстах одновременно
Примеры из реальной жизни :• Человек и среда. Человек сам по себе обладает рядом умений и
знаний, может быть помещен в самую разную среду (контекст), и в среде его поведение будет обусловлено им самим + условиями и факторами среды
• Графические объекты —> Графический редактор• Табличные Item-ы —> Current Context (number page, page
size) • Текстовые объекты в текстовом редакторе (Информацию о конкретных стилях текста может содержать сам контекст)• Табличные ячейки —> видимая область TableView (ячейки ничего не знают о том, видны они или нет)
• Нам нужна оптимизация, и как-то надо избежать “мульена" схожих объектов
UML-диаграмма Flyweight
• Основной смысл приспособленца - именно в разграничении
• Многие путают его с объектным пулом• Можно было бы назвать также этот паттерн
“Контекстом”.
• Вопрос, как именно реализовать разграничение между внешним и внутренним состоянием
Особенности реализации :
• Как связать (ассоциировать) внешнее состояние с внутренним (можно использовать Dictionary и спец. идентификаторы, например)• Как получить приспособленца? Обычно это делают через Пул объектов
MARK: Еще один пример из жизни : Файл и его метаданные
• Обычно приспособленцы - это либо иммутабельные объекты, либо при получении объекта из пула - он копируется (может использоваться Прототип)
MARK: Еще пример : Ячейки CollectionView и их Layout
• Состояние и Стратегию имеет смысл делать через Приспособленца
Родственные паттерны :
• В целом, не существует канонического способа реализации Flyweight
СПАСИБО ЗА ВНИМАНИЕЕсли у вас возникли вопросы, я с удовольствием обсужу их с вами.
Alexander Babenko (HuktoDev)iOS DeveloperImprove Digital