Ключевое противоречие ООП
Как известно, классическое ООП покоится на трех китах
Классическая же реализация по умолчанию:
- Инкапсуляция — публичные и приватные члены класса
- Наследование — реализация функционала за счет расширения одного класса-предка, защищенные члены класса.
- Полиморфизм — виртуальные методы класса-предка.
Но еще в 1986 году была обозначена серьезнейшая проблема, кратко формулируемая так:
Наследование ломает инкапсуляцию
- Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома —
антипаттерн Паблик Морозов - Реально изменить поведение предка можно только с помощью перекрытия виртуальных методов.
- Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку.
- Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода
- Информация из пункта 4 зависит от реализации класса-предка, включая приватные члены и их код.
В теории мы уже имеем былинный отказ, но как насчет практики?
- Зависимость, создаваемая наследованием, чрезвычайно сильна.
- Наследники гиперчувствительны к любым изменениям предка.
- Наследование от чужого кода добавляет адскую боль при сопровождении:
разработчики библиотеки рискуют получить обструкцию из-за поломанной обратной совместимости при малейшем изменении базового класса, а прикладники — регрессию при любом обновлении используемых библиотек.
Все, кто используют фреймворки, требующие наследования от своих классов (WinForms, WPF, WebForms, ASP.NET), легко найдут подтверждения всем трем пунктам в своем опыте.
Неужели все так плохо?
Теоретическое решение
Влияние проблемы можно ослабить принятием некоторых конвенций
- Защищенные члены не нужны.
Это соглашение ликвидирует пабликов морозовых как класс. - Виртуальные методы предка ничего не делают.
Это соглашение позволяет сочетать знание о реализации предка с независимостью от нее реализации уже в потомке. - Виртуальные методы предка никогда не вызываются в его коде.
Это соглашение позволяет потомкам не зависеть от внутренней реализации предка, а также требует публичности всех виртуальных методов. - Экземпляры предка никогда не создаются
Это соглашение позволяет избавиться от несоответствия требований к виртуальными методам (публичный контракт класса) с одной стороны и обязанностью ничего не делать (защищенный контракт класса) с другой. Теперь принцип подстановки Лисков можно соблюсти, не вступая в порочную связь с закрытым содержимым предка. - Невиртуальных членов у предка нет
С учетом предыдущих соглашений невиртуальные члены предка становятся бесполезными и подлежат ликвидации.
Результат: если класс-предок состоит из публичных виртуальных пустых методов и требований к ним для потомков, то наследование уже не ломает инкапсуляцию. Что и требовалось доказать.
Попутно получаем возможность решение проблемы ромба для случая множественного наследования от конвенционных предков.
Но это все теория, а нам нужны...
Практические решения
- Виртуальные методы-пустышки уже есть во многих языках и носят гордое звание абстрактных.
- Классы, экземпляры которых создавать нельзя, тоже есть во многих языках и даже имеют то же звание.
- Полное соблюдение указанных соглашений в языке C++ использовалось как паттерн для проектирования и реализации Component Object Model.
- Ну и самое приятное: в C# и многих других языках соглашения реализованы как первоклассный элемент "интерфейс".
Происхождение названия очевидно — в результате соблюдения соглашений от класса остается только его публичный интерфейс. И если множественное наследование от обычных классов — редкость, то от интерфейсов оно доступно без всяких ограничений.
Итоги
- Языки, где нет наследования от классов, но есть — от интерфейсов (например, Go), нельзя лишать звания объектно-ориентированных. Более того, такая реализация ООП правильнее теоретически и безопаснее практически.
- Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.
- Избегайте наследования реализаций без крайней необходимости.
- Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
- Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.
PS: Дополнения и критика традиционно приветствуются.
Автор: Bonart