Здравствуйте, Tilir, Вы писали:
BDA>> А ежели кто просит примеров для объяснения другим, я всегда сильно подозреваю недостаточное собственное понимание.
T>ЧСВ over 9000?
Нет, жизненный опыт. Один дурачок мне глаза открыл дюжину лет назад. Он мне доказывал что-то типа распространения бесплатного образования в Германии, весьма оригинальным способом: зашел на свой любимый трепный форум и попросил накидать примеров. Ему накидали ссылок на немецком, он их притащил мне — на, типа, переводи и читай сам. Доказано Занусси. Я офонарел от этого и впервые задумался, что у других мышление может отличаться. Для меня просить примеры для доказательства или объяснения — дико. Это показатель (не)владения темой, то есть, повод задуматься, откуда я взял те мысли, которые собираюсь доказывать или объяснять. С тех пор я этот паттерн то и дело встречаю в жизни. Но пока не доводилось у преподавателей.
T>Ни у кого, никогда не может быть достаточного понимания языка C++. Даже его создатель знает его на 6/10. Впрочем, учитывая дальнейшее...
Тема называется «Объясняем наследование». C++ я знаю балла на 3 из 10 и не стесняюсь в этом признаться. За точку отсчета в 10 баллов я беру Пашу Кузнецова, который стандарт знает назубок и одного чувака, который меряет for fun производительность метапрограммирования разных компиляторов. А вы не читаете, что вам пишут. Красной, как говорится, нитью, проходило у меня: не путайте наследование в C++ и наследование вообще. C++ полон чудес и legacy-ограничений.
Теперь я проверил то, что и подозревал: учить наследованию просто вы не собирались. Воспитываете еще одно поколение упертых и зашоренных плюсовиков.
T>Я, пожалуй, соберу ваши основные мысли в кучку, слитное чтение всего этого бугурта немного поднимает мне настроение:
Не все то бугурт, что воняет. Бывает просто банан критика.
BDA>> В одном древнем языке понятие интерфейса с его имманентным ограничением на отсутствие какой бы то ни было реализации отсутствует. BDA>> Язык слишком... либеральный. То в нем в интерфейсах реализация допускается, то неоткрытое наследование. Кто-то потом на подобной уродливости строит грязные хаки («потому, что могут», ага), а их дурацкий комитет потом не дает запретить. Так это и надо рассказывать. BDA>> Недометаданные в языке без базового класса Object выродились в уродливый костыль. Наследование тут не при чем. Нужда в нем обычно возникает, когда в реальной программе под Уиндоус приходится гонять через WinAPI объекты. BDA>> Это не академическое ковыряние в носу, извините. Это жизнь и зарабатывание денег. Чему я бы и учил в первую очередь.
T>В общем, с вами всё ясно. Вступать в диспуты с дотнетчиками здесь я считаю таким же оффтопом как и с джавистами. Интернет также полон ими. Ваш тон offensive enough, но и я со своей стороны презираю скриптоязыки для виртуальных машин и в общем согласен на взаимность. Посоветовать что-либо вы мне вряд ли сможете.
Я не дотнетчик. А шарп с явой не скриптоязыки, JFYI. Но вы мне открылись с новой стороны. Вот с этой: http://rsdn.ru/forum/job/5746297.1
T>Но не удержусь прокомментировать главный перл:
BDA>>Что касается деструктора, это обработчик события уничтожения, что на нем останавливаться? Что в нем такого особого?
T>Понимаете, виртуальный деструктор это то, что надо знать в первую очередь и как отче наш. Зачем он нужен, что будет если его не объявить и отнаследоваться и т.д.
Нет, не понимаю. Сделайте то же самое с обработчиком любого другого события — получите глюк. И этот (утечка ресурсов) даже не самый страшный. Самый страшный — порча памяти, затрагивающая vtbl. Вот где жопа, но на нее хитрый болт пока не придумали. GC, разве что.
>Но не в вашем случае, когда вы скриптуете виртуальную машинку и за вами всё чистит сборщик мусора, а в случае, если мы пишем на языке, где память это ресурс и им надо управлять.
Дотнетовский рантайм не виртуальная машинка. Офенсив офенсиву рознь. Потому, что вы уже начали искажать факты.
UPDATE: а впрочем, черт его знает. Все это майкрософтовский маркетинг, по большому счету. Продемонстрировать преимущество перед Джавой прямо с порога. Забираю назад предыдущий абзац.
Здравствуйте, Tilir, Вы писали:
T>Не к наследованию, а к вашему примеру наследования на примере List, Set, etc. Необходимость тянуть List от ICollection вызвана именно отсутствием нормальных шаблонов, о чём вам сразу и было сказано.
Я этого не заметил.
А что плохого, что они наследуются от Collection и Iterable?
Здравствуйте, Dufrenite, Вы писали:
... D>Самое простое, это объяснить на примере компьютерной игры.
D>Привожу пример из жизни:
D>1. D>class Actor;
D>2. D>class Character : public Actor; D>class Vehicle : public Actor; D>class Trigger : public Actor; D>class Door : public Actor;
D>3. D>class Player : public Character; D>class Enemy : public Character;
D>class Tank : public Vehicle; D>class Helicopter : public Vehicle;
D>...
Да, действительно просто. Только поясни — как с помощью этой иерархии найти все юниты, которые летают?
Здравствуйте, Dufrenite, Вы писали:
... 1>>Да, действительно просто. Только поясни — как с помощью этой иерархии найти все юниты, которые летают?
D>1. IVisitor D>2. virtual bool IsFlying() D>... D>1000. dynamic_cast<IFlyingUnit>()
Так ведь иерархия не используется. С таким же успехом можно обойтись вообще без наследования.
D>Дайте контекст — обсудим.
А я вот именно не это и намекаю — без контекста и понимания логики построение иерархии — мартышкин труд. В лучшем случае он пропадёт, а в худшем — приведёт к проблемам в будущем.
Здравствуйте, 1303, Вы писали:
1>Так ведь иерархия не используется. С таким же успехом можно обойтись вообще без наследования.
Я понял в чём дело. У меня так называемый data-oriented подход. Главное правильно разместить данные, а алгоритмы обхода делаются по мере надобности. Все другие способы построения иерархий классов, в частности с привязкой к реальному миру приводят к эпик-фейлам.
1>А я вот именно не это и намекаю — без контекста и понимания логики построение иерархии — мартышкин труд. В лучшем случае он пропадёт, а в худшем — приведёт к проблемам в будущем.
Я вам привёл пример как это делается в реальный проектах. В частности, эта иерархия мной взята (по памяти, возможно не совсем точно) из Unreal Engine 4.
Здравствуйте, Dufrenite, Вы писали:
... 1>>Так ведь иерархия не используется. С таким же успехом можно обойтись вообще без наследования.
D>Я понял в чём дело. У меня так называемый data-oriented подход. Главное правильно разместить данные, а алгоритмы обхода делаются по мере надобности.
Не понял — что значит правильно разместить данные.
1>>А я вот именно не это и намекаю — без контекста и понимания логики построение иерархии — мартышкин труд. В лучшем случае он пропадёт, а в худшем — приведёт к проблемам в будущем.
D>Я вам привёл пример как это делается в реальный проектах. В частности, эта иерархия мной взята (по памяти, возможно не совсем точно) из Unreal Engine 4.
Ну вот а в другом реальном проекте будет совершенно иное. Но предлагаю тут с этим закругляться, тема про проблемы ООП где-то рядом была.
Здравствуйте, Куть, Вы писали: К>Эту иерархию классов можно использовать в написании простенького графического редактора.
Начинается...
Нельзя! К>На примере фигур можно показать, что код с наследованием получается проще и понятнее: К>1. Можно нарисовать все фигуры, пробежав по списку указателей std::list<Figure *> и вызывая виртуальный метод draw(). К>2. Можно найти сумму площадей в пару строчек. К>3. Можно сохранить данные всех фигур в файл, заодно можно рассмотреть проблему с чтением из файла и её решение в виде фабричного метода.
К>А перед этим можно показать пример, где часть этих задач решается без наследования, с помощью длинных switch(type) или указателей на функции.
А после этого можно показать пример, где все эти задачи решаются без наслежования и без switch, и без указателей на функции.
Вменяемый инженер всё сделает через примитив Path, состоящий из прямых, арок, и кривых Безье, а все остальные "фигуры" будут просто конструкторами Path с различными аргументами.
Потому, что все ваши "простые и понятные" примеры сдохнут сразу же, как я попрошу вас реализовать мне операцию Intersect для двух фигур.
К>И добить добавлением нового типа фигуры. С наследованием будет добавлен один класс (изменения локализированы), а без наследования изменения будут размазаны по всей программе.
А без наследования все изменения сведутся к написанию ещё одной функции.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Tilir, Вы писали:
T>Но весь этот план требует некоего сквозного примера для которого проводить объяснения. И вот тут у меня затык.
T>Все книги по C++ которые объясняют наследование вызывают желание убить себе лицо рукой.
Молодец! Никого не слушай — всё правильно думаешь.
Я бы начал вот с чего:
1. Объяснил бы концепцию полиморфизма вообще. На примере, сначала, одной функции. Показываем, как можно добиться интересных эффектов путём параметризации алгоритма функцией. Объясняем, что это в жизни ещё очень пригодится, и что одного этого приёма хватает в очень многих случаях.
Пример — поиск в массиве.
Развитие примера — поиск в дереве. Пока что даже нет нужды вводить какие-либо классы; дерево вполне может быть plain struct. Главная идея — объяснить, зачем вообще разделять код итерирования по дереву и код предиката: на переборе массива неочевидно, что find(array, less_or_equal_to_5()) лучше, чем for(int i=0;i<arraayLen; i++) if array[i]<=5 { res = i; break;}.
Здесь как бы уже возникает дихотомия между интерфейсом (сигнатурой функции) и реализацией — реальной функцией.
2. Объяснил бы тот момент, когда придётся отказаться от функции, и перейти к объектам. Например, хранение состояния — например, агрегирующая функция. Типа как нам подсчитать сумму/среднее/минимум/максимум элементов в массиве/дереве/и т.п?
Теперь у нас есть интерфейс IAggregator (c одним методом), и возможность реализовать разные алгоритмы агрегирования.
3. Расширил бы пример с введением интерфейса из нескольких функций, которые должны работать согласованно. Например, с умножением матриц — если мы берём пару из (*, +), то получаем классическое перемножение матриц. А если берём пару из (+, min), то мы находим кратчашие пути при умножении матрицы смежности самой на себя.
4. В качестве домашнего задания предложил бы реализовать класс "калькулятора" — т.е. штуки, которая умеет вести вычисления с плавающей запятой. При этом надо сделать две реализации — одну "быструю", с табличной реализацией тригонометрии и загрублённой арифметикой с бит-шифтами, другую "точную", через сопроцессор. И сравнить результаты решения одной и той же численной задачи с использованием двух таких калькуляторов.
5. Напоследок бы сказал, что ситуация, когда используется более одного уровня наследования — экзотика, которая скорее всего им в жизни потребуется очень-очень нескоро. И что когда кажется, что есть повод такую иерархию ввести (например, отнаследовавшись от класса, реализующего интерфейс), надо сначала посмотреть, нет ли способа решить задачу без наследования.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Вы всё правильно говорите, и про intersect() я сам думал — писать или нет. Видимо надо было написать.
Ключевое слово — "простенький" редактор. Такой, в котором не нужно искать пересечения и использовать математику сложнее школьной для расчета площадей фигур.
Плюс графического редактора — наследование можно объяснить в терминах, понятных последним двоечникам. Тогда как понятия "кривая Безье" или "агрегатор" могут вообще ничего не говорить половине студентов.
Ваше предложение ниже мне однозначно нравится больше, но вариант с редактором тоже имеет право на существование.
Здравствуйте, Tilir, Вы писали: T>У меня тут есть немного студентов на нашей базовой кафедре, которых надо научить плюсам в свободное от работы время. Пока всё шло хорошо, но скоро надо будет объяснить им наследование. Пока план примерно такой:
Наследование идет от АТД (Абстрактный Тип Данных), под наследование можно загнать все что угодно.
Например мебель. Бизнес область домашний интерьер под ключ. Визуализация транспортировка и сборка.
Есть базовый интерфейс мебель. Например компания по обстановке мебели в квартире.
ЭлементМебели{атрибуты: размеры, вес, хрупкий, занимаемый объем в пространстве} абстратный
стол|стул|шкаф|лежак — наследники ЭлементМебели тоже абстрактные типы, добавляют свои атрибуты — специфицируют базовый интерфейс.
Каждый элемент можно расширить своим списком свойств у стола количество персон, у стула например мягкий-жесткий материал сидения, у шкафа количество дверей, наличие антресоли, количество полок. Лежак: диван, кровать, уголок. Уголок может состоять из дивана и кресла (композит) теоретически можно собирать как конструктор, переопределением может служить количество персон диван:2 кресло:1, в сложенном состоянии могут сесть не (1+2)=3, а 4 персоны. Множественное наследование это парта — и стол и стул конструктивно неделимо. Дверь/ящик/матрас не является элементом мебели. Например шкаф-дверь это композиция(шкаф без двери уже хз вроде как не шкаф), а шкаф-полка агрегация(т.к. количество полок можно варьировать ну и без полок шкафы тож бывают).
Добавляем динамики двери можно пооткрывать, ящики выдвигать, диван раскладывать. При действии совершаемой над мебелью меняется объем занимаемого пространства. Если добавить декораторы к мебели: колесики или светодиодная подсветка, то мебель можно покатать и включить свет.
Контейнер мебели комната. Если надо показать именно виртуализацию, то можно над этими объектами совершать различные манипуляции по разному. Например подготовка к показу: закрыть все двери и задвинуть все ящики. Или например подготовка ко сну: включить все что включается, разложить диван, закрыть все дверцы шкафов и задвинуть ящики.
Здравствуйте, Куть, Вы писали:
К>Вы всё правильно говорите, и про intersect() я сам думал — писать или нет. Видимо надо было написать. К>Ключевое слово — "простенький" редактор. Такой, в котором не нужно искать пересечения и использовать математику сложнее школьной для расчета площадей фигур. К>Плюс графического редактора — наследование можно объяснить в терминах, понятных последним двоечникам. Тогда как понятия "кривая Безье" или "агрегатор" могут вообще ничего не говорить половине студентов.
Опасность этого эксперимента — в том, что многие и до седин упорно считают, что именно так и надо писать графические редакторы.
Получается какой-то парадокс: мы берём концепцию, которая должна помогать нам решать сложные задачи, и внезапно оказывается, что всё наоборот — для синтетической упрощённой задачи она ещё хоть как-то подходит, а стоит задачу чуть усложнить — всё, приплызд, отматывай назад. Вся диаграмма классов летит в корзину, объём кода сокращается в шесть раз, функциональность вырастает в два.
Я, будучи на месте вот такого "студента", почувствовал себя, мягко говоря, обманутым в момент осознания.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Dufrenite, Вы писали:
... 1>>Не понял — что значит правильно разместить данные.
D>Термин "декомпозиция" вам знаком?
В общих чертах. Правда, я не уверен, что это как-то связано с наследованием.
Здравствуйте, Dufrenite, Вы писали:
D>Я понял в чём дело. У меня так называемый data-oriented подход. Главное правильно разместить данные, а алгоритмы обхода делаются по мере надобности. Все другие способы построения иерархий классов, в частности с привязкой к реальному миру приводят к эпик-фейлам.
Вот тут опять есть нюансы. На моей памяти, попытка выстроить иерархию наследования на основе данных как раз и приводит к эпик фейлам.
Классический пример — квадрат/прямоугольник/эллипс/круг. Вроде бы и про данные всё понятно, а иерархия никак не вытанцовывается.
Единственный понятный мне способ ОО-проектирования — это плясать от поведения.
Ну, то есть решаем задачу в стиле "ок, вот у нас есть идеальный "решатель задачи" для каждой сформулированной нами задачи. Давайте посмотрим, можно ли выстроить какую-то классификацию этих решателей; можно ли выделить из их ответственностей какие-то фрагменты, которые сделают общее решение проще и понятнее".
Важный момент — речь идёт про "решатель задачи", а не просто про "класс для представления сущности из предметной области".
Например, если у нас речь шла про геометрические фигуры, то "задача" — это типа "нарисовать на экране прямоугольник", и решатель, соотвественно — это "рисователь прямоугольников на экране", а вовсе не "класс Прямоугольник с методом Draw".
В результате такой декомпозиции мы быстро получим "рисователя отрезков на экране" и "рисователя ломаных на экране", а рисователи прямоугольников, треугольников, квадратов и прочих фигур исчезнут за ненадобностью.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Dufrenite, Вы писали:
D>>Я понял в чём дело. У меня так называемый data-oriented подход. Главное правильно разместить данные, а алгоритмы обхода делаются по мере надобности. Все другие способы построения иерархий классов, в частности с привязкой к реальному миру приводят к эпик-фейлам.
S>Вот тут опять есть нюансы. На моей памяти, попытка выстроить иерархию наследования на основе данных как раз и приводит к эпик фейлам. S>Классический пример — квадрат/прямоугольник/эллипс/круг. Вроде бы и про данные всё понятно, а иерархия никак не вытанцовывается.
Согласен, если рассматривать иерархию как "вещь в себе". А если рассмотреть её в контексте архитектурного паттерна, например MVC, то это имеет смысл.
В этом случае я бы спроектировал следующие взаимодействующие иерархии:
1. Model:
class Shape;
class Square : public Shape;
class Rectangle : public Shape;
...
1.1 Visitor:
struct IShapeVisitor;
2. View:
class ShapeDrawer : private IShapeVisitor;
class ShapeNetworkSender : private IShapeVisitor;
...
3. Controller:
class CanvasController;
...
S>Единственный понятный мне способ ОО-проектирования — это плясать от поведения. S>Ну, то есть решаем задачу в стиле "ок, вот у нас есть идеальный "решатель задачи" для каждой сформулированной нами задачи. Давайте посмотрим, можно ли выстроить какую-то классификацию этих решателей; можно ли выделить из их ответственностей какие-то фрагменты, которые сделают общее решение проще и понятнее".
К сожалению, я сталкивался с ситуациями, когда попытки построить иерархию на основе поведения приводили к разрастанию иерархии до безумных размеров с совершенно головоломной логикой наследования.
Если же отделить "мух от котлет", то есть данные от поведения, то можно получить минималистичные и чётко сфокусированные иерархии, не имеющие абсолютно ничего лишнего.
S>Важный момент — речь идёт про "решатель задачи", а не просто про "класс для представления сущности из предметной области". S>Например, если у нас речь шла про геометрические фигуры, то "задача" — это типа "нарисовать на экране прямоугольник", и решатель, соотвественно — это "рисователь прямоугольников на экране", а вовсе не "класс Прямоугольник с методом Draw". S>В результате такой декомпозиции мы быстро получим "рисователя отрезков на экране" и "рисователя ломаных на экране", а рисователи прямоугольников, треугольников, квадратов и прочих фигур исчезнут за ненадобностью.
Можно и к этому придти, но я предпочитаю базировать слой более специфичных классов на слое более общих.
В этом случае дальнейшее развитие нашего модельного примера могло бы выглядеть как-то так:
2. View:
class ShapeDrawer : private IShapeVisitor;
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Dufrenite, Вы писали:
S>Единственный понятный мне способ ОО-проектирования — это плясать от поведения.
И в результате данные и поведение перемешиваются в винегрет, т.к. как типы данных сильно реже меняются, нежели чем алгоритмы их обработки, теперь я понимаю почему вы мне поставили минус Учите мат. часть и ваши программы станут шелковистее.
Классическая иерархия Figure.Draw нарушает SRP. Когда мы делали рисовалку на DirectX, то базовый объект у нас выдавал массив объектов которые надо отрисовать, рисовальщик был отдельно, т.к. алгоритм отрисовки и её необходимость не зависит от того когда объект рисуется.
Здравствуйте, diez_p, Вы писали: S>>Единственный понятный мне способ ОО-проектирования — это плясать от поведения. _>И в результате данные и поведение перемешиваются в винегрет, т.к. как типы данных сильно реже меняются, нежели чем алгоритмы их обработки,
Не вижу, как вы сделали переход от посылки к результату.
_>Классическая иерархия Figure.Draw нарушает SRP. Когда мы делали рисовалку на DirectX, то базовый объект у нас выдавал массив объектов которые надо отрисовать, рисовальщик был отдельно, т.к. алгоритм отрисовки и её необходимость не зависит от того когда объект рисуется.
Совершенно верно. Вы пришли к правильному решению задачи. Внезапно оказалось, что вместо развесистой иерархии фигур вы получили фиксированный набор примитивов.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Dufrenite, Вы писали: D>Согласен, если рассматривать иерархию как "вещь в себе". А если рассмотреть её в контексте архитектурного паттерна, например MVC, то это имеет смысл.
Вот это меня опять настораживает. Я имею в виду — ввод паттернов на ровном месте.
D>В этом случае я бы спроектировал следующие взаимодействующие иерархии:
D>1. Model: D>class Shape; D>class Square : public Shape; D>class Rectangle : public Shape; D>...
D>1.1 Visitor: D>struct IShapeVisitor;
D>2. View: D>class ShapeDrawer : private IShapeVisitor; D>class ShapeNetworkSender : private IShapeVisitor; D>...
D>3. Controller: D>class CanvasController; D>...
Ух ты ж блин! Сколько всего. И всё это вместо пары классов Path и Canvas.
Круто, чё.
D>К сожалению, я сталкивался с ситуациями, когда попытки построить иерархию на основе поведения приводили к разрастанию иерархии до безумных размеров с совершенно головоломной логикой наследования.
Вынужден поверить вам на слово, хотя сам такого не видел.
D>Если же отделить "мух от котлет", то есть данные от поведения, то можно получить минималистичные и чётко сфокусированные иерархии, не имеющие абсолютно ничего лишнего.
Тут — согласен. Как раз чрезмерное увлечение слиянием данных с поведением и является обычной причиной чудовищных ОО-решений.
D>В этом случае дальнейшее развитие нашего модельного примера могло бы выглядеть как-то так:
D>2. View: D>class ShapeDrawer : private IShapeVisitor;
D>4. Render: D>class LineRenderer D>{ D>public: D> void DrawLines(const std::vector<Point>& points, const Color& color); D>};
А какой код и сколько нам потребуется в ShapeDrawer? Я же правильно понял, что в нём будет по методу на каждого потомка Shape?
Простите, но такой код хорош только для списания затрат по time spent basis.
Для реальной работы надо минимизировать усилия. В вашем коде стоимость добавления новой фигуры — запредельна.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.