Можно ли действительно решить проблему алмазов?

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

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

[удален пример автомобиля]

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


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

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

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

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


person Mecki    schedule 18.02.2009    source источник
comment
Это больше похоже на сообщение в блоге, чем на вопрос. К сожалению, кажется, что большинство ответов здесь до сих пор не прочитали все это. Но интересную перспективу вы представляете.   -  person Ken    schedule 18.02.2009


Ответы (18)


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

person Joris Timmermans    schedule 18.02.2009
comment
Хорошая точка зрения. Но разве множественное наследование всегда не нарушает этот принцип, поскольку наследование от двух классов всегда делает дочерний класс шире? - person Mecki; 18.02.2009
comment
Если два интерфейса полностью ортогональны (т.е. они не относятся к одной и той же концепции), то вам это сойдет с рук. НО... тогда вы нарушаете принцип единой ответственности, а это другой мир боли. - person Joris Timmermans; 18.02.2009
comment
узкое назначение класса называется специализацией :) - person leppie; 25.02.2009
comment
Я не согласен. Амфибия ЯВЛЯЕТСЯ и наземным, и водным транспортным средством, у него НЕТ наземного и водного транспортных средств. Я больше не вижу пример, но метод перемещения должен указывать поверхность или что-то в этом роде, и это заставит его работать нормально. Если это не так, интерфейс автомобиля должен быть улучшен. - person Jordi; 21.03.2009
comment
Использование IS A в качестве мнемоники для наследования ошибочно, собственно, это и есть причина обратить особое внимание на правильную замену Лискова. В часто цитируемом примере используются квадраты и прямоугольники с методом площадей, где совсем нетривиально получить правильную замену Лискова. - person Joris Timmermans; 23.03.2009

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

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

person Douglas Leeder    schedule 18.02.2009

В вашем примере move() принадлежит интерфейсу Vehicle и определяет контракт, «идущий из точки А в точку Б».

Когда 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() {
        // ???
    }
}

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

person Olivier    schedule 18.02.2009
comment
Мне нравится ваш пример с Vehicle и GraphicalComponent - это не настоящая проблема с бриллиантами, но это также одна из больших проблем, которые вы можете получить с интерфейсами, где тот факт, что никакая реализация не наследуется, на самом деле не спасает ваш день ;-) - person Mecki; 18.02.2009
comment
Отличный ответ! Хотя было бы неплохо, если бы в нем прямо указывалось, что проблема алмаза на самом деле не такая уж большая проблема и что разрешение MI с интерфейсами на самом деле не избавляет от особых проблем. - person Jordi; 21.03.2009

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

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

person Steven Evers    schedule 18.02.2009
comment
См. выше - то, что у move нет реализации, не решает проблему - я все еще не могу реализовать move() в классе D таким образом, чтобы он работал. - person Mecki; 18.02.2009
comment
Тогда позвольте мне перефразировать: если вы решите реализовать ромбовидный шаблон с интерфейсами, то ваша проблема решить, КАК это реализовать. Однако в этом нет ничего плохого. - person Steven Evers; 18.02.2009
comment
По этой логике в MI также нет проблемы с алмазами: вы просто указываете порядок, если значение по умолчанию нежелательно. Это похоже на интерфейсы SI+, за исключением того, что вам не нужно писать диспетчер самостоятельно. - person Ken; 18.02.2009
comment
это все еще бриллиант, если вы не можете добраться до реализации либо не всегда того или другого. - person ShuggyCoUk; 19.02.2009

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

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

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

person Patrick McDonald    schedule 18.02.2009

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

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

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

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

person Clayton    schedule 18.02.2009
comment
Для компилятора, но я, как программист, должен решить, как это реализовать. Таким образом, вместо абстрактных классов можно сказать, что множественное наследование — это нормально, и если есть какая-либо двусмысленность, ни одна из реализаций не наследуется, программист должен перезаписать метод. - person Mecki; 18.02.2009
comment
Я думаю, либо вы должны быть в состоянии написать метод так, чтобы он работал правильно в обоих случаях, либо вы сделали ошибку, пытаясь объединить классы таким образом - возможно, вам нужны moveLand() и moveWater() , или какой-то другой подход. - person rmeador; 18.02.2009

