Слово «фабрика» – безусловно одно из самых часто употребляемых программистами при обсуждении своих (или чужих) программ. Но смысл в него вкладываемый бывает очень разным: это может быть и класс, порождающий объекты (полиморфно или нет); и метод, создающий экземпляры какого-либо типа (статический или нет); бывает, и даже просто любой порождающий метод (включая, конструкторы).
Конечно, не все, что угодно, порождающее экземпляры чего-либо, может называться словом «фабрика». Более того, под этим словом могут скрываться два разных порождающих шаблона из арсенала «Банды четырех» – «фабричный метод» и «абстрактная фабрика», в подробности которых я и хотел бы немного углубиться, уделяя особое внимание классическим их пониманию и реализации.
А на написание этого очерка меня вдохновил Джошуа Керивски (глава «Industrial Logic»), а точнее, его книга «Refactoring to Patterns», которая вышла в начале века в рамках серии книг, основанной Мартином Фаулером (именитым автором современной классики программирования – книги «Рефакторинг»). Если кто-то не читал или даже не слышал о первой (а я знаю таких много), то обязательно добавьте ее себе в список для чтения. Это достойный «сиквел» как «Рефакторинга», так и еще более классической книги – «Приемов объектно-ориентированного проектирования. Паттерны проектирования».
Книга, помимо прочего, содержит в себе несколько десятков рецептов избавления от различных «запахов» в коде с помощью шаблонов проектирования. В том числе и три (как минимум) «рецепта» на обсуждаемую тему.
Керивски в своей книге приводит два случая, когда применение этого шаблона будет полезным.
Первый – это инкапсуляция знаний о конкретных классах, связанных общим интерфейсом. В таком случае этими знаниями будет обладать лишь тип, являющейся фабрикой. Публичный API фабрики будет состоять из набора методов (статических или нет), возвращающих экземпляры типа общего интерфейса и имеющих какие-либо «говорящие» названия (чтобы понимать, какой метод необходимо вызвать для той или иной цели).
Второй пример очень похож на первый (и, в общем-то, все сценарии использования паттерна более-менее подобны друг другу). Речь идет о случае, когда экземпляры одного или нескольких типов одной группы создаются в разных местах программы. Фабрика в этом случае опять-таки инкапсулирует знания о создающем экземпляры коде, но с несколько иной мотивацией. Например, это особенно актуально, если процесс создания экземпляров этих типов сложный и не ограничивается вызовом конструктора.
Чтобы быть ближе к теме разработки под «iOS», удобно упражняться на подклассах
Предположим, мы в приложении торгуем средствами передвижения, и от типа конкретного средства зависит отображение: мы будем использовать разные подклассы
Таким образом, у нас есть семейство объектов одной группы, экземпляры типов которых создаются в одних и тех же местах в зависимости от какого-то условия (например, пользователь нажал на товар в списке, и в зависимости от того, самокат это или велосипед, мы создаем соответствующий контроллер). Конструкторы контроллеров имеют некоторые параметры, которые также необходимо каждый раз задавать. Не свидетельствуют ли эти два довода в пользу создания «фабрики», которая одна будет обладать знаниями о логике создания нужного контроллера?
Конечно, пример достаточно простой, и в реальном проекте в похожем случае вводить «фабрику» будет явным «overengineering». Тем не менее, если представить, что типов транспортных средств у нас не два, а параметров у конструкторов – не один, то преимущества «фабрики» станут более очевидными.
Итак, объявим интерфейс, который будет играть роль «абстрактной фабрики»:
(Довольно краткий «гайдлайн» по проектированию «API» на языке «Swift» рекомендует называть «фабричные» методы начиная со слова «make».)
(Пример в книге банды четырех приведен на «C++» и основывается на наследовании и «виртуальных» функциях. Используя «Swift» нам, конечно, ближе парадигма протокольно-ориентированного программирования.)
Интерфейс абстрактной фабрики содержит всего два метода: для создания контроллеров для продажи велосипедов и самокатов. Методы возвращают экземпляры не конкретных подклассов, а общего базового класса. Таким образом, ограничивается область распространения знаний о конкретных типах пределами той области, в которой это действительно необходимо.
В качестве «конкретных фабрик» будем использовать две реализации интерфейса абстрактной фабрики:
В данном случае, как видно из кода, конкретные фабрики отвечают за транспортные средства разного состояния (новые и подержанные).
Создание нужного контроллера отныне будет выглядеть примерно так:
Теперь вкратце пробежимся по примерам использования, которые предлагает в своей книге Керивски.
Первый «кейс» связан с инкапсуляцией конкретных классов. Для примера возьмем те же контроллеры для отображения данных о транспортных средствах:
Предположим, мы имеем дело с каким-либо отдельным модулем, например, подключаемой библиотекой. В этом случае объявленные выше классы остаются (по умолчанию)
Второй «кейс» описывает сложную инициализацию объекта, и Керивски, в качестве одного из путей упрощения кода и оберегания принципов инкапсуляции, предлагает ограничение распространения знаний о процессе инициализации пределами фабрики.
Предположим, мы захотели продавать заодно уж и автомобили. А это, несомненно, более сложная техника, обладающая бóльшим числом характеристик. Для примера ограничимся типом используемого топлива, типом трансмиссии и размером колесного диска:
Пример инициализации соответствующего контроллера:
Мы можем ответственность за все эти «мелочи» водрузить на «плечи» специализированной фабрики:
И создавать контроллер уже таким образом:
Второй «однокоренной» шаблон также инкапсулирует знания о конкретных порождаемых типах, но не за счет сокрытия этих знаний внутри специализированного класса, а за счет полиморфизма. Керивски в своей книге приводит примеры на «Java» и предлагает пользоваться абстрактными классами, но обитатели вселенной «Swift» с таким понятием не знакомы. У нас тут своя атмосфера… и протоколы.
В качестве классического примера пользы шаблона рассмотрим случай, когда в иерархии разные типы имеют идентичную реализацию одного метода за исключением объекта, который в этом методе создается и используется. В качестве решения предлагается создание этого объекта вынести в отдельный метод и реализовывать его отдельно, а общий метод – поднять выше в иерархии. Таким образом, разные типы будут использовать общую реализацию метода, а объект, необходимый для этого метода, будет создаваться полиморфно.
Для примера вернемся к нашим контроллерам для отображения транспортных средств:
И предположим, что для их отображения используется некая сущность, например, координатор, который представляет эти контроллеры модально из другого контроллера:
При этом метод
Предлагаемое решение – это вынести создание используемого объекта в отдельный метод:
А основной метод – снабдить базовой реализацией:
Конкретные типы в таком случае примут вид:
Я попытался данную несложную тему осветить, совместив три подхода:
При этом я попытался быть максимально близким хрестоматийной структуре шаблонов, насколько это возможно, не разрушая принципы современного подхода к разработке под систему «iOS» и используя возможности языка «Swift» (вместо более распространенных «С++» и «Java»).
Как оказалось, найти подробные материалы на тему, содержащие прикладные примеры довольно сложно. Большинство существующих статей и руководств содержат лишь поверхностные обзоры и сокращенные примеры, уже довольно урезанные по сравнению с хрестоматийными версиями реализаций.
Надеюсь, хотя бы отчасти мне удалось достичь поставленных целей, а читателю – хотя бы отчасти было интересно или хотя бы любопытно узнать или освежить свои знания по данной теме.
Другие мои материалы на тему шаблонов проектирования:
А это ссылка на мой «Twitter», где я публикую ссылки на свои очерки и немного сверх того.
Конечно, не все, что угодно, порождающее экземпляры чего-либо, может называться словом «фабрика». Более того, под этим словом могут скрываться два разных порождающих шаблона из арсенала «Банды четырех» – «фабричный метод» и «абстрактная фабрика», в подробности которых я и хотел бы немного углубиться, уделяя особое внимание классическим их пониманию и реализации.
А на написание этого очерка меня вдохновил Джошуа Керивски (глава «Industrial Logic»), а точнее, его книга «Refactoring to Patterns», которая вышла в начале века в рамках серии книг, основанной Мартином Фаулером (именитым автором современной классики программирования – книги «Рефакторинг»). Если кто-то не читал или даже не слышал о первой (а я знаю таких много), то обязательно добавьте ее себе в список для чтения. Это достойный «сиквел» как «Рефакторинга», так и еще более классической книги – «Приемов объектно-ориентированного проектирования. Паттерны проектирования».
Книга, помимо прочего, содержит в себе несколько десятков рецептов избавления от различных «запахов» в коде с помощью шаблонов проектирования. В том числе и три (как минимум) «рецепта» на обсуждаемую тему.
Абстрактная фабрика
Керивски в своей книге приводит два случая, когда применение этого шаблона будет полезным.
Первый – это инкапсуляция знаний о конкретных классах, связанных общим интерфейсом. В таком случае этими знаниями будет обладать лишь тип, являющейся фабрикой. Публичный API фабрики будет состоять из набора методов (статических или нет), возвращающих экземпляры типа общего интерфейса и имеющих какие-либо «говорящие» названия (чтобы понимать, какой метод необходимо вызвать для той или иной цели).
Второй пример очень похож на первый (и, в общем-то, все сценарии использования паттерна более-менее подобны друг другу). Речь идет о случае, когда экземпляры одного или нескольких типов одной группы создаются в разных местах программы. Фабрика в этом случае опять-таки инкапсулирует знания о создающем экземпляры коде, но с несколько иной мотивацией. Например, это особенно актуально, если процесс создания экземпляров этих типов сложный и не ограничивается вызовом конструктора.
Чтобы быть ближе к теме разработки под «iOS», удобно упражняться на подклассах
UIViewController. И действительно, это точно один из самых распространенных типов в «iOS»-разработке, почти всегда «наследуется» перед применением, а конкретный подкласс при этом зачастую даже и не важен для клиентского кода.Я постараюсь сохранять примеры кода как можно ближе к классической реализации из книги «Банды четырех», но в реальной жизни часто код бывает упрощенным тем или иным образом. И лишь достаточное понимание шаблона открывает двери для его более вольного использования.
Подробный пример
Предположим, мы в приложении торгуем средствами передвижения, и от типа конкретного средства зависит отображение: мы будем использовать разные подклассы
UIViewController для разных средств передвижения. Помимо этого, все средства передвижения различаются состоянием (новые и б/у):enum VehicleCondition{ case new case used } final class BicycleViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("BicycleViewController: init(coder:) has not been implemented.") } } final class ScooterViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("ScooterViewController: init(coder:) has not been implemented.") } }
Таким образом, у нас есть семейство объектов одной группы, экземпляры типов которых создаются в одних и тех же местах в зависимости от какого-то условия (например, пользователь нажал на товар в списке, и в зависимости от того, самокат это или велосипед, мы создаем соответствующий контроллер). Конструкторы контроллеров имеют некоторые параметры, которые также необходимо каждый раз задавать. Не свидетельствуют ли эти два довода в пользу создания «фабрики», которая одна будет обладать знаниями о логике создания нужного контроллера?
Конечно, пример достаточно простой, и в реальном проекте в похожем случае вводить «фабрику» будет явным «overengineering». Тем не менее, если представить, что типов транспортных средств у нас не два, а параметров у конструкторов – не один, то преимущества «фабрики» станут более очевидными.
Итак, объявим интерфейс, который будет играть роль «абстрактной фабрики»:
protocol VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController func makeScooterViewController() -> UIViewController }
(Довольно краткий «гайдлайн» по проектированию «API» на языке «Swift» рекомендует называть «фабричные» методы начиная со слова «make».)
(Пример в книге банды четырех приведен на «C++» и основывается на наследовании и «виртуальных» функциях. Используя «Swift» нам, конечно, ближе парадигма протокольно-ориентированного программирования.)
Интерфейс абстрактной фабрики содержит всего два метода: для создания контроллеров для продажи велосипедов и самокатов. Методы возвращают экземпляры не конкретных подклассов, а общего базового класса. Таким образом, ограничивается область распространения знаний о конкретных типах пределами той области, в которой это действительно необходимо.
В качестве «конкретных фабрик» будем использовать две реализации интерфейса абстрактной фабрики:
struct NewVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .new) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .new) } } struct UsedVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .used) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .used) } }
В данном случае, как видно из кода, конкретные фабрики отвечают за транспортные средства разного состояния (новые и подержанные).
Создание нужного контроллера отныне будет выглядеть примерно так:
let factory: VehicleViewControllerFactory = NewVehicleViewControllerFactory() let vc = factory.makeBicycleViewController()
Инкапусляция классов с помощью фабрики
Теперь вкратце пробежимся по примерам использования, которые предлагает в своей книге Керивски.
Первый «кейс» связан с инкапсуляцией конкретных классов. Для примера возьмем те же контроллеры для отображения данных о транспортных средствах:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
Предположим, мы имеем дело с каким-либо отдельным модулем, например, подключаемой библиотекой. В этом случае объявленные выше классы остаются (по умолчанию)
internal, а в качестве публичного «API» библиотеки выступит фабрика, которая в своих методах возвращает базовые классы контроллеров, таким образом оставляя знания о конкретных подклассах внутри библиотеки:public struct VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController() } func makeScooterViewController() -> UIViewController { return ScooterViewController() } }
Перемещение знаний о создании объекта внутрь фабрики
Второй «кейс» описывает сложную инициализацию объекта, и Керивски, в качестве одного из путей упрощения кода и оберегания принципов инкапсуляции, предлагает ограничение распространения знаний о процессе инициализации пределами фабрики.
Предположим, мы захотели продавать заодно уж и автомобили. А это, несомненно, более сложная техника, обладающая бóльшим числом характеристик. Для примера ограничимся типом используемого топлива, типом трансмиссии и размером колесного диска:
enum Condition { case new case used } enum EngineType { case diesel case gas } struct Engine { let type: EngineType } enum TransmissionType { case automatic case manual } final class CarViewController: UIViewController { private let condition: Condition private let engine: Engine private let transmission: TransmissionType private let wheelDiameter: Int init(engine: Engine, transmission: TransmissionType, wheelDiameter: Int = 16, condition: Condition = .new) { self.engine = engine self.transmission = transmission self.wheelDiameter = wheelDiameter self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("CarViewController: init(coder:) has not been implemented.") } }
Пример инициализации соответствующего контроллера:
let engineType = EngineType.diesel let engine = Engine(type: engineType) let transmission = TransmissionType.automatic let wheelDiameter = 18 let vc = CarViewController(engine: engine, transmission: transmission, wheelDiameter: wheelDiameter)
Мы можем ответственность за все эти «мелочи» водрузить на «плечи» специализированной фабрики:
struct UsedCarViewControllerFactory { let engineType: EngineType let transmissionType: TransmissionType let wheelDiameter: Int func makeCarViewController() -> UIViewController { let engine = Engine(type: engineType) return CarViewController(engine: engine, transmission: transmissionType, wheelDiameter: wheelDiameter, condition: .used) } }
И создавать контроллер уже таким образом:
let factory = UsedCarViewControllerFactory(engineType: .gas, transmissionType: .manual, wheelDiameter: 17) let vc = factory.makeCarViewController()
Фабричный метод
Второй «однокоренной» шаблон также инкапсулирует знания о конкретных порождаемых типах, но не за счет сокрытия этих знаний внутри специализированного класса, а за счет полиморфизма. Керивски в своей книге приводит примеры на «Java» и предлагает пользоваться абстрактными классами, но обитатели вселенной «Swift» с таким понятием не знакомы. У нас тут своя атмосфера… и протоколы.
Книга «Банды четырех» сообщает, что шаблон также известен под названием «виртуальный конструктор», и это не зря. В «C++» виртуальной называется функция, переопределяемая в производных классах. Возможности объявить виртуальным конструктор язык не дает, и не исключено, что именно попытка сымитировать нужное поведение привела к изобретению данного паттерна.
Полиморфное создание объектов
В качестве классического примера пользы шаблона рассмотрим случай, когда в иерархии разные типы имеют идентичную реализацию одного метода за исключением объекта, который в этом методе создается и используется. В качестве решения предлагается создание этого объекта вынести в отдельный метод и реализовывать его отдельно, а общий метод – поднять выше в иерархии. Таким образом, разные типы будут использовать общую реализацию метода, а объект, необходимый для этого метода, будет создаваться полиморфно.
Для примера вернемся к нашим контроллерам для отображения транспортных средств:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
И предположим, что для их отображения используется некая сущность, например, координатор, который представляет эти контроллеры модально из другого контроллера:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() }
При этом метод
start() используется всегда одинаково, за исключением того, что в нем создаются разные контроллеры:final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = BicycleViewController() presentingViewController?.present(vc, animated: true) } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = ScooterViewController() presentingViewController?.present(vc, animated: true) } }
Предлагаемое решение – это вынести создание используемого объекта в отдельный метод:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() func makeViewController() -> UIViewController }
А основной метод – снабдить базовой реализацией:
extension Coordinator { func start() { let vc = makeViewController() presentingViewController?.present(vc, animated: true) } }
Конкретные типы в таком случае примут вид:
final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return BicycleViewController() } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return ScooterViewController() } }
Заключение
Я попытался данную несложную тему осветить, совместив три подхода:
- классическая декларация существования приема, навеянная книгой «Банды четырех»;
- мотивация использования, неприкрыто вдохновленная книгой Керивски;
- прикладное применение на примере близкой мне отрасли программирования.
При этом я попытался быть максимально близким хрестоматийной структуре шаблонов, насколько это возможно, не разрушая принципы современного подхода к разработке под систему «iOS» и используя возможности языка «Swift» (вместо более распространенных «С++» и «Java»).
Как оказалось, найти подробные материалы на тему, содержащие прикладные примеры довольно сложно. Большинство существующих статей и руководств содержат лишь поверхностные обзоры и сокращенные примеры, уже довольно урезанные по сравнению с хрестоматийными версиями реализаций.
Надеюсь, хотя бы отчасти мне удалось достичь поставленных целей, а читателю – хотя бы отчасти было интересно или хотя бы любопытно узнать или освежить свои знания по данной теме.
Другие мои материалы на тему шаблонов проектирования:
- «Архитектурный шаблон «Посетитель» (“Visitor”) во вселенной «iOS» и «Swift»
- «Архитектурный шаблон «Итератор» («Iterator») во вселенной «Swift»
- «Архитектурный шаблон «Строитель» во вселенной «Swift» и «iOS»/«macOS»
А это ссылка на мой «Twitter», где я публикую ссылки на свои очерки и немного сверх того.
