Иногда говорят, что интерфейсы решают несколько проблем объектно-ориентированного программирования, в частности проблему круг-эллипс . Это будет этот интерфейс
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
представляет собой эллипс изменяемого размера в каждом измерении. Круг не реализует этот интерфейс, поскольку в сплющенном виде он больше не является кругом. И запись определения эллипса в интерфейсе не решает эту проблему.
Вы действительно хотите получить наследство?
@Jarod42 Я слышал, что реализация интерфейсов лучше, чем наследование классов (например, в Java разные синтаксисы для этих двух механизмов). Интересно, чем именно интерфейсы лучше, особенно для решения проблемы круг-эллипс?
@Eljay Подтип вообще никогда не является его супертипом. Если бы это было так, это был бы изоморфизм типа, а не подтипа. Вопрос в том, как интерфейсы лучше представляют подтипы, чем наследование классов.
Ре. "I heard that implementing interfaces is better than class inheritance"
: пожалуйста, предоставьте конкретную ссылку (ссылка, которую вы предоставляете, довольно расплывчата и обширна). Кроме того, C++
не имеет формального определения «интерфейса», поэтому, если под интерфейсом вы подразумеваете класс, содержащий только чистые виртуальные члены, вам следует это указать.
@Г.М. Я буквально слышал это от некоторых моих коллег, поэтому у меня нет URL-адресов :-) Однако я вижу, что Java и C# имеют разные синтаксисы для интерфейсов и классов, и я думаю, что Go запретил наследование классов.
Java и C# разделяют интерфейсы и классы, поскольку они поддерживают только множественное наследование от интерфейсов. В C++ такого разделения на уровне языка нет. Обе стороны, конечно, будут утверждать, что их выбор «лучше».
Вам следует перегрузить operator<<
для своего IEllipse
класса.
В реальной жизни, кхм, круги и эллипсы не меняют своих размеров. Удалите сеттеры, и проблема исчезнет в логическом порыве. Круг снова становится своего рода эллипсом, и в Стране Замещения Лисков снова все хорошо и хорошо. Сеттеры в любом случае вам не друзья по разным причинам. Используйте неизменяемые объекты как можно чаще.
IMO, проблема Circle-Elipse является хорошим примером того, как полиморфизм, основанный на наследовании, не является хорошим решением. Это иронично, потому что он почти всегда используется в учебниках по полиморфизму, основанному на наследовании.
Я имел в виду, ты действительно хочешь иметь ICircle
? bool IElipse::isCircle() const
может иметь больше смысла. Точно так же вы не создаете IElipse_5x_4y
для определенного размера.
Re: «Java и C# имеют разные синтаксисы интерфейсов и классов», но это можно считать ошибкой.
Интерфейсы не решают проблему круга-эллипса и не могут решить эту проблему, по крайней мере, если вы пытаетесь также сохранить математические свойства каждой фигуры. Причина, по которой используется эта задача, заключается в том, что она является хорошим примером математической концепции (круг — это особый вид эллипса), не работающей с правильным наследованием (т. е. принципом подстановки Лискова).
Вот глупый пример, показывающий проблему:
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
, который поддерживает модель типа, от которой зависит ваша программа.
Впервые вижу содержательный пример частного наследования, спасибо.
Я не уверен, что эта «проблема» реальна. Это скорее недостаток воображения, чем что-либо еще. Мышление в координатах XY ограничивает ваши возможности. Указание (двух) фокусов эллипса и его среднего радиуса обеспечивает идеально плавный переход между обеими формами (оба фокуса одинаковы для круга). Если вы настаиваете на мышлении в координатах X-Y, вам также понадобится угол, чтобы большая и малая полуоси эллипса соответствовали реальности. Именно GetXRadius() и GetYRadius() концептуально ошибочны!
@MartinBrown - Прошло около 25 лет с тех пор, как я изучал геометрию, поэтому я подчиняюсь вам в фокусах. Однако эта реализация по-прежнему не может обеспечить правильное наследование, если база class
позволяет манипулировать фокусами независимо, а Circle
требует foci1 == foci2
.
@MartinBrown - Если вы все еще не думаете, что проблема реальна, я настоятельно рекомендую вам прочитать все здесь: isocpp.org/wiki/faq/proper-inheritance#circle-ellipse. он объясняет проблему гораздо лучше, чем я, и включает обсуждение правильного наследования. Последнее полезно, поскольку оно применимо ко всем языкам с объектно-ориентированной функциональностью, а не только к C++.
Цель интерфейса — отделить поведение объекта от фактической реализации. Нет ничего плохого в том, что объект 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; }
};
Эллипс вообще не является кругом, поскольку один не должен наследовать от другого. (Эллипс — это круг в частном вырожденном случае, но не в общем случае, и инварианты круга не являются инвариантами эллипса.)