В прошлой статье я использовал принцип ООП предпочтение композиции перед наследованием как одну из причин отказа от использования реализаций протокола по умолчанию в Swift. Я не хотел вдаваться в подробности, потому что это заняло бы слишком много времени.
В этой статье я собираюсь использовать знакомый пример, чтобы показать, как мы используем композицию вместо наследования и почему, когда целью является повторное использование кода, это лучший подход.
BaseViewController
Одно из самых популярных применений наследования для повторного использования кода, которое я когда-либо встречал в проекте iOS, - это наличие BaseViewController
. Каждый UIViewController
в проекте является подклассом этого BaseViewController
, поэтому они получают общие поведения, функции, компоненты, зависимости и т. Д.
Что-то похожее на это:
Хорошо, так в чем проблема?
От BaseViewController к BaseViewContainer
BaseViewController
быстро становится контейнером для методов, которые используются (или будут потенциально использоваться) двумя или более подклассами. Это сложно поддерживать, и это нарушает принцип единой ответственности SOLID.
Каждый модуль или класс должен нести ответственность за отдельную часть функциональности, предоставляемой программным обеспечением.
Связь
BaseViewController
свойства и методы / функции не обязательно используются каждым UIViewController
подклассом. Это означает, что подклассы, вероятно, зависят от кода, который им не нужен или не используется. Вы можете увидеть пример в функциональности1 и функциональности2.
Согласно принципам ООП: Вы не должны зависеть от методов, которые не используете.
С помощью композиции вы можете использовать инверсию управления и разделение интерфейсов, определяя и согласовывая небольшие протоколы. Однако наследование не позволяет делать то же самое.
Инкапсуляция
Инкапсуляция относится к идее, что объекты должны управлять своим собственным поведением и состоянием, чтобы их соавторы не беспокоились о внутренней работе объекта.
Есть несколько способов, которыми BaseViewController
нарушает инкапсуляцию:
BaseViewController
↔︎ Подклассы (I): Если какому-либоBaseViewController
подклассу разрешен доступ к членам, унаследованным от него, изменения вBaseViewController
могут также потребовать обслуживания подклассов.BaseViewController
↔︎ Подклассы (II): Если какому-либо подклассуBaseViewController
разрешено переопределять унаследованные от него члены (пока они не являются окончательными), переопределение методовBaseViewController
может изменить его поведение.ChildViewControllers
↔︎ Клиенты: поскольку Swift не предоставляет защищенный аксессор для подклассов, как Java,BaseViewController
общие методы и свойства должны быть общедоступными или внутренними. Подклассы наследуют эти общедоступные / внутренние члены с одинаковой видимостью. Предполагая, что некоторые из этих членов должны быть частными, любой клиент имеет открытый доступ к своей внутренней работе.
Гибкость
Во-первых, BaseViewController
методы / функции не могут быть изменены или внедрены во время выполнения, как это можно сделать с помощью композиции. Это потому, что UIViewControllers
зависят от конкретных реализаций, а не от абстракций.
И, во-вторых, эти методы / функции нельзя повторно использовать в классе, отличном от UIViewController
, если только вы не хотите использовать UIViewController
в качестве зависимости для этого 🤯.
Модульное тестирование
Предполагая, что вы пишете тесты для своего кода, вы можете внедрить и имитировать все зависимости для модульного тестирования. На этом этапе должно быть довольно ясно, что создание подкласса BaseViewController
не позволит вам сделать это с унаследованными методами / функциями.
Общий заголовок, нижний колонтитул или фон
В этой статье рассматриваются некоторые причины, по которым вы должны отдавать предпочтение композиции перед наследованием, когда дело доходит до совместного использования кода.
Но вы можете подумать, что все еще можете использовать BaseViewController
только для установки некоторого общего заголовка, нижнего колонтитула или фона в нескольких UIViewController
без дублирования кода.
Я думаю, что вы в конечном итоге столкнетесь с некоторыми из предыдущих проблем из-за чего-то, чего вы могли бы достичь с помощью другого подхода.
Если вы используете верхние и нижние колонтитулы или фон из разных UIViewController
, вы можете подумать об использовании ChildViewControllers, чтобы отделить контейнер от фактического UIViewController
. Таким образом, каждый компонент несет свою ответственность, и, кроме того, ваш UIViewController
может быть введен в разные контейнеры.
Семантика наследования
Я оставил эту тему на потом, потому что она скорее концептуальная, чем практическая, и может быть спорной. Поэтому, пожалуйста, примите это как дополнительную и субъективную причину.
Наследование классов или, если хотите, подтипов, в данном случае - это механизм, который устанавливает связь is-a между двумя классами.
Независимо от правильности и полноты этой диаграммы, здесь мы видим, что каждый Cat
- это Mammal
, а каждый Mammal
- это VertebrateAnimal
. В этой таксономии каждый класс определяет свойства и методы, которые принадлежат концепции, которую он представляет.
В случае с BaseViewController
, учитывая, что он, вероятно, не будет использоваться как реальный UIViewController
, мы не можем сказать, что у нас есть отношения. BaseViewController
никогда не будет создан, и он, вероятно, переопределит init
методы на fatalErrors
для этой цели.
Если BaseViewController
не UIViewController
, мы можем нарушить семантику наследования здесь. BaseViewController
это не UIViewController
. И мы делаем это только ради повторного использования кода.
Swift не обеспечивает множественное наследование, поэтому на этом этапе вы можете подумать о решении этой проблемы, перейдя от подкласса к соответствию BaseViewControllerProtocol
с реализациями по умолчанию, но это может быть еще хуже, поскольку вы можете прочитать здесь.
Давайте использовать композицию!
Давайте посмотрим, как мы могли бы переписать первый пример, используя композицию:
- Здесь мы вводим
Functionality1
иFunctionality2
только в те классы, где они нам нужны. - Мы зависим только от тех методов, которые нам нужны или которые мы используем.
- Мы можем изменить реализацию во время выполнения, внедрив любой класс, соответствующий _42 _ / _ 43_.
Functionality1Protocol
иFunctionality2Protocol
могут соответствовать двум классам, а также одному классу.- Мы инкапсулируем зависимости.
- Мы можем внедрять макеты, соответствующие
Functionality1Protocol
иFunctionality2Protocol
для модульного тестирования.
Спасибо за чтение! Если статья понравилась, пожалуйста, аплодируйте :)