Если я знаю, что у меня есть интерфейс AmphibianVehicle, который наследует GroundVehicle и WaterVehicle, как мне реализовать его метод move()?

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

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

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

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

person eljenso    schedule 18.02.2009

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

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

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

person asdfwera    schedule 26.04.2010
comment
Вы, вероятно, правы; проблема только в том, что интерфейсы, подобные этому, имеют тенденцию расти со временем, и тот, кто создал интерфейс учителя, может быть другим человеком, чем человек, который создал интерфейс ученика, и ни один из них никогда не думал, что может быть класс, имеющий оба интерфейса одновременно. в то же время. Может быть, третье лицо создало класс GradTeachingFellow, и это было до того, как у студента или учителя вообще появился метод отдела. - person Mecki; 28.04.2010
comment
Я пытаюсь сказать здесь, что в идеальном объектно-ориентированном языке разные люди создают разные классы, и в идеале независимо от того, как выглядит иерархия наследования или как эти классы изменяются, ничто никогда не сломается. Хотя это далеко от реальности. ИМХО множественное наследование прекрасно работает только в том случае, если: 1. существует наследование за пределами библиотеки/бинарного файла; 2. каждый, кто работает над библиотекой/бинарником, имеет доступ к исходному коду и разрешение все менять, если захочет. Если одно из них неверно, лучше придерживаться одиночного наследования (гораздо меньше головной боли). - person Mecki; 28.04.2010

Я не думаю, что предотвращение конкретного множественного наследования переносит проблему с компилятора на программиста. В приведенном вами примере программисту по-прежнему необходимо указать компилятору, какую реализацию использовать. Компилятор никак не может догадаться, что правильно.

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

move()
{

  if (this.isOnLand())
  {
     this.moveLikeLandVehicle();
  }
  else
  {
    this.moveLikeWaterVehicle();
  }
}
person Dave Turvey    schedule 18.02.2009

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

person Kibbee    schedule 18.02.2009
comment
Хотя это хорошее решение, однако это означает, что вы должны переписать весь код, который вы написали для двух других дочерних классов, который на самом деле будет хорошо работать и для AmphibiousVehicle... хорошее решение, но несколько разрушает идею наследования реализации - person Mecki; 18.02.2009
comment
Только если многие функции амфибийного транспортного средства такие же, как у водного транспортного средства и наземного транспортного средства. Если что-то наследуется от двух объектов, которые являются почти полными противоположностями, большую часть этого, вероятно, все равно придется переписывать. - person Kibbee; 18.02.2009
comment
Я думаю, что семантически правильнее всего сделать это Алмазным путем. Это особенно очевидно, если WaterVehicle и LandVehicle реализуют некоторые методы, не унаследованные напрямую от Vehicle (например, isSinking или inTrafficJam). AmphibiousVehicle должен уметь делать и то, и другое. - person Jordi; 21.03.2009
comment
На самом деле, LandVehicle также может реализовать isSinking, если вы хотите уточнить это. - person Kibbee; 21.03.2009

Если 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 не была осуждена как непригодная для использования из-за того, что не предоставляет явных реализаций интерфейса, предполагает, что проблема не является существенной для реального использования.

person ShuggyCoUk    schedule 18.02.2009
comment
У Java с этим проблем нет. Класс Java, реализующий Destructible и Garment, будет иметь 3 метода: Wear(), Rip() и Disrobe(). Тот факт, что износ может иметь 2 значения в английском языке, является проблемой имени, а не проблемой Java. Wear() будет делать все, что вы запрограммируете, независимо от имени. - person Clayton; 19.02.2009
comment
Именно так, но износ не может определить, вызывался ли он в контексте просьбы о ношении или деградации самого себя. И это несмотря на тот факт, что такая информация могла быть доступна для системы типов при компиляции. Таким образом, он не может эффективно делать и то, и другое. - person ShuggyCoUk; 19.02.2009
comment
Противоречивые желания из-за конфликта имен, который не может быть разрешен, является алмазной проблемой. Если вы можете включить информацию для ее решения, она перестает быть проблемой и по определению больше не является алмазом. - person ShuggyCoUk; 19.02.2009

