Как интерфейсы решают проблему круга и эллипса?

Иногда говорят, что интерфейсы решают несколько проблем объектно-ориентированного программирования, в частности проблему круг-эллипс . Это будет этот интерфейс

class IEllipse {
public:
  virtual double GetXRadius() const = 0;
  virtual double GetYRadius() const = 0;
  virtual void SetXRadius(double r) = 0;
  virtual void SetYRadius(double r) = 0;
} 

Но тогда у нас все равно будет проблематичная функция, которая работает для некоторых реализаций IEllipse, но не для реализации круга (которая подключает и SetXRadius, и SetYRadius к одной и той же переменной хранения radius):

std::string SerializeDestructively(IEllipse& ellipse) {
  std::stringstream s;
  s << ellipse.GetXRadius() << " ";
  ellipse.SetXRadius(0.0);
  s << ellipse.GetYRadius();
  ellipse.SetYRadius(0.0);
  return s.str();
}

Кажется, проблема в том, что IEllipse представляет собой эллипс изменяемого размера в каждом измерении. Круг не реализует этот интерфейс, поскольку в сплющенном виде он больше не является кругом. И запись определения эллипса в интерфейсе не решает эту проблему.

Эллипс вообще не является кругом, поскольку один не должен наследовать от другого. (Эллипс — это круг в частном вырожденном случае, но не в общем случае, и инварианты круга не являются инвариантами эллипса.)

Eljay 13.07.2024 18:54

Вы действительно хотите получить наследство?

Jarod42 13.07.2024 19:22

@Jarod42 Я слышал, что реализация интерфейсов лучше, чем наследование классов (например, в Java разные синтаксисы для этих двух механизмов). Интересно, чем именно интерфейсы лучше, особенно для решения проблемы круг-эллипс?

V. Semeria 13.07.2024 19:26

@Eljay Подтип вообще никогда не является его супертипом. Если бы это было так, это был бы изоморфизм типа, а не подтипа. Вопрос в том, как интерфейсы лучше представляют подтипы, чем наследование классов.

V. Semeria 13.07.2024 19:57

Ре. "I heard that implementing interfaces is better than class inheritance": пожалуйста, предоставьте конкретную ссылку (ссылка, которую вы предоставляете, довольно расплывчата и обширна). Кроме того, C++ не имеет формального определения «интерфейса», поэтому, если под интерфейсом вы подразумеваете класс, содержащий только чистые виртуальные члены, вам следует это указать.

G.M. 13.07.2024 20:02

@Г.М. Я буквально слышал это от некоторых моих коллег, поэтому у меня нет URL-адресов :-) Однако я вижу, что Java и C# имеют разные синтаксисы для интерфейсов и классов, и я думаю, что Go запретил наследование классов.

V. Semeria 13.07.2024 20:09

Java и C# разделяют интерфейсы и классы, поскольку они поддерживают только множественное наследование от интерфейсов. В C++ такого разделения на уровне языка нет. Обе стороны, конечно, будут утверждать, что их выбор «лучше».

BoP 13.07.2024 20:18

Вам следует перегрузить operator<< для своего IEllipse класса.

Thomas Matthews 13.07.2024 20:57

В реальной жизни, кхм, круги и эллипсы не меняют своих размеров. Удалите сеттеры, и проблема исчезнет в логическом порыве. Круг снова становится своего рода эллипсом, и в Стране Замещения Лисков снова все хорошо и хорошо. Сеттеры в любом случае вам не друзья по разным причинам. Используйте неизменяемые объекты как можно чаще.

n. m. could be an AI 13.07.2024 22:41

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

Galik 14.07.2024 08:02

Я имел в виду, ты действительно хочешь иметь ICircle? bool IElipse::isCircle() const может иметь больше смысла. Точно так же вы не создаете IElipse_5x_4y для определенного размера.

Jarod42 14.07.2024 14:32

Re: «Java и C# имеют разные синтаксисы интерфейсов и классов», но это можно считать ошибкой.

jaco0646 14.07.2024 18:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
12
134
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

Вот глупый пример, показывающий проблему:

void makeTallAndSkinny(IEllipse &e) {
    auto const oldX = e.GetXRadius();
    auto const oldY = e.GetYRadius();
    e.SetYRadius(oldY * 2);
    e.SetXRadius(oldX * 0.5);
}

Единственный способ, при котором интерфейс будет работать, — это нарушить контракт IEllipse до такой степени, что SetXRadius и SetYRadius фактически станут бессмысленными. Честно говоря, вы также можете сломать концепцию Circle до такой степени, что она станет бессмысленной (по крайней мере, по сравнению с математическим понятием круга), но вы все равно что-то нарушите.

Это также верно, если ваш Mammal интерфейс предполагает GivesLiveBirth() == true, а вы Platypus смоделировали как Mammal. Тот факт, что отношение соответствует вашей ментальной модели, не означает, что оно соответствует интерфейсам и объектно-ориентированному программированию.

Что вы можете сделать в C++, так это использовать Circle рычаги private наследования. Это позволило бы Circle и friend использовать его в качестве IEllipse там, где это уместно, в тех случаях, когда они знали, что интерфейс может не идеально сохраняться, но безопасен (например, не в моем примере выше). Однако общий код не сможет передать Circle, который поддерживает модель типа, от которой зависит ваша программа.

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

V. Semeria 13.07.2024 21:48

Я не уверен, что эта «проблема» реальна. Это скорее недостаток воображения, чем что-либо еще. Мышление в координатах XY ограничивает ваши возможности. Указание (двух) фокусов эллипса и его среднего радиуса обеспечивает идеально плавный переход между обеими формами (оба фокуса одинаковы для круга). Если вы настаиваете на мышлении в координатах X-Y, вам также понадобится угол, чтобы большая и малая полуоси эллипса соответствовали реальности. Именно GetXRadius() и GetYRadius() концептуально ошибочны!

Martin Brown 13.07.2024 22:44

@MartinBrown - Прошло около 25 лет с тех пор, как я изучал геометрию, поэтому я подчиняюсь вам в фокусах. Однако эта реализация по-прежнему не может обеспечить правильное наследование, если база class позволяет манипулировать фокусами независимо, а Circle требует foci1 == foci2.

Stephen Newell 14.07.2024 00:17

@MartinBrown - Если вы все еще не думаете, что проблема реальна, я настоятельно рекомендую вам прочитать все здесь: isocpp.org/wiki/faq/proper-inheritance#circle-ellipse. он объясняет проблему гораздо лучше, чем я, и включает обсуждение правильного наследования. Последнее полезно, поскольку оно применимо ко всем языкам с объектно-ориентированной функциональностью, а не только к C++.

Stephen Newell 14.07.2024 00:27

Цель интерфейса — отделить поведение объекта от фактической реализации. Нет ничего плохого в том, что объект am имеет несколько интерфейсов, если это имеет смысл. Модель COM от Microsoft предполагает, что это будет довольно распространено, и дает вам возможность запросить интерфейс у объекта, если у вас уже есть указатель на один из других его интерфейсов (см. IUnknown::QueryInterface).

class IEllipse {
public:
  virtual double GetXRadius() const = 0;
  virtual double GetYRadius() const = 0;
  virtual void SetXRadius(double r) = 0;
  virtual void SetYRadius(double r) = 0;
};

class ICircle {
public:
  virtual double GetRadius() const = 0;
  virtual void SetRadius(double r) = 0;
};

class ConcreteEllipse: public ICircle, IEllipse
{
  ICircle * GetCircleInterface() { return this; }
  IEllipse * GetEllipseInterface() { return this; }
};

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