Ромбовидное наследование


Ромбовидное наследование (англ. diamond inheritance) — ситуация в объектно-ориентированных языках программирования с поддержкой множественного наследования, когда два класса B и C наследуют от A, а класс D наследует от обоих классов B и C. При этой схеме наследования может возникнуть неоднозначность: если объект класса D вызывает метод, определенный в классе A (и этот метод не был переопределен в классе D), а классы B и C по-своему переопределили этот метод, то от какого класса его наследовать: B или C?

Например, в области разработки графических интерфейсов класс Button («Кнопка») может одновременно наследовать от класса Rectangle («Прямоугольник», для внешнего вида) и от класса Clickable («Доступен для кликанья мышкой», для реализации функциональности/обработки ввода), а Rectangle и Clickable наследуют от класса Object («Объект»). Если вызвать метод equals («Равно») для объекта Button, и в классе Button не окажется такого метода, но в классе Object будет присутствовать метод equals по-своему переопределенный как в классе Rectangle, так и в Clickable, то какой из методов должен быть вызван?

Проблема ромба (англ. diamond problem) получила своё название благодаря очертаниям диаграммы наследования классов в этой ситуации. В данной статье, класс A обозначается в виде вершины, классы B и C по отдельности указываются ниже, а D соединяется с обоими в самом низу, образуя ромб.

Языки, допускающие лишь простое наследование (как например, Ада, Objective-C, PHP, C#, Delphi/Free Pascal и Java), предусматривают множественное наследование интерфейсов (в Objective-C называемых протоколами). Интерфейсы по сути являются абстрактными базовыми классами, все методы которых также абстрактны, и где отсутствуют поля. Таким образом, проблема не возникает, так как всегда будет только одна реализация определенного метода или свойства, не допуская возникновения неопределенности.

Проблема ромба не ограничивается лишь наследованием. Она также возникает в таких языках, как Си и C++, когда заголовочные файлы A, B, C и D, а также отдельные предкомпилированные заголовки, созданные из B и C, подключаются (при помощи инструкции #include) один к другому по ромбовидной схеме, указанной вверху. Если эти два предкомпилированных заголовка объединяются, объявления в A дублируются, и директива защиты подключения #ifndef становится неэффективной. Также проблема обнаруживается при объединении стеков подпрограммного обеспечения; например, если A — это база данных, а B и C — кэши, то D может запросить как B, так и C подтвердить (COMMIT) выполнение транзакции, приводя к дублирующим вызовам подтверждений A.