Я понимаю, что это конкретный случай, а не общее решение, но похоже, что вам нужна дополнительная система для определения состояния и решения о том, какой тип move() будет выполнять транспортное средство.

Создается впечатление, что в случае с амфибией вызывающий абонент (скажем, «дроссель») не будет иметь представления о состоянии воды/земли, но промежуточный определяющий объект, такой как «трансмиссия» в сочетании с «антипробуксовочной системой», может разберитесь с этим, затем вызовите move() с правильным параметром move(wheels) или move(prop).

person willoller    schedule 18.02.2009

Проблема действительно существует. В примере AmphibianVehicle-Class нужна другая информация — поверхность. Мое предпочтительное решение — добавить метод получения/установки в класс AmpibianVehicle для изменения члена поверхности (перечисления). Реализация теперь может работать правильно, а класс остается инкапсулированным.

person p2u    schedule 18.02.2009

У вас может быть проблема с ромбом в C++ (который допускает множественное наследование), но не в Java или C#. Невозможно наследовать от двух классов. Реализация двух интерфейсов с одним и тем же объявлением метода в данной ситуации не подразумевается, так как конкретная реализация метода может быть выполнена только в классе.

person Victor Rodrigues    schedule 18.02.2009
comment
Проблема алмазов все еще существует в определенной степени в java для импорта библиотек. Это частично решается уточнением полного имени, но есть вероятность того, что код, который вы уже написали с использованием класса X, может быть сделан двусмысленным путем импорта другой библиотеки, которая определяет класс X. - person B T; 26.04.2010
comment
@BT Я действительно хочу увидеть такой код Java. Когда я говорю что-то подобное, многие думают, что я сумасшедший. Можете ли вы поделиться кодом в репозитории git или здесь. - person ASharma7; 12.06.2020
comment
@ ASharma7 У меня нет хорошего примера. Я, конечно, лично сталкивался с такими случаями, хотя в далеком прошлом, когда я еще регулярно работал с java. Любому хорошему программисту должно быть совершенно ясно, что неуправляемые пространства имен с невероятной вероятностью время от времени будут приводить к разочаровывающим конфликтам. Без какого-либо централизованного способа определить, кто получает какое имя, вы в конечном итоге столкнетесь с двумя вещами, использующими одно и то же имя, и будете вынуждены делать несколько надоедливых хаков. - person B T; 12.06.2020
comment
@BT хорошо, это было так давно. Я видел, что ты сейчас работаешь разработчиком Javascript. - person ASharma7; 12.06.2020

Алмазная проблема в C++ уже решена: используйте виртуальное наследование. Или еще лучше, не ленитесь и наследуйте, когда это не нужно (или неизбежно). Что касается примера, который вы привели, его можно решить, переопределив, что значит быть способным двигаться по земле или по воде. Действительно ли способность двигаться по воде определяет водное транспортное средство или это просто то, на что транспортное средство способно? Я скорее думаю, что описанная вами функция move() имеет какую-то логику, которая спрашивает: «Где я и могу ли я на самом деле двигаться сюда?» Эквивалент функции bool canMove(), которая зависит от текущего состояния и внутренних возможностей транспортного средства. И вам не нужно множественное наследование, чтобы решить эту проблему. Просто используйте миксин, который отвечает на вопрос по-разному в зависимости от того, что возможно, и принимает суперкласс в качестве параметра шаблона, чтобы виртуальная функция canMove была видна через цепочку наследования.

person Michel    schedule 18.02.2009

