- PVSM.RU - https://www.pvsm.ru -

«Фабричный метод» и «Абстрактная фабрика» во вселенной «Swift» и «iOS»

Слово «фабрика» – безусловно одно из самых часто употребляемых программистами при обсуждении своих (или чужих) программ. Но смысл в него вкладываемый бывает очень разным: это может быть и класс, порождающий объекты (полиморфно или нет); и метод, создающий экземпляры какого-либо типа (статический или нет); бывает, и даже просто любой порождающий метод (включая, конструкторы [1]).

Конечно, не все, что угодно, порождающее экземпляры чего-либо, может называться словом «фабрика». Более того, под этим словом могут скрываться два разных порождающих шаблона из арсенала «Банды четырех» – «фабричный метод» [2] и «абстрактная фабрика» [3], в подробности которых я и хотел бы немного углубиться, уделяя особое внимание классическим их пониманию и реализации.

А на написание этого очерка меня вдохновил Джошуа Керивски [4] (глава «Industrial Logic» [5]), а точнее, его книга «Refactoring to Patterns» [6], которая вышла в начале века в рамках серии книг, основанной Мартином Фаулером [7] (именитым автором современной классики программирования – книги «Рефакторинг» [8]). Если кто-то не читал или даже не слышал о первой (а я знаю таких много), то обязательно добавьте ее себе в список для чтения. Это достойный «сиквел» как «Рефакторинга», так и еще более классической книги – «Приемов объектно-ориентированного проектирования. Паттерны проектирования» [9].

Книга, помимо прочего, содержит в себе несколько десятков рецептов избавления от различных «запахов» [10] в коде с помощью шаблонов проектирования [11]. В том числе и три (как минимум) «рецепта» на обсуждаемую тему.

Абстрактная фабрика

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

Первый – это инкапсуляция [12] знаний о конкретных классах, связанных общим интерфейсом. В таком случае этими знаниями будет обладать лишь тип, являющейся фабрикой. Публичный API [13] фабрики будет состоять из набора методов (статических или нет), возвращающих экземпляры типа общего интерфейса и имеющих какие-либо «говорящие» названия (чтобы понимать, какой метод необходимо вызвать для той или иной цели).

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

Чтобы быть ближе к теме разработки под «iOS» [14], удобно упражняться на подклассах UIViewController [15]. И действительно, это точно один из самых распространенных типов в «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» [16]. Тем не менее, если представить, что типов транспортных средств у нас не два, а параметров у конструкторов – не один, то преимущества «фабрики» станут более очевидными.

Итак, объявим интерфейс, который будет играть роль «абстрактной фабрики»:

protocol VehicleViewControllerFactory {
    func makeBicycleViewController() -> UIViewController
    func makeScooterViewController() -> UIViewController
}

(Довольно краткий «гайдлайн» по проектированию «API» [17] на языке «Swift» [18] рекомендует называть «фабричные» методы начиная со слова «make».)

(Пример в книге банды четырех приведен на «C++» [19] и основывается на наследовании [20] и «виртуальных» функциях [21]. Используя «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()

Инкапусляция классов с помощью фабрики

Теперь вкратце пробежимся по примерам использования, которые предлагает в своей книге Керивски.

Первый «кейс» связан с инкапсуляцией конкретных классов [22]. Для примера возьмем те же контроллеры для отображения данных о транспортных средствах:

final class BicycleViewController: UIViewController { }
final class ScooterViewController: UIViewController { }

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

public struct VehicleViewControllerFactory {
    
    func makeBicycleViewController() -> UIViewController {
        return BicycleViewController()
    }
    func makeScooterViewController() -> UIViewController {
        return ScooterViewController()
    }
    
}

Перемещение знаний о создании объекта внутрь фабрики

Второй «кейс» описывает сложную инициализацию объекта [24], и Керивски, в качестве одного из путей упрощения кода и оберегания принципов инкапсуляции, предлагает ограничение распространения знаний о процессе инициализации пределами фабрики.

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

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)
    }
    
}</source
И создавать контроллер уже таким образом:

<source lang="swift">let factory = UsedCarViewControllerFactory(engineType: .gas,
                                           transmissionType: .manual,
                                           wheelDiameter: 17)
let vc = factory.makeCarViewController()

Фабричный метод

Второй «однокоренной» шаблон также инкапсулирует знания о конкретных порождаемых типах, но не за счет сокрытия этих знаний внутри специализированного класса, а за счет полиморфизма. Керивски в своей книге приводит примеры на «Java» [25] и предлагает пользоваться абстрактными классами [26], но обитатели вселенной «Swift» с таким понятием не знакомы. У нас тут своя атмосфера… и протоколы.

