Может ли проблема алмазов быть действительно решена?
Типичная проблема программирования OO - проблема алмаза. У меня есть родительский класс A с двумя подклассами B и C. A имеет абстрактный метод, B и C реализуют его. Теперь у меня есть подкласс D, который наследует B и C. Теперь проблема с алмазом, какая реализация должна использовать D, одна из B или одна из C?
Люди утверждают, что Java не знает проблемы с алмазами. Я могу иметь только множественное наследование с интерфейсами, и поскольку у них нет реализации, у меня нет проблемы с алмазами. Это правда? Я так не думаю. См. ниже:
[снятый пример транспортного средства]
Является ли проблема с алмазом всегда причиной плохого дизайна класса и что ни программист, ни компилятор не должны решать, потому что он не должен существовать в первую очередь?
Обновление: Возможно, мой пример был плохо выбран.
Смотреть это изображение
Проблема с алмазами http://cartan.cas.suffolk.edu/oopdocbook/opensource/src/multinheritance/PersonStudentTeacher.png
Конечно, вы можете сделать Личность виртуальной на С++ и, таким образом, у вас будет только один экземпляр человека в памяти, но реальная проблема сохраняется ИМХО. Как бы вы реализовали getDepartment() для GradTeachingFellow? Подумайте, он может быть студентом в одном отделе и преподавать в другом. Таким образом, вы можете либо вернуть один отдел, либо другой; нет идеального решения проблемы, и тот факт, что никакая реализация не может быть унаследована (например, Студент и Учитель могут быть как интерфейсами), похоже, не решают проблему для меня.
Ответы
Ответ 1
Что вы видите, так это то, как нарушения принципа Liskov Substitution Principle делают очень трудным создать рабочую логическую объектно-ориентированную структуру.
В принципе, (публичное) наследование должно только сузить цель класса, а не расширять его. В этом случае, наследуя от двух типов транспортных средств, вы фактически расширяете цель, и, как вы заметили, это не сработает - движение должно быть очень разным для водного транспорта, чем для дорожного транспортного средства.
Вместо этого вы можете объединить водный транспорт и объект наземного транспортного средства на своем земноводном корабле и решить извне, какая из двух будет соответствовать текущей ситуации.
В качестве альтернативы вы можете решить, что класс "автомобиль" бесполезен, и у вас будут отдельные интерфейсы для обоих. Это не решает проблему для вашего самолета-амфибии самостоятельно, хотя, если вы назовете способ передвижения "двигаться" в обоих интерфейсах, у вас все еще будет проблема. Поэтому я предлагаю агрегацию вместо наследования.
Ответ 2
C#
имеет явная реализация интерфейса, чтобы частично справиться с этим. По крайней мере, в том случае, если у вас есть один из промежуточных интерфейсов (его объект..)
Однако, вероятно, происходит то, что объект AmphibianVehicle знает, находится ли он в настоящее время на воде или на земле и делает правильные вещи.
Ответ 3
В вашем примере 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)
contract определяется интерфейсом 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() {
// ???
}
}
Но тогда это не было бы бриллиантом. Больше похоже на перевернутый треугольник.
Ответ 4
Люди утверждают, что Java не знает проблемы с алмазами. Я могу иметь только множественное наследование с интерфейсами, и поскольку у них нет реализации, у меня нет проблемы с алмазами. Это правда?
да, потому что вы контролируете реализацию интерфейса в D. Подпись метода одинакова между обоими интерфейсами (B/C) и видением, поскольку интерфейсы не имеют реализации - никаких проблем.
Ответ 5
Я не знаю Java, но если Interfaces B и C наследуют от интерфейса A, а класс D реализует интерфейсы B и C, тогда класс D просто реализует метод перемещения один раз, и это A.Move, что он должен реализовать, Как вы говорите, у компилятора нет проблем с этим.
Из примера, который вы даете в отношении AmphibianVehicle, использующего GroundVehicle и WaterVehicle, это можно легко решить, например, путем хранения ссылки на окружающую среду и отображения свойства поверхности в среде, которое будет проверять метод перемещения AmphibianVehicle. Нет необходимости, чтобы это передавалось как параметр.
Вы правы в том смысле, что это что-то для программиста, но, по крайней мере, оно компилируется и не должно быть проблемой.
Ответ 6
Нет проблемы с алмазом с наследованием на основе интерфейса.
С наследованием на основе класса множественные расширенные классы могут иметь различную реализацию метода, поэтому существует двусмысленность относительно того, какой метод фактически используется во время выполнения.
С наследованием на основе интерфейса существует только одно воплощение метода, поэтому нет никакой двусмысленности.
EDIT: На самом деле то же самое относится к наследованию класса для методов, объявленных как Абстрактные в суперклассе.
Ответ 7
Если я знаю, есть AmphibianVehicle интерфейса, который наследует GroundVehicle и WaterVehicle, как я бы выполнил его метод move()?
Вы предложили бы реализацию, подходящую для AmphibianVehicle
s.
Если a GroundVehicle
перемещается "по-другому" (т.е. принимает разные параметры, чем a WaterVehicle
), то AmphibianVehicle
наследует два разных метода: один для воды, один на земле. Если это невозможно, то AmphibianVehicle
не следует наследовать от GroundVehicle
и WaterVehicle
.
Является ли проблема с алмазами всегда причиной плохого дизайна и что-то ни программист, ни компилятор решить, потому что он не должен существовать в первую очередь?
Если это из-за плохого дизайна класса, программист должен его решить, поскольку компилятор не знает, как это сделать.
Ответ 8
Проблема, которую вы видите в примере "Студент/Учитель", просто заключается в том, что ваша модель данных неверна или, по крайней мере, недостаточна.
Студенческие и преподавательские классы объединяют две разные концепции "отдела", используя одно и то же имя для каждого из них. Если вы хотите использовать этот вид наследования, вы должны определить что-то вроде "getTeachingDepartment" в "Учитель" и "getResearchDepartment" в Student. Ваш GradStudent, который является как учителем, так и учеником, реализует оба.
Конечно, учитывая реалии аспирантуры, даже эта модель, вероятно, недостаточна.
Ответ 9
Я не думаю, что предотвращение конкретного множественного наследования переносит проблему из компилятора в программиста. В приведенном ниже примере программисту будет необходимо указать компилятору, какую реализацию использовать. Компилятор не может догадаться, что правильно.
Для вашего класса амфибий вы можете добавить метод, чтобы определить, находится ли транспортное средство на воде или земле, и используйте это, чтобы выбрать способ перемещения. Это сохранит безпараметрический интерфейс.
move()
{
if (this.isOnLand())
{
this.moveLikeLandVehicle();
}
else
{
this.moveLikeWaterVehicle();
}
}
Ответ 10
В этом случае было бы, вероятно, наиболее выгодным, чтобы AmphibiousVehicle был подклассом Vehicle (sibling of WaterVehicle и LandVehicle), чтобы полностью избежать проблемы в первую очередь. В любом случае, это было бы более правильным, так как амфибийный автомобиль не является водным транспортным средством или наземным транспортным средством, это совсем другое.
Ответ 11
Если move() имеет семантические отличия, основанные на том, что это Ground или Water (а не GroundVehicle и WaterVehicle, интерфейсы сами расширяют интерфейс GeneralVehicle, который имеет подпись move()), но ожидается, что вы будете смешивать и сопоставлять землю и воду реализаторы, то ваш пример один действительно является плохо спроектированным api.
Реальная проблема заключается в том, что столкновение имен является, фактически, случайным.
например, (очень):
interface Destructible
{
void Wear();
void Rip();
}
interface Garment
{
void Wear();
void Disrobe();
}
Если у вас есть куртка, которой вы хотите стать как одеждой, так и разрушаемой, у вас будет столкновение имен по методу изнашивания (законно названный).
Java не имеет решения для этого (то же самое верно для нескольких других статически типизированных языков). Динамические языки программирования будут иметь аналогичную проблему, даже без алмаза или наследования, это просто столкновение имен (присущая потенциальная проблема с Duck Typing).
.Net имеет концепцию явных реализаций интерфейса, посредством чего класс может определять два метода с одним и тем же именем и подписи, если оба помечены на двух разных интерфейсах. Определение соответствующего метода для вызова основано на известном интерфейсе переменной времени компиляции (или если отражением путем явного выбора вызываемого абонента)
Такие разумные, вероятные коллизии имен настолько сложны и что Java не был разбит на полях как непригодный для использования, не предоставляя явных реализаций интерфейса, предположил бы, что проблема не является существенной для использования в реальном мире.
Ответ 12
Я понимаю, что это конкретный экземпляр, а не общее решение, но похоже, что вам нужна дополнительная система для определения состояния и принятия решения о том, какой тип движения() будет выполнять автомобиль.
Похоже, что в случае амфибийного автомобиля вызывающий (скажем, "дроссель" ) не имел бы представления о состоянии воды/земли, но промежуточный определяющий объект, такой как "передача" в сочетании с "тягой" control "может понять это, затем вызвать move() с правильным перемещением параметров (колеса) или переместить (prop).
Ответ 13
Проблема действительно существует. В образце AmphibianVehicle-Class нужна другая информация - поверхность. Мое предпочтительное решение - добавить метод getter/setter в классе AmpibianVehicle, чтобы изменить элемент поверхности (перечисление). Реализация теперь может сделать правильную вещь, и класс останется инкапсулированным.
Ответ 14
У вас может быть проблема с алмазом в С++ (которая допускает множественное наследование), но не на Java или на С#. Не существует способа унаследовать от двух классов. Реализация двух интерфейсов с одним и тем же объявлением метода не подразумевает в этой ситуации, так как реализация конкретного метода может быть выполнена только в классе.
Ответ 15
Проблема алмаза в С++ уже решена: используйте виртуальное наследование. Или еще лучше, не ленитесь и наследуйте, когда это не необходимо (или неизбежно). Что касается примера, который вы дали, это можно было бы решить, переопределив, что значит быть способным управлять на земле или в воде. Способна ли способность двигаться по воде действительно определять транспортное средство на водной основе или что-то, что может сделать транспортное средство? Я бы предпочел, чтобы функция move(), которую вы описали, имела какую-то логику, которая спрашивает: "Где я, и могу ли я действительно переехать сюда?" Эквивалент функции bool canMove()
, который зависит от текущего состояния и присущих ему возможностей транспортного средства. И вам не нужно многократное наследование для решения этой проблемы. Просто используйте mixin, который отвечает на вопрос по-разному в зависимости от того, что возможно, и переводит суперкласс в качестве параметра шаблона, поэтому функция virtualMove будет видна через цепочку наследования.
Ответ 16
Собственно, если Student
и Teacher
являются обоими интерфейсами, это фактически решает вашу проблему. Если они являются интерфейсами, то getDepartment
- это просто метод, который должен появиться в вашем классе GradTeachingFellow
. Тот факт, что интерфейсы Student
и Teacher
обеспечивают соблюдение этого интерфейса, вовсе не является конфликтом. Реализация getDepartment
в вашем классе GradTeachingFellow
будет насыщать оба интерфейса без каких-либо проблем с алмазами.
НО, как указано в комментарии, это не решает проблему a GradStudent
обучения/является TA в одном отделе и будучи студентом в другом. Инкапсуляция, вероятно, вы хотите здесь:
public class Student {
String getDepartment() {
return "Economics";
}
}
public class Teacher {
String getDepartment() {
return "Computer Engineering";
}
}
public class GradStudent {
Student learning;
Teacher teaching;
public String getDepartment() {
return leraning.getDepartment()+" and "+teaching.getDepartment(); // or some such
}
public String getLearningDepartment() {
return leraning.getDepartment();
}
public String getTeachingDepartment() {
return teaching.getDepartment();
}
}
Не имеет значения, чем GradStudent
не концептуально "имеет" учитель и ученик - инкапсуляция все еще остается.
Ответ 17
интерфейс A
{ void add();
}
интерфейс B расширяет A
{ void add();
}
интерфейс C расширяет A
{ void add();
}
класс D реализует B, C
{
}
Не проблема с алмазом.