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

StackOverflow https://stackoverflow.com/questions/561729

Вопрос

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

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

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

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


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

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

Diamond Problem
(источник: Саффолк.edu)

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

Это было полезно?

Решение

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

Другие советы

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

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

В вашем примере 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() {
        // ???
    }
}

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

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

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

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

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

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

При наследовании на основе интерфейса не существует «проблемы ромба».

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

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

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

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

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

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

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

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

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

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

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

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

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

move()
{

  if (this.isOnLand())
  {
     this.moveLikeLandVehicle();
  }
  else
  {
    this.moveLikeWaterVehicle();
  }
}

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

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

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

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

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

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

Проблема ромба в C++ уже решена:использовать виртуальное наследование.Или, что еще лучше, не ленитесь и наследуйте, когда в этом нет необходимости (или неизбежно).Что касается примера, который вы привели, то эту проблему можно решить, переопределив, что значит иметь возможность управлять автомобилем по земле или по воде.Действительно ли способность передвижения по воде определяет водное транспортное средство или это просто то, на что оно способно?Я бы предпочел думать, что функция Move (), которую вы описали, имеет какую -то логику, которая спрашивает: «Где я, и могу ли я двигаться сюда?» Эквивалент bool canMove() функция, которая зависит от текущего состояния и свойств автомобиля.И для решения этой проблемы не требуется множественное наследование.Просто используйте примесь, которая отвечает на вопрос по-разному в зависимости от того, что возможно, и принимает суперкласс в качестве параметра шаблона, чтобы виртуальная функция 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 {void add ();}

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

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

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

}

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top