Может ли Алмазная проблема быть действительно решена?
Типичной проблемой в ОО-программировании является Алмазная задача. У меня есть родительский класс A с двумя подклассами B и C. A имеет абстрактный метод, B и C реализуют его. Теперь у меня есть подкласс D, который наследует B и C. проблема алмаза теперь заключается в том, какую реализацию должен использовать D, одну из B или одну из C?
Люди утверждают, что Java не знает проблемы алмазов. У меня может быть только множественное наследование с интерфейсами, и поскольку они не имеют реализации, у меня нет проблемы с алмазами. Является это действительно так? Я так не думаю. Смотрите ниже:
[удален пример транспортного средства]
Является ли Алмазная проблема всегда причиной плохого дизайна класса и чего-то, что ни программист, ни компилятор не должны решать, потому что она не должна существовать в первую очередь?
Update: возможно, мой пример был неудачно выбран.
Смотрите это изображение
Конечно, вы можете сделать человека виртуальным в C++ , и таким образом у вас будет только один экземпляр человека в памяти, но реальная проблема сохраняется IMHO. Как бы вы реализовали getDepartment () для GradTeachingFellow? Подумайте, он может быть студентом на одном факультете и преподавать на другом. Таким образом, вы можете либо вернуть один отдел, либо другой; нет идеального решения проблемы и тот факт, что никакая реализация не может быть унаследована (например, ученик и учитель могут быть интерфейсами), кажется, не решает проблему для меня.
17 ответов:
Однако, вероятно, происходит следующее: объект-амфибия знает, находится ли он в данный момент на воде или на суше, и поступает правильно.
C#имеет явную реализацию интерфейса , чтобы частично справиться с этим. По крайней мере, в том случае, когда у вас есть один из промежуточных интерфейсов (его объект..)
В вашем примере
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() { // ??? } }
Люди утверждают, что Ява не знает алмазной проблемы. У меня может быть только множественное наследование с интерфейсами, и поскольку они не имеют реализации, у меня нет проблемы с алмазами. Неужели это правда?Да, потому что вы контролируете реализацию интерфейса В D. сигнатура метода одинакова между обоими интерфейсами (B/C), и видя, что интерфейсы не имеют реализации - никаких проблем.
Я не знаю Java, но если интерфейсы B и C наследуют от интерфейса A, А класс D реализует интерфейсы B и C, то класс D просто реализует метод move один раз, и это A. Move, который он должен реализовать. Как вы говорите, компилятор не имеет никаких проблем с этим.
Из примера, который вы приводите относительно амфибии, реализующей наземную и водную машины, это можно легко решить, сохранив ссылку на окружающую среду, например, и выставив свойство поверхности на Окружающая среда, которую будет проверять метод перемещения амфибии. Нет необходимости передавать его в качестве параметра.
Вы правы в том смысле, что это то, что программист должен решить, но, по крайней мере, он компилирует и не должен быть "проблемой".
Нет никакой алмазной проблемы с наследованием на основе интерфейса.
При наследовании на основе классов несколько расширенных классов могут иметь различную реализацию метода, поэтому существует неопределенность относительно того, какой метод фактически используется во время выполнения.
При интерфейсном наследовании существует только одна имплементация метода, поэтому нет никакой двусмысленности.
EDIT: на самом деле, то же самое относится к классовому наследованию для методов, объявленных как абстрактные в надкласс.
Если я знаю, есть амфибия интерфейс, который наследует Наземный и водный транспорт, как буду ли я реализовывать метод move ()?
Вы бы предоставили реализацию, подходящую для
AmphibianVehicles.Если a
GroundVehicleдвижется "по-другому" (т. е. принимает другие параметры, чем aWaterVehicle), тоAmphibianVehicleнаследует два различных метода, один для воды, другой для Земли. Если это невозможно, тоAmphibianVehicleне должно наследовать отGroundVehicleиWaterVehicle.Является ли Алмазная проблема всегда причиной плохой дизайн класса и что-то еще ни программисту, ни компилятору это не нужно. решать, потому что его не должно быть во-первых?
Если это связано с плохим дизайном класса, то программист должен решить его, так как компилятор не знает, как это сделать.
Проблема, которую вы видите в Примере Ученик / Учитель, заключается просто в том, что ваша модель данных неверна или, по крайней мере, недостаточна.
Классы студентов и преподавателей объединяют два различных понятия" кафедра", используя одно и то же название для каждого из них. Если вы хотите использовать этот вид наследования, вы должны вместо этого определить что-то вроде "getTeachingDepartment" в Учителе и "getResearchDepartment" в студенте. Ваш GradStudent, который является одновременно учителем и студентом, реализует оба. Конечно, учитывая реалии Высшей школы, даже эта модель, вероятно, недостаточна.
Я не думаю, что предотвращение конкретного множественного наследования переносит проблему с компилятора на программиста. В приведенном примере программисту все равно нужно будет указать компилятору, какую реализацию использовать. Компилятор никак не может угадать, какая из них правильная.
Для вашего класса амфибий вы можете добавить метод для определения того, находится ли транспортное средство на воде или на суше, и использовать этот метод для выбора способа перемещения. Это позволит сохранить интерфейс без параметров.
move() { if (this.isOnLand()) { this.moveLikeLandVehicle(); } else { this.moveLikeWaterVehicle(); } }
В этом случае, вероятно, было бы наиболее выгодно, чтобы амфибия была подклассом транспортного средства (родственным водному и наземному транспортному средству), чтобы полностью избежать этой проблемы в первую очередь. Это, вероятно, было бы более правильным в любом случае, поскольку амфибия-это не водный или наземный транспорт, это совершенно другое дело.
Если move() имеет семантические различия, основанные на том, что он является Ground или Water (а не groundvehicle и WaterVehicle интерфейсы оба сами расширяют GeneralVehicle интерфейс, который имеет подпись move ()), но ожидается, что вы будете смешивать и сопоставлять ground и water реализаторы, то ваш пример действительно является одним из плохо разработанных api.
Реальная проблема заключается в том, что коллизия имен, по сути, случайна. например (очень синтетический):interface Destructible { void Wear(); void Rip(); } interface Garment { void Wear(); void Disrobe(); }Если вы имейте пиджак, который вы хотите быть и одеждой, и разрушаемым, у вас будет столкновение имени на (законно названном) способе ношения.
Java не имеет решения для этого (то же самое верно для нескольких других статически типизированных языков). Динамические языки программирования будут иметь аналогичную проблему, даже без алмаза или наследования, это просто столкновение имен (неотъемлемая потенциальная проблема с утиным типом)..Net имеет понятие явного интерфейса реализации , в которых класс может определить два метода с одинаковым именем и сигнатурой, если оба они помечены для двух разных интерфейсов. Определение соответствующего метода для вызова основано на известном интерфейсе переменной во время компиляции (или при отражении явным выбором вызываемого объекта)
Что разумные, вероятные коллизии имен так трудно найти, и что java не была привязана к столбу как непригодная для не предоставления явных реализаций интерфейса. предположим, что эта проблема не является существенной для использования в реальном мире.
Я понимаю, что это конкретный пример, а не общее решение, но похоже, что вам нужна дополнительная система, чтобы определить состояние и решить, какой тип движения() будет выполнять транспортное средство.
Похоже, что в случае амфибии вызывающий объект (скажем, "дроссель") не имел бы представления о состоянии воды / грунта, но промежуточный определяющий объект, такой как "трансмиссия" в сочетании с "контролем тяги", мог бы выяснить это, а затем вызвать move () с помощью функции правильный параметр move (колеса) или move (опора).
Проблема действительно существует. В образце амфибийного класса нужна еще одна информация-поверхность. Мое предпочтительное решение-добавить метод getter / setter в класс AmpibianVehicle, чтобы изменить элемент surface (перечисление). Теперь реализация может сделать все правильно, и класс останется инкапсулированным.
Вы можете иметь алмазную проблему в C++ (что позволяет множественное наследование), но не в Java или C#. Не существует способа наследования от двух классов. Реализация двух интерфейсов с одним и тем же объявлением метода не подразумевает в этой ситуации, так как конкретная реализация метода может быть выполнена только в классе.
Алмазная проблема в C++ уже решена: используйте виртуальное наследование. Или еще лучше, не ленитесь и наследуйте, когда это не нужно (или неизбежно). Что касается примера, который вы привели, это можно решить, переопределив, что значит быть способным управлять автомобилем на земле или в воде. Действительно ли способность двигаться по воде определяет транспортное средство на водной основе или это просто что-то, что транспортное средство способно делать? Я бы предпочел думать, что функция move (), которую вы описали, имеет какую-то логику за этим стоит вопрос: "где я нахожусь и могу ли я на самом деле переехать сюда?"Эквивалент функции
bool canMove(), которая зависит от текущего состояния и врожденных способностей транспортного средства. И вам не нужно множественное наследование, чтобы решить эту проблему. Просто используйте mixin, который отвечает на вопрос по-разному в зависимости от того, что возможно, и принимает суперкласс в качестве параметра шаблона, чтобы виртуальная функция canMove была видна через цепочку наследования.
На самом деле, если
StudentиTeacherявляются интерфейсами, это действительно решает вашу проблему. Если это интерфейсы, тоgetDepartment- это просто метод, который должен появиться в вашем классеGradTeachingFellow. Тот факт, что оба интерфейсаStudentиTeacherобеспечивают выполнение этого интерфейса, вовсе не является конфликтом. РеализацияgetDepartmentв вашем классеGradTeachingFellowудовлетворит оба интерфейса без каких-либо проблем с алмазами.Но, как указано в комментарии, это не решает проблему
GradStudentобучения / бытия та в одном отделении, а будучи студентом в другом. Инкапсуляция, вероятно, то, что вы хотите здесь: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концептуально не "имеет" учитель и ученик - инкапсуляция все еще является способом идти.
Интерфейс A { пустоту добавить(); }
B интерфейс расширяет { пустоту добавить(); }
Интерфейс C расширяет A { пустоту добавить(); }
Класс D реализует B, C {
}
Разве это не Алмазная проблема?
Comments