Pull to refresh

Наследование реализаций: закопайте стюардессу

Reading time 3 min
Views 47K

Ключевое противоречие ООП


Как известно, классическое ООП покоится на трех китах:


  1. Инкапсуляция
  2. Наследование
  3. Полиморфизм

Классическая же реализация по умолчанию:


  1. Инкапсуляция — публичные и приватные члены класса
  2. Наследование — реализация функционала за счет расширения одного класса-предка, защищенные члены класса.
  3. Полиморфизм — виртуальные методы класса-предка.

Но еще в 1986 году была обозначена серьезнейшая проблема, кратко формулируемая так:


Наследование ломает инкапсуляцию


  1. Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома — антипаттерн Паблик Морозов;
  2. Реально изменить поведение предка можно только с помощью перекрытия виртуальных методов;
  3. Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку;
  4. Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода;
  5. Информация из пункта 4 зависит от реализации класса-предка, включая приватные члены и их код.

В теории мы уже имеем былинный отказ, но как насчет практики?


  1. Зависимость, создаваемая наследованием, чрезвычайно сильна;
  2. Наследники гиперчувствительны к любым изменениям предка;
  3. Наследование от чужого кода добавляет адскую боль при сопровождении: разработчики библиотеки рискуют получить обструкцию из-за поломанной обратной совместимости при малейшем изменении базового класса, а прикладники — регрессию при любом обновлении используемых библиотек.

Все, кто используют фреймворки, требующие наследования от своих классов (WinForms, WPF, WebForms, ASP.NET), легко найдут подтверждения всем трем пунктам в своем опыте.
Неужели все так плохо?


Теоретическое решение


Влияние проблемы можно ослабить принятием некоторых конвенций:


1. Защищенные члены не нужны
Это соглашение ликвидирует пабликов морозовых как класс.


2. Виртуальные методы предка ничего не делают
Это соглашение позволяет сочетать знание о реализации предка с независимостью от нее реализации уже в потомке.


3. Виртуальные методы предка никогда не вызываются в его коде
Это соглашение позволяет потомкам не зависеть от внутренней реализации предка, а также требует публичности всех виртуальных методов.


4. Экземпляры предка никогда не создаются
Это соглашение позволяет избавиться от несоответствия требований к виртуальными методам (публичный контракт класса) с одной стороны и обязанностью ничего не делать (защищенный контракт класса) с другой. Теперь принцип подстановки Лисков можно соблюсти, не вступая в порочную связь с закрытым содержимым предка.


5. Невиртуальных членов у предка нет
С учетом предыдущих соглашений невиртуальные члены предка становятся бесполезными и подлежат ликвидации.


Результат: если класс-предок состоит из публичных виртуальных пустых методов и требований к ним для потомков, то наследование уже не ломает инкапсуляцию. Что и требовалось доказать.


Попутно получаем возможность решение проблемы ромба для случая множественного наследования от конвенционных предков. Но это все теория, а нам нужны...


Практические решения


  1. Виртуальные методы-пустышки уже есть во многих языках и носят гордое звание абстрактных.
  2. Классы, экземпляры которых создавать нельзя, тоже есть во многих языках и даже имеют то же звание.
  3. Полное соблюдение указанных соглашений в языке C++ использовалось как паттерн для проектирования и реализации Component Object Model.
  4. Ну и самое приятное: в C# и многих других языках соглашения реализованы как первоклассный элемент "интерфейс".
    Происхождение названия очевидно — в результате соблюдения соглашений от класса остается только его публичный интерфейс. И если множественное наследование от обычных классов — редкость, то от интерфейсов оно доступно без всяких ограничений.

Итоги


  1. Языки, где нет наследования от классов, но есть — от интерфейсов (например, Go), нельзя лишать звания объектно-ориентированных. Более того, такая реализация ООП правильнее теоретически и безопаснее практически.
  2. Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.
  3. Избегайте наследования реализаций без крайней необходимости.
  4. Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
  5. Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.

PS: Дополнения и критика традиционно приветствуются.

Tags:
Hubs:
+33
Comments 349
Comments Comments 349

Articles