В прошлой статье я использовал принцип ООП предпочтение композиции перед наследованием как одну из причин отказа от использования реализаций протокола по умолчанию в 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 для модульного тестирования.

Спасибо за чтение! Если статья понравилась, пожалуйста, аплодируйте :)