Может ли Алмазная проблема действительно быть решена? - PullRequest
14 голосов
/ 18 февраля 2009

Типичная проблема в ОО-программировании - проблема алмазов. У меня есть родительский класс A с двумя подклассами B и C. A имеет абстрактный метод, B и C реализуют его. Теперь у меня есть подкласс D, который наследует B и C. Теперь проблема с алмазом в том, какую реализацию использует D, одну из B или одну из C?

Люди утверждают, что в Java нет проблем с бриллиантами. У меня может быть только множественное наследование с интерфейсами, и поскольку у них нет реализации, у меня нет проблем с алмазом. Это правда? Я так не думаю. См. Ниже:

[пример снятого автомобиля]

Всегда ли проблема с бриллиантами является причиной плохого проектирования классов, и ее не нужно решать ни программисту, ни компилятору, потому что она вообще не должна существовать?


Обновление: возможно, мой пример был неудачно выбран.

Посмотри это изображение

Diamond Problem
(источник: suffolk.edu )

Конечно, вы можете сделать Person виртуальным в C ++, и, таким образом, у вас будет только один экземпляр person в памяти, но реальная проблема сохраняется IMHO. Как бы вы реализовали getDepartment () для GradTeachingFellow? Подумайте, он может быть студентом на одном факультете и преподавать на другом. Таким образом, вы можете вернуть либо один отдел, либо другой; не существует идеального решения проблемы, и тот факт, что ни одна реализация не может быть унаследована (например, ученик и учитель могут быть интерфейсами), кажется, не решает проблему для меня.

Ответы [ 17 ]

17 голосов
/ 18 февраля 2009

То, что вы видите, это то, как нарушения принципа замены Лискова действительно затрудняют создание работающей, логичной объектно-ориентированной структуры.
По сути, (публичное) наследование должно только сужать цель класса, а не расширять его. В этом случае, наследуя от двух типов транспортных средств, вы фактически расширяете цель, и, как вы заметили, это не работает - движение для водного транспортного средства должно сильно отличаться от дорожного транспортного средства.
Вместо этого вы можете объединить водный транспорт и объект наземного транспортного средства в своем автомобиле-амфибии и принять внешнее решение, какое из двух будет соответствовать текущей ситуации.
В качестве альтернативы вы можете решить, что класс «vehicle» является излишне родовым, и у вас будут отдельные интерфейсы для обоих. Однако это не решает проблему для вашего автомобиля-амфибии - если вы называете метод перемещения «перемещение» в обоих интерфейсах, у вас все равно будут проблемы. Поэтому я бы предложил агрегацию вместо наследования.

6 голосов
/ 18 февраля 2009

C# имеет явную реализацию интерфейса , чтобы частично разобраться с этим. По крайней мере, в случае, если у вас есть один из промежуточных интерфейсов (его объект ..)

Однако, вероятно, происходит то, что объект AmphibianVehicle знает, находится ли он в данный момент на воде или на земле, и делает правильные вещи.

6 голосов
/ 18 февраля 2009

В вашем примере move() относится к интерфейсу Vehicle и определяет контракт "переход от точки A к точке B".

Когда GroundVehicle и WaterVehicle расширяются Vehicle, они неявно наследуют этот контракт (аналогия: List.contains наследует свой контракт от Collection.contains - представьте, если он указал что-то другое!).

Таким образом, когда бетон AmphibianVehicle реализует move(), контракт, который он действительно должен соблюдать, это Vehicle. Есть бриллиант, но контракт не меняется, рассматриваете ли вы одну сторону бриллианта или другую (или я бы назвал это проблемой дизайна).

Если вам нужен контракт «перемещения» для воплощения понятия поверхности, не определяйте его в типе, который не моделирует это понятие:

public interface GroundVehicle extends Vehicle {
    void ride();
}
public interface WaterVehicle extends Vehicle {
    void sail();
}

(аналогия: контракт get(int) определяется интерфейсом List. Он не может быть определен Collection, поскольку коллекции не обязательно упорядочены)

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

public interface Vehicle {
    void move(Surface s) throws UnsupportedSurfaceException;
}

Единственная проблема, которую я вижу при реализации нескольких интерфейсов, - это когда два метода из совершенно не связанных между собой интерфейсов сталкиваются:

public interface Vehicle {
    void move();
}
public interface GraphicalComponent {
    void move(); // move the graphical component on a screen
}
// Used in a graphical program to manage a fleet of vehicles:
public class Car implements Vehicle, GraphicalComponent {
    void move() {
        // ???
    }
}

Но тогда это не был бы бриллиант. Больше похоже на перевернутый треугольник.

5 голосов
/ 18 февраля 2009

