Здравствуйте, Кодёнок, Вы писали:
Кё>Я ищу примеры классов объектов, которые вызывают трудности при объектном моделировании в современных ЯП типа C++ или C#.
Мне кажется, трудности во многом вызываются использованием единого механизма наследования для выражения совершнно разных отношений:
тип — подтип;
структура — расширение cтруктуры;
интерфейс — реализация;
интерфейс — расширеный интерфейс;
используемый интерфейс — реализация;
реализация — точно такая же, но с перламутровыми пуговицами;
...
Здравствуйте, Кодёнок, Вы писали:
Кё>Я ищу примеры классов объектов, которые вызывают трудности при объектном моделировании в современных ЯП типа C++ или C#. В качестве примера, я знаю всего две классические задачи:
Кё>1. Построить иерархию классов для прямоугольника и квадрата, с изменяющимися размерами. Трудность в том, что хотя квадрат всегда является прямоугольником (наследуется), он не захочет наследовать его реализацию (зачем ему раздельно хранить ширину и высоту?). Другая трудность в том, что прямоугольник иногда может являться квадратом, но классические отношения между классами, которые предлагают ЯП (наследование и implicit-conversion) не могут выразить такого отношения.
Хотя квадрат и является частным случаем прямоугольника, это не значит, что выгодно его наследовать от прямоугльника.
Скорее наоборот. Square : Figure { width }; Rectangle : Figure { width, height }; Figure { location, rotation, scale };
Тут замечаем, что у фигур есть общие поля. Теперь мы можем:
либо (1) завести для них общий базовый класс with_width { width };
И тогда Square : Figure, with_width {}; Rectangle : Figure, with_width { height };
либо (2) отнаследовать прямоугольник от квадрата на прямую Square : Figure { width }; Rectangle : Figure, Square { height };
Но эти решения не всегда эффективны. В случае графического редактора может быть выгоднее другое.
У фигур есть параметры. Параметры бывают разных типов, но у всех у них есть имя.
Кроме того фигуры бывают разных типов. Для типов заводим базовый абстрактный класс Prototype.
Для сериализации картинки, у типов фигур должны быть имена.
// параметры
interface IParam
{};
struct text_param : IParam
{};
struct number_param : IParam
{};
// ПараметрЫ. указатель на список параметров ( на хэш )typedef map<string, IParam *> * Params;
// фигура (не абстрактный класс!)struct Figure
{
Prototype * htype;
Params hparams;
};
Figure::constructor(Prototype * hnew_type)
{
htype= hnew_type;
hparams= htype->get_default_params();
}
Figure::draw()
{
htype->draw(this);
}
Figure::serialize(Archive & ar)
{
// сериализим имя типа фигуры
// сериализим список параметров
}
// прототипы фигурstruct Prototype abstract
{
virtual draw(Figure *) abstract;
virtual Params get_default_params() abstract;
virtual string get_name() abstract;
};
// прототип прямоугольникаstruct type_rect : Prototype
{
draw(Figure * hf)
{ /*рисуем прямоугольник*/ }
// другие методы
};
// тут нужны другие типы фигур ...
// картинкаstruct Picture
{
// тут список фигур
// методы:
draw(); // рисуем фигуры
serialize(...); // сериализим фигуры
add_figure(...);
del_figure(...);
// и т.д.
};
Квадрат имеет один параметр "сторона" ("side");
У прямоугольника "width", "height";
У круга "radius";
Фигура в этой схеме имеет указатели на тип и на хэш параметров для того, чтобы можно было
конвертировать одну фигуру в другую. К примеру квадрат преобразовать в прямоугольник.
Тут не фигура сама себя рисует. За действия над фигурами отвечают их прототипы.
Таким образом типы фигур не hard-coded, а могут загружаться из библиотек типов.
т.е. Тип фигуры можно тоже сериализовать.
Здравствуйте, Кодёнок, Вы писали:
Кё>1. Построить иерархию классов для прямоугольника и квадрата, с изменяющимися размерами. Трудность в том, что хотя квадрат всегда является прямоугольником (наследуется), он не захочет наследовать его реализацию (зачем ему раздельно хранить ширину и высоту?).
Потому что у него есть и ширина, и высота. По мне так можно унаследовать квадрат от прямоугольника, переопределить сеттеры и при изменении одной стороны автоматически изменять другую.
Здравствуйте, Delight, Вы писали:
D>Потому что у него есть и ширина, и высота. По мне так можно унаследовать квадрат от прямоугольника, переопределить сеттеры и при изменении одной стороны автоматически изменять другую.
Наследование класса — это сахар. Да и определение класса — тоже сахар; по сути происходит описание интерфейса и его же реализация. Вот если иммутабельные Rectangle и Square реализуют интерфейс IRectange, то проблем не возникает. А зачем нужны мутабельные Rectangle и Square?
Здравствуйте, Кодёнок, Вы писали:
Кё>1. Построить иерархию классов для прямоугольника и квадрата, с изменяющимися размерами.
Для решения какого класса задач нужна такая иерархия?
Кё>2. Комплексное число и вещественное число.
Опять-таки: для решения какого класса задач нужна такая иерархия?
Кё>Также буду рад услышать названия языков, которые попытались реализовать более богатый набор отношений, чем даёт классическое понимание объектно-ориентированного проектирования.
Предположим, такой язык нашелся. Ваши дальнейшие действия? Чем это Вам поможет при решении конкретных задач?
Здравствуйте, Delight, Вы писали: D>Потому что у него есть и ширина, и высота. По мне так можно унаследовать квадрат от прямоугольника, переопределить сеттеры и при изменении одной стороны автоматически изменять другую.
Ребята, попользуйте поиск. Квадраты с прямоугольниками в ООП обсуждались на этом сайте досконально уже раза три. Все ответы, которые вы можете придумать, уже были даны.
Правильный ответ — такой: при проектировании иерархии не надо думать о свойствах. Думайте о поведении. Если речь идет о CAD или векторном граф.редакторе, у вас 100% не будет классов ни для квадратов, ни для прямоугольников. Если речь идет о системе для решения геометрических задач, то никаких сеттеров ни у кого не будет вообще.
... << RSDN@Home 1.2.0 alpha rev. 677>>
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, konsoletyper, Вы писали:
D>>Потому что у него есть и ширина, и высота. По мне так можно унаследовать квадрат от прямоугольника, переопределить сеттеры и при изменении одной стороны автоматически изменять другую.
K>Наследование класса — это сахар. Да и определение класса — тоже сахар; по сути происходит описание интерфейса и его же реализация. Вот если иммутабельные Rectangle и Square реализуют интерфейс IRectange, то проблем не возникает. А зачем нужны мутабельные Rectangle и Square?
Если нужны мутабельные, то нужно делать мутабельные. Впрочем у меня эта боян-проблема-в-себе вызывает довольно вялый интерес. На флейм не пойду.
Здравствуйте, Кодёнок, Вы писали:
Кё>1. Построить иерархию классов для прямоугольника и квадрата, с изменяющимися размерами. Трудность в том, что хотя квадрат всегда является прямоугольником (наследуется), он не захочет наследовать его реализацию (зачем ему раздельно хранить ширину и высоту?). Другая трудность в том, что прямоугольник иногда может являться квадратом, но классические отношения между классами, которые предлагают ЯП (наследование и implicit-conversion) не могут выразить такого отношения.
Последнее отношение выполняется не всегда. Есть верное утверждение "квадрат? x => прямоугольник? x", т.е. любой квадрат можно представить как прямоугольник, но утверждение "прямоугольник? x => квадрат? x" в общем случае неверно, соответственно, никто по рукам бить не будет, если мы нарушим LSP. Так что можно обойтись явным приведением прямоугольника к квадрату, с выбросом исключения при необходимости.
Кё>2. Комплексное число и вещественное число. Хотя вещественное число является комплексным (наследование), вы не станете наследовать его от комплексного по многим причинам: вам не нужна его реализация (два float в памяти вместо одного), вы вряд ли будете рады тому что sqrt(4) вместо 2.0 вернёт тупл из двух комплексных чисел, и т.п.
А кто сказал, что нужно наследовать реализацию? С интерфейсами не возникает никаких проблем. В частности, sqrt для float и sqrt для complex — это, вообще-то, математически разные вещи, просто символ радикала является полиморфным (например, в той же математики делается различие между Ln и ln). Так что тут надо либо давать другое имя, либо делать explicit-реализацию метода IComplex.Sqrt, либо юзать язык с перегрузкой типов по возвращаемому значению.
Зато есть другая вещь, которую вот так просто в терминах "классового" ООП не выразишь. Например, поле комплексных чисел является алгебраически замкнутым, а поле действительных — нет. Или ещё пример: Z является кольцом, а R — полем. И что тут делать? Хочу заметить, что подобные вещи вообще в терминах "классовых" ООЯ неудобно записывать. Гораздо лучше в терминах type classes из Haskell. Или на худой конец, в динамике. Если выражать в статике, то получим множественные runtime проверки типов, что по сути та же динамака.
Кё>Также буду рад услышать названия языков, которые попытались реализовать более богатый набор отношений, чем даёт классическое понимание объектно-ориентированного проектирования.
Smalltalk, Self вроде бы теоретически больше позволяют, чем "классовые" ООЯ, хотя у них есть свои проблемы.
Здравствуйте, mkizub, Вы писали:
M>А какие ещё отношения ты знаешь?
Например то, что в некоторых языках реализовано как трейты (классы типов):
trait Addable[ThisType] { def +(ThisType other) }
Позволяет классифицировать как Addable любой тип, в том числе будущий или не подозревающий о существовании Addable. В классическом понимании ООП ты должен построить иерархию сразу, унаследовав всё что нужно от Addable. Если ты берешь стороннюю библиотеку (типичная ситуация при компонентной разработке), её иерархия не может включать твой класс Addable, и таким образом ты имеешь классы, про которые невооруженным взглядом видно что они Addable, но средства языка вроде C# не позволят тебе работать с ними, там, где требуется Addable.
Или отношение, которое сейчас принято реализовать паттерном прокси. Например, если есть некий IString, и есть классы CString и std::string, которые очевидно могут являться IString, но тот же C++ не позволит тебе добавить еще один интерфейс к уже готовым классам. А в идеальном варианте я бы дописал что-то вроде
implementation IString for std::string {
char GetChatAt(int i) { return at(i); }
}
Здравствуйте, Кодёнок, Вы писали:
M>>А какие ещё отношения ты знаешь?
Кё>Например то, что в некоторых языках реализовано как трейты (классы типов):
Кё>Или отношение, которое сейчас принято реализовать паттерном прокси.
Угу. А ещё есть возможность динамически добавлять методы и поля (Python и прочие динамические языки), динамически менять супер-класс (наследование делегированием сообщений). Есть ещё возможность автоматического приведения типов (вроде view в Scala, или просто преобразования int->float в С).
Но они никак не помогут решить проблему Квадрат-Прямоугольник или Complex-Float-Integer. Первая — просто неправильно построенное наследование, и Квадрат с Прямоугольником должны, скорее, унаследоваться от Фигура (как и Треугольник, Многоугольник, Круг и пр.) — так же как это сделано для byte, int, long, float, double в процедурных языках — они независимы, а не образуют иерархию. А с набором независимых типов есть проблема, заключающаяся в необходимости их знать все заранее. Если ты добавишь к числам complex — то отгребёшь по самое нехочу. А потом ещё можно добавить BigInteger/BigFloat, с неограниченным количеством бит. А если complex и BigFloat добавлены независимо, отдельно друг от друга? Ведь надо-бы иметь BigComplex, а иначе вся система рушится — ведь чему будет равен результат умножения BigFloat * complex?
При фиксированной задаче мы можем знать набор требований, и можем выбрать иерархию Квадрат->Прямоугольник или Прямоугольник->Квадрат или Фигура->Прямоугольник|Квадрат — в зависимости от той модели, которую нам надо реализовать. Но эта модель будет всегда ограничена. А неограниченная (универсальная) модель автоматически приводит к проблеме несовместимости расширений (как complex*BigFloat).
И это не проблема ООП. В функциональщине те-же проблемы. Так называемая Expression Problem. Вот в Scala её Мартин Одерски предлагает решать как здесь. По моему, очень коряво. Но альтернативные решения не лучше
Здравствуйте, Кодёнок, Вы писали:
Кё>Это с какой стати? Вообще-то я моделирую предметную область с помощью средств языка программирования. С какого перепуга я должен забыть о предметной области?
Насколько я понял, Igor Trofimov говорит о том, что квадрат из геометрии (реального мира) не является объектом предметной области. Объект предметной области — квадрат с изменяющейся стороной.
Здравствуйте, Кодёнок, Вы писали:
Кё>Я ищу примеры классов объектов, которые вызывают трудности при объектном моделировании в современных ЯП типа C++ или C#. В качестве примера, я знаю всего две классические задачи:
Нужно определиться с тем, что именно означает иерархия структур в каждом случае: это специализация (субклассирование) или расширение (наследование).
К специализации применим LSP, к расширению — очевидно, нет.
Благодаря тому, что в обычных языках наследование как технический инструмент позволяет и специализировать, и расширять класс (перекрытие/перегрузка интерфейсных функций; надстройка layout'а и изменение семантики на корню), возникает прекрасная почва для путаницы в сознании.
В Блоге Труъ Программиста в августе была серия статей на тему.
Кё>Также буду рад услышать названия языков, которые попытались реализовать более богатый набор отношений, чем даёт классическое понимание объектно-ориентированного проектирования.
Haskell и O'Haskell. В последнем — вопросам классовой борьбы уделяется много внимания.
Здравствуйте, elmal, Вы писали:
E>Здравствуйте, Кодёнок, Вы писали:
Кё>>То есть предлагаешь иерархию использовать только для классификации их всех как "чисел", а автоматическую совместимость между ними реализовать через implicit conversion? E>В общем да, а вот в деталях может оказаться что и нет . От деталей реализации все зависит.
Кё>>А как решить проблему sqrt? E>А какая проблема с sqrt? sqrt(Number number) и должен возвращать набор чисел. sqrt(4) = 2 и -2 насколько я помню. И собственно когда мы его будем возвращать — эти числа должно быть наиболее адекватного типа. Ну а sqrt(-1) должен возвратить комплексное число, вроде оно наже одно (блин, уже школьную программу забываю — ужас).
Здравствуйте, Кодёнок, Вы писали:
Кё>... квадрат всегда является прямоугольником ...
Изменяемый квадрат не всегда является изменяемым прямоугольником. А в программировании в отличии от математики квадраты и прямоугольники как правило изменяемы.
Кё>>... квадрат всегда является прямоугольником ...
I>Изменяемый квадрат не всегда является изменяемым прямоугольником. А в программировании в отличии от математики квадраты и прямоугольники как правило изменяемы.
Кстати, интересный факт из геометрии — квадрат, является не только прямоугольником, но и ромбом, параллелограммом и четырехугольником, а также возможно и трапецией (здесь я не уверен). Так что, судя по всему, объектная иерархия может существенно запутаться
Забавное обсуждение
Автор привел пару примеров задач, решение которых в рамках традиционного ООП является затруднительным, и попросил подкинуть еще (т.е. добавить убедительности, по сути).
А ему в ответ все дружно начали объяснять, как эти примеры нужно переписать, чтобы затруднений не было (т.е. решать прямо обратную задачу).
Понятно, что можно все красиво переделать, оставаясь в рамках ООП. Или функциональщины. Или процедурного программирования. Понятно, что любой алгоритм можно переписать на ассемблере или на Smalltalk-е, было бы желание.
Но все это ни в малейшей степени не означает, что приведенные примеры не являются затруднительными для решения. Идеальный язык не будет навязывать свою парадигму мышления, он будет логично встраиваться в систему взглядов эксперта предметной области. Я так понимаю, что речь именно об этом.
M>Забавное обсуждение M>Автор привел пару примеров задач, решение которых в рамках традиционного ООП является затруднительным, и попросил подкинуть еще (т.е. добавить убедительности, по сути).
M>Но все это ни в малейшей степени не означает, что приведенные примеры не являются затруднительными для решения. Идеальный язык не будет навязывать свою парадигму мышления, он будет логично встраиваться в систему взглядов эксперта предметной области. Я так понимаю, что речь именно об этом.
Так ведь суть в том, что это не задачи, а вопросы из серии — что было раньше, курица или яйцо, или с какого конца надо разбивать яйцо? А ведь эти проблемы легкими для решения тоже не назовешь. Другой вопрос, стоит ли их решать?
Здравствуйте, Кодёнок, Вы писали:
Кё>1. Построить иерархию классов для прямоугольника и квадрата, с изменяющимися размерами. Трудность в том, что хотя квадрат всегда является прямоугольником (наследуется), он не захочет наследовать его реализацию (зачем ему раздельно хранить ширину и высоту?). Другая трудность в том, что прямоугольник иногда может являться квадратом, но классические отношения между классами, которые предлагают ЯП (наследование и implicit-conversion) не могут выразить такого отношения.
Одинаковый интерфейс, разные реализации (данные представлены по-разному).
Тут все просто.
public interface IRectangle
{
double Width { get; }
double Height { get; }
}
public class Rectangle : IRectangle
{
readonly double _width;
readonly double _height;
public double Width { get { return _width; } }
public double Height { get { return _width; } }
}
public class Square : IRectangle
{
readonly double _size;
public double Size { get { return _size; } }
public double Width { get { return Size; } }
public double Height { get { return Size; } }
}
M>Идеальный язык не будет навязывать свою парадигму мышления, он будет логично встраиваться в систему взглядов эксперта предметной области.
Ох как красиво сказал! Утопично, но как звучит!