Книга «Банды четырех» сообщает, что шаблон также известен под названием «виртуальный конструктор», и это не зря. В «C++» виртуальной называется функция, переопределяемая в производных классах. Возможности объявить виртуальным конструктор язык не дает, и не исключено, что именно попытка сымитировать нужное поведение привела к изобретению данного паттерна.

Полиморфное создание объектов

В качестве классического примера пользы шаблона рассмотрим случай, когда в иерархии разные типы имеют идентичную реализацию одного метода за исключением объекта, который в этом методе создается и используется [27]. В качестве решения предлагается создание этого объекта вынести в отдельный метод и реализовывать его отдельно, а общий метод – поднять выше в иерархии. Таким образом, разные типы будут использовать общую реализацию метода, а объект, необходимый для этого метода, будет создаваться полиморфно.

Для примера вернемся к нашим контроллерам для отображения транспортных средств:

final class BicycleViewController: UIViewController { }
final class ScooterViewController: UIViewController { }

И предположим, что для их отображения используется некая сущность, например, координатор [28], который представляет эти контроллеры модально из другого контроллера:

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»).

Как оказалось, найти подробные материалы на тему, содержащие прикладные примеры довольно сложно. Большинство существующих статей и руководств содержат лишь поверхностные обзоры и сокращенные примеры, уже довольно урезанные по сравнению с хрестоматийными версиями реализаций.

Надеюсь, хотя бы отчасти мне удалось достичь поставленных целей, а читателю – хотя бы отчасти было интересно или хотя бы любопытно узнать или освежить свои знания по данной теме.

Другие мои материалы на тему шаблонов проектирования:

А это ссылка на мой «Twitter», где я публикую ссылки на свои очерки и немного сверх того. [31]

Автор: Никита Лазарев-Зубов

Источник [32]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ios-development/317353

Ссылки в тексте:

[1] конструкторы: https://en.wikipedia.org/wiki/Constructor_(object-oriented_programming

[2] «фабричный метод»: https://en.wikipedia.org/wiki/Factory_method_pattern

[3] «абстрактная фабрика»: https://en.wikipedia.org/wiki/Abstract_factory_pattern

[4] Джошуа Керивски: https://twitter.com/joshuakerievsky

[5] «Industrial Logic»: https://industriallogic.com

[6] «Refactoring to Patterns»: https://amazon.com/Refactoring-Patterns-Joshua-Kerievsky/dp/0321213351

[7] Мартином Фаулером: https://martinfowler.com

[8] «Рефакторинг»: https://amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672

[9] «Приемов объектно-ориентированного проектирования. Паттерны проектирования»: https://amazon.com/Design-Patterns-Object-Oriented-Addison-Wesley-Professional-ebook/dp/B000SEIBB8

[10] «запахов»: https://en.wikipedia.org/wiki/Code_smell

[11] шаблонов проектирования: https://en.wikipedia.org/wiki/Software_design_pattern

[12] инкапсуляция: https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)

[13] API: https://en.wikipedia.org/wiki/Application_programming_interface

[14] «iOS»: https://apple.com/ru/ios/ios-12

[15] UIViewController: https://developer.apple.com/documentation/uikit/uiviewcontroller

[16] «overengineering»: https://en.wikipedia.org/wiki/Overengineering

[17] «гайдлайн» по проектированию «API»: https://swift.org/documentation/api-design-guidelines

[18] «Swift»: https://apple.com/swift

[19] «C++»: https://en.wikipedia.org/wiki/C%2B%2B

[20] наследовании: https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)

[21] «виртуальных» функциях: https://en.wikipedia.org/wiki/Virtual_function

[22] инкапсуляцией конкретных классов: https://industriallogic.com/xp/refactoring/classesWithFactory.html

[23] internal: https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html

[24] сложную инициализацию объекта: https://industriallogic.com/xp/refactoring/creationWithFactory.html

[25] «Java»: https://java.com

[26] абстрактными классами: https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html

[27] в иерархии разные типы имеют идентичную реализацию одного метода за исключением объекта, который в этом методе создается и используется: https://industriallogic.com/xp/refactoring/polymorphicCreationFactory.html

[28] координатор: https://habr.com/ru/post/444038/

[29] «Архитектурный шаблон «Посетитель» (“Visitor”) во вселенной «iOS» и «Swift»»: https://habr.com/ru/post/432558/

[30] «Архитектурный шаблон «Итератор» («Iterator») во вселенной «Swift»»: https://habr.com/ru/post/437614/

[31] А это ссылка на мой «Twitter», где я публикую ссылки на свои очерки и немного сверх того.: https://twitter.com/lazarevzubov

[32] Источник: https://habr.com/ru/post/451324/?utm_source=habrahabr&utm_medium=rss&utm_campaign=451324