Хрупкий базовый класс
![]() | Эта статья включает список общих ссылок , но в ней отсутствуют достаточные соответствующие встроенные цитаты . ( сентябрь 2009 г. ) |
Проблема хрупкого базового класса — это фундаментальная архитектурная проблема систем объектно-ориентированного программирования , в которых базовые классы ( суперклассы ) считаются «хрупкими», поскольку кажущиеся безопасными модификации базового класса при наследовании производными классами могут привести к сбоям в работе производных классов. . Программист не может определить, безопасно ли изменение базового класса, просто исследуя методы базового класса по отдельности.
Одним из возможных решений является сделать переменные экземпляра частными для их определяющего класса и заставить подклассы использовать методы доступа для изменения состояний суперкласса. Язык также может сделать так, чтобы подклассы могли контролировать, какие унаследованные методы будут доступны публично. Эти изменения не позволяют подклассам полагаться на детали реализации суперклассов и позволяют подклассам предоставлять только те методы суперкласса, которые применимы к ним самим.
Альтернативное решение — использовать интерфейс вместо суперкласса.
В проблеме хрупкого базового класса обвиняют открытую рекурсию (динамическую отправку методов на this
), с предположением, что вызов методов на this
по умолчанию используется закрытая рекурсия (статическая диспетчеризация, раннее связывание), а не открытая рекурсия (динамическая диспетчеризация, позднее связывание), открытая рекурсия используется только тогда, когда это специально запрошено; внешние вызовы (без использования this
) будет динамически отправляться, как обычно. [1] [2]
Пример Java
[ редактировать ]Следующий тривиальный пример написан на языке программирования Java и показывает, как, казалось бы, безопасная модификация базового класса может привести к сбою в работе наследующего подкласса из-за входа в бесконечную рекурсию , что приведет к переполнению стека .
class Super {
private int counter = 0;
void inc1() {
counter++;
}
void inc2() {
counter++;
}
}
class Sub extends Super {
@Override
void inc2() {
inc1();
}
}
Вызов динамически связанного метода inc2() в экземпляре Sub правильно увеличит счетчик поля на единицу. Однако если код суперкласса изменить следующим образом:
class Super {
private int counter = 0;
void inc1() {
inc2();
}
void inc2() {
counter++;
}
}
вызов динамически связанного метода inc2() в экземпляре Sub вызовет бесконечную рекурсию между ним и методом inc1() суперкласса и в конечном итоге вызовет переполнение стека. Этой проблемы можно было бы избежать, объявив методы суперкласса как Final , что сделало бы невозможным их переопределение подклассом. Однако это не всегда желательно и возможно. Поэтому суперклассам рекомендуется избегать изменения вызовов динамически привязанных методов.
Решения
[ редактировать ]- В Objective-C есть категории, а также нехрупкие переменные экземпляра .
- Компонентный Паскаль не поддерживает вызовы суперклассов .
- Java , C++ (начиная с C++11) и D позволяют запретить наследование или переопределение метода класса, пометив объявление класса или метода соответственно ключевым словом "
final
В книге «Эффективная Java » автор Джошуа Блох пишет (в пункте 17), что программисты должны «Проектировать и документировать наследование или запретить его». - В C# и VB.NET, как и в Java, есть "
sealed
" и "Not Inheritable
" Ключевые слова объявления класса, чтобы запретить наследование и потребовать, чтобы подкласс использовал ключевое слово "override
"о переопределении методов, [3] то же решение позже было принято в Scala. - Scala требует, чтобы подкласс использовал ключевое слово "
override
" явно для того, чтобы переопределить метод родительского класса. В книге "Программирование на Scala, 2-е издание" автор пишет, что (с изменениями здесь) Если не было метода f(), исходная клиентская реализация метода f() не могло быть модификатора переопределения. Как только вы добавите метод f() во вторую версию класса вашей библиотеки, перекомпиляция клиентского кода приведет к ошибке компиляции вместо неправильного поведения. - В Котлине классы и методы по умолчанию являются окончательными. Чтобы включить наследование классов, класс должен быть помечен значком
open
модификатор. Аналогично, метод должен быть помечен какopen
чтобы разрешить переопределение метода. - Julia допускает только подтипирование абстрактных типов и использует композицию в качестве альтернативы наследованию . Однако он имеет несколько отправок .
См. также
[ редактировать ]- Хрупкая проблема двоичного интерфейса
- Наследование реализации
- Семантика наследования
- Хрупкость программного обеспечения
- Виртуальное наследование
Ссылки
[ редактировать ]Внешние ссылки
[ редактировать ]- Михайлов, Леонид; Секерински, Эмиль (1998). «ECOOP'98 — Объектно-ориентированное программирование» (PDF) . ЭКООП'98 — Объектно-ориентированное программирование . ЭКООП 1998. LCNS . Том. 1445. стр. 355–382. дои : 10.1007/BFb0054099 . ISBN 978-3-540-64737-9 . ISSN 0302-9743 . QID 29543920 . Проверено 21 июля 2020 г.
- Голуб, Аллен (1 августа 2003 г.). «Почему расширяется – это зло» . Панель инструментов Java. JavaWorld . Проверено 21 июля 2020 г.