Композиция > наследование
У меня давно назревала подобная статья, еще примерно с момента осознания того, почему синглтон это плохо (может когда-нибудь будет статья и на эту тему).
Для умников в комментах: глобальный мутабельный стейт – это плохо :)
Поэтому, тут я постараюсь осветить подробно (и с примерами), как можно использовать композицию в ООП вместо наследования, почему наследование это плохо и тому подобные нюансы крутого проггинга 😎.
Полиморфизм в Swift
По сути, обе концепции (и композиция и наследование) позволяют достичь полиморфизма. Но для начала, давайте вспомним что такое полиморфизм. Существует множество различных видов полиморфизмов, что прекрасно описано, например, на википедии. Я же сосредоточусь скорее на том, что будет являться наиболее важным в контексте Swift.
Цель полиморфизма состоит в том, чтобы избежать избыточности кода, например дублирования функций, которые принимают разные типы параметров, но делают одно и то же, что делает возможным повторное использование кода.
Рассмотрим на примере:
Логика следующая. Допустим есть:
Player
- которыйSolid
,Movable
иVisible
Cloud
- которыйMovable
иVisible
, но неSolid
Building
- которыйSolid
иVisible
, но неMovable
Trap
- которыйSolid
, но неVisible
и неMovable
И тут мы действительно попадаем в ловушку. В каком-нибудь с++, например, не запрещено наследоваться от множества классов. В Swift множественное наследование классов запрещено (что в целом достаточно логично, если вспомнить о том, что класс – это reference type).
Подобное множественное наследование довольно опасно использовать без должного понимания сути проблемы, и обычно оно приводит к тому, что в англоязычном мире зовут the diamond problem. Кроме того, даже с идеальным пониманием механизмов наследования, данный код получится, откровенно говоря, довольно дерьмовым. Как же быть? В Swift данная проблема решается с помощью механизма протоколов и расширений.
Рассмотрим на примере Visible
:
Ну протоколы протоколами дядя, но как это решает проблему в конечном итоге? Да и к тому же, разве это не чёртова прорва кода? Да, возможно кода стало немного больше, но поверьте мне, это позволит избежать огромных проблем в дальнейшем.
Логика здесь четко разнесена, есть Player
, который Visible
, Solid
и Movable
. Всё это интерфейсы, протокол содержит только описательную часть, а сама реализация протокола подвешивается в extension
. И в таком случае достаточно написать extension
к Visible
, содержащий данную конкретную реализацию метода draw
. В случае, когда нам понадобится логика Invisible
, мы просто добавим этот протокол к конечной структуре/классу. Все просто!
В случае с Movable
, например, всё делается абсолютно аналогично:
Далее создаемBuilding
и добавляем в него соответствие протоколуIrremovable
. И все. Никаких тебе the diamond problem.
В с++ данная проблема решается немного иначе, с помощью virtual inheritance. По сути, если не вдаваться в детали, аналогом протоколов там являются pure virtual functions. Хотя протокол это конечно скорее элемент интерфейса, и там ничего виртуального нет.
Player
в данном случае будет выглядеть так:
Swift не запрещает классу/структуре/перечислению/другому протоколу соответствовать сразу нескольким протоколам. Протоколы, на самом деле хороши тем, что позволяют настраивать гибкое поведение, не наследуя разнообразные болячки от родителей.
Выводы
При проектировке архитектуры конечного приложения стоит отдавать свое предпочтение композиции, нежели наследованию, поскольку первая более гибкая и легко модифицируется в дальнейшем. Хотя, разумеется, не стоит использовать данный подход всегда и везде.
С помощью композиции легко изменить поведение на лету с помощью dependency injection. В свою очередь, наследование это более "жесткий" подход, поскольку большинство (нормальных) языков не позволяют наследовать более одного типа.
Кстати, принцип "композиция вместо наследования" (или принцип "составного повторного использования") в ООП – это принцип, согласно которому классы должны достигать полиморфного поведения и повторного использования кода посредством их композиции (путем включения экземпляров других классов, реализующих желаемую функциональность), а не путём наследования от базового класса. Это часто упоминаемый принцип ООП, например, в книге Design Patterns (1994).
Кстати а вообще причем тут синглтон? Вместо синглтона лучше использовать dependency injection. Используя композицию вместо наследования, гораздо проще менять поведение "на лету", используя например всё тот же dependency injection.
Полезные ссылки:
Inheritance vs Composition (Swift)
Composition over Inheritance (с примерами на с++)
Статья подготовлена для канала Hello World.