Люди утверждают, что в Java нет проблем с бриллиантами. У меня может быть только множественное наследование с интерфейсами, и поскольку у них нет реализации, у меня нет проблем с алмазом. Это правда?

да, потому что вы управляете реализацией интерфейса в D. Сигнатура метода одинакова для обоих интерфейсов (B / C), и, поскольку интерфейсы не имеют реализации - нет проблем.

4 голосов
/ 18 февраля 2009

Нет проблем с бриллиантами при наследовании на основе интерфейса.

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

С наследованием на основе интерфейса есть только одна реализация метода, поэтому нет никакой двусмысленности.

РЕДАКТИРОВАТЬ: На самом деле то же самое будет применяться к наследованию на основе классов для методов, объявленных в суперклассе как Abstract.

4 голосов
/ 18 февраля 2009

Я не знаю Java, но если интерфейсы B и C наследуются от интерфейса A, а класс D реализует интерфейсы B и C, то класс D просто реализует метод move один раз, и это A.Move, что он должен реализовать , Как вы говорите, с этим у компилятора проблем нет.

Из приведенного вами примера относительно AmphibianVehicle, реализующего GroundVehicle и WaterVehicle, это можно легко решить, сохранив, например, ссылку на Environment и выставив свойство Surface для Environment, которое будет проверять метод Move AmphibianVehicle. Нет необходимости передавать это как параметр.

Вы правы в том смысле, что программист должен что-то решать, но, по крайней мере, он компилируется и не должен быть «проблемой».

3 голосов
/ 18 февраля 2009

Если я знаю, у вас есть автомобиль-амфибия интерфейс, который наследует от Наземный и водный транспорт, как я бы реализовал его метод move ()?

Вы бы предоставили реализацию, подходящую для AmphibianVehicle с.

Если GroundVehicle движется «по-другому» (т.е. принимает параметры, отличные от WaterVehicle), то AmphibianVehicle наследует два разных метода, один для воды, другой для земли. Если это невозможно, то AmphibianVehicle не должен наследоваться от GroundVehicle и WaterVehicle.

Всегда ли проблема с алмазами плохого дизайна класса и что-то не нужен ни программист, ни компилятор решить, потому что он не должен существовать на первом месте?

Если это связано с плохим дизайном класса, это должен решить программист, так как компилятор не знает, как.

2 голосов
/ 27 апреля 2010

Проблема, которую вы видите в примере Ученик / Учитель, заключается просто в том, что ваша модель данных неверна или, по крайней мере, недостаточна.

Классы ученика и учителя объединяют два разных понятия «отдел», используя одно и то же имя для каждого из них. Если вы хотите использовать этот тип наследования, вам следует вместо этого определить что-то вроде «getTeachingDepartment» в Teacher и «getResearchDepartment» в Student. Ваш GradStudent, который является одновременно учителем и учеником, реализует оба варианта.

Конечно, учитывая реалии аспирантуры, даже этой модели, вероятно, недостаточно.

1 голос
/ 18 февраля 2009

Если у move () есть семантические различия, основанные на том, что это Ground или Water (а не GroundVehicle и WaterVehicle, оба расширяют интерфейс GeneralVehicle, имеющий подпись move ()), но ожидается, что вы будете смешивать и сочетать Ground и Water реализаторы, то ваш пример один действительно один из плохо разработанных API.

Реальная проблема заключается в том, что столкновение имен, по сути, случайно. например ( очень синтетический):

interface Destructible
{
    void Wear();
    void Rip();
}

interface Garment
{
    void Wear();
    void Disrobe();
}

Если у вас есть куртка, которую вы хотите носить как одежду, так и разрушаемую, у вас произойдет столкновение с именем в методе износа (с законным именем).

Java не имеет решения для этого (то же самое верно для нескольких других статически типизированных языков). У языков динамического программирования будет похожая проблема, даже без алмаза или наследования, это просто коллизия имен (потенциальная проблема, связанная с Duck Typing).

.Net имеет концепцию явных реализаций интерфейса , согласно которой класс может определять два метода с одинаковыми именами и сигнатурами, если оба они помечены для двух разных интерфейсов. Определение соответствующего метода для вызова основывается на известном интерфейсе переменной времени компиляции (или, если это отражено явным выбором вызываемого объекта)

Это разумное, вероятное столкновение имен так трудно найти, и что java не был опущен как непригодный из-за отсутствия явных реализаций интерфейса, что позволяет предположить, что проблема не является существенной для реального использования.

1 голос
/ 18 февраля 2009

В этом случае, вероятно, было бы наиболее выгодно иметь AmphibiousVehicle в качестве подкласса Vehicle (родственного элемента WaterVehicle и LandVehicle), чтобы полностью избежать проблемы в первую очередь. В любом случае, это было бы более правильно, поскольку амфибия - это не водный или наземный транспорт, это совсем другое.

...