На самом деле, если Student и Teacher оба являются интерфейсами, это действительно решает вашу проблему. Если это интерфейсы, то getDepartment — это просто метод, который должен появиться в вашем классе GradTeachingFellow. Тот факт, что оба интерфейса Student и Teacher используют этот интерфейс, вовсе не является конфликтом. Реализация getDepartment в вашем классе GradTeachingFellow удовлетворит оба интерфейса без каких-либо проблем с алмазами.

НО, как указано в комментарии, это не решает проблему 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 концептуально не "имеет" учителя и ученика - инкапсуляция все еще впереди.

person B T    schedule 26.04.2010
comment
И как это решает проблему, что GradTeachingFellow может быть студентом на одном факультете, но преподавателем на другом? Даже если это интерфейс, он потерпит неудачу, просто не на уровне кода. - person Mecki; 26.04.2010
comment
Вы правы, это решает проблему на уровне кода, но не на уровне семантики. В случае, о котором вы говорите, наследование может быть не тем, что вам нужно здесь - может быть, вам нужна инкапсуляция. Это не будет работать в Java, но в качестве альтернативы было бы семантически возможно наследовать оба метода под псевдонимами - например. getTeachingDepartment() и getStudentDepartment(). Java не очень гибкий язык, поэтому инкапсуляция, вероятно, лучший выбор для класса GradStudent. - person B T; 27.04.2010

интерфейс A { недействительным add(); }

интерфейс B расширяет A { void add(); }

интерфейс C расширяет A { void add(); }

класс D реализует B, C {

}

Разве это не проблема с бриллиантами?

person Nirupma    schedule 07.02.2011
comment
Нет, потому что это интерфейсы. - person BoltClock; 07.02.2011

Композиция над наследованием

(Слишком много ответов, но так как это еще не придумали)

Как правило, проблемы такого типа и, в первую очередь, смертельный алмаз смерти решаются/обходятся путем использования композиции (класс a имеет x, y, z ) над наследованием (класс a является x, y, z).

Это решено / Относится к чему?

  • Вы правы, что касается вашего дизайна, это не решает проблему множественного наследования. Язык, способный перемаркировать базовые методы для подкласса (или подкласса), мог бы решить эту проблему (по крайней мере, для прямых вызовов, когда приведение вниз это все еще оставалось бы проблематичным; см. другие попытки ниже).
  • Что касается языка (Java), это фактически решено. Компилятору (и даже разработчику) понятно, что оба метода интерфейса (с одинаковым именем и сигнатурой) будут реализованы только через один метод.

Дальнейшие практические мысли

Тем не менее, оба случая решаются при использовании композиции, что, однако, может быть немного неудобным, например. в Java, поскольку (там) вы не можете напрямую использовать mySpecificGradTeachingStudent.getName().

  • Некоторые языки (например, Go) требуют базового типа, если он неоднозначен (аналогично this.Student.getDepartment), который идет - от образа мыслей - уже в направлении композиции (через наследование).

  • Точно так же другие языки (например, Rust) идут дальше и вместо этого используют то, что называется traits (полностью исключая наследование). Это означает, что структура реализует TraitA и TraitB, а не A и/или B.

  • Поскольку все эти идеи в основном основаны на концепциях интерфейса и (упрощенно) добавлении синтаксического сахара в композицию (что, по моему мнению, действительно правильно, и решает и другие проблемы, например, объекты, полностью изменяющиеся после нескольких уровней наследования), здесь последний (очень) другая попытка. Любой язык (с множественным наследованием) может просто добавить концепцию приоритета. Это означает, что (например) первым реализованным базовым компонентом/классом будет тот, который предоставляет свой стек методов (т.е. Student), а другие методы просто игнорируются. Это по-прежнему вызывает такие вопросы, как: что происходит, когда специально приводятся к объекту Teacher, но даже эти случаи могут быть решены по соглашению. (Внимание! Это может быть не самое интуитивно понятное решение с точки зрения дизайна, но, тем не менее, простое для спецификации языка.)

Наконец, ссылка на некоторые из этих мыслей для примера ученика/учителя (с небольшими отклонениями, такими как драконы): https://dev.to/thisismahmoud/composition-over-inheritance-4fn9

person Levite    schedule 02.07.2021