Форум
Философия программирования
Тема
Как правильно задавать вопросы
B
I
abc
U
X
3
X
3
H1
H2
H3
H4
H5
H6
Asm
C/C++
C#
Erlang
Haskell
IDL
Java
Lisp
MSIL
Nemerle
ObjC
OCaml
Pascal
Perl
PHP
Prolog
Python
Ruby
Rust
SQL
VB
Здравствуйте, Геннадий Васильев, Вы писали: ГВ>Здравствуйте, igor-booch, Вы писали: ГВ>>>Правильно понял, но это справедливо только при условии, что наследование (inheritance) типов в данной программной системе действительно выражает отношение "тип-подтип" в том смысле, который подразумевает LSP. Обычно полагают, что так оно и есть, но в отдельных случаях это правило может нарушаться. IB>>Приведите пожалуйста примеры неправильного и правильного наследования сточки зрения LSP. ГВ>С точки зрения LSP нет никакого "наследования". Есть отношение "тип-подтип", а воплощено оно наследованием или нет - не имеет никакого значения. Главное - сформулировать набор требований к соответствующим сущностям. То есть прямо сформулировать, а не надеяться на "само собой получится, если мы правильно постигнем дзен". Не получится, не надейтесь. ГВ>Итак, записываем набор требований к сигнатурам и поведению соответствующих типов. Для примера возьмём набившую оскомину задачу рисования кругов и прочих треугольников. Обобщать мы их будем в виде некоторых графических примитивов, которые обозначим как сущность Shape. ГВ>Что должен уметь графический примитив? В общем-то, ничего, если не определён контекст его использования. Значит, мы должны разобраться с контекстом. Итак, в контексте: ГВ>- Некоторое "Графическое пространство" (Canvas); ГВ>- Модель представления координат (X-Y); ГВ>- Цветовое пространство (Palette, Color); ГВ>- "Микропримитивная" операция: putPixel(Canvas, Color, Coordinate x, Coordinate y). ГВ>Вот теперь, в этом наборе символов начинаем игру в графические примитивы. Чего мы хотим от графического примитива? Допустим, что мы хотим, чтобы он умел себя нарисовать в заданной базовой точке на заданном холсте. Получаем первую формальную запись требований к такому объекту (здесь и далее - псевдокод): ГВ>[code]void draw(Shape shape, Canvas canvas, Coordinate X, Coordinate Y);[/code] ГВ>Вторая формальная запись относится к цветам и прочей колористике. На данный момент нам безразлично, каким именно цветом будет рисовать себя примитив, главное, чтобы он не выходил за рамки дозволенного, поэтому сделаем так: ГВ>[code]void setPalette(Shape shape, Palette palette); ГВ>Palette getPalette(Shape shape);[/code] ГВ>Прикидываем одно место к носу и замечаем, что язык у нас объектно-ориентированный, значит, общий для всех параметр shape можно вынести в "класс", то есть: ГВ>[code]class Shape { ГВ>void draw(Canvas canvas, Coordinate X, Coordinate Y); ГВ>void setPalette(Palette palette); ГВ>Palette getPalette(); ГВ>}[/code] ГВ>Но и это ещё не всё. Скорее всего Canvas тоже будет меняться не слишком часто, да и о базовой точке мы тоже, вполне вероятно, захотим получать какую-никакую справку, а менять её потребуется нечасто. Значит, получаем вот такой набор функций (впрочем, это уже не функции, а методы класса, и всё в сумме вполне похоже на некий интерфейс) : ГВ>[code]class Shape { ГВ> void setCanvas(Canvas canvas); // Выносим отдельно управление Canvas ГВ> Canvas getCanvas(); // Должно выполняться условие: s.setCanvas(c); s.getCanvas() == c; Что положили, то и вытащили. ГВ> void setBasePoint(Coordinate X, Coordinate Y); ГВ> Coordinate getBasePointX(); ГВ> Coordinate getBasePointY(); ГВ> void draw(); // draw растерял все свои аргументы ГВ> void setPalette(Palette palette); ГВ> Palette getPalette(); ГВ>}[/code] ГВ>Получив такой интерфейс, мы можем заняться разработкой системы рисования, не имея ещё ни одного примитива. Опускаю промежуточные рассуждения, сразу приведу примерно то, что получается: ГВ>[code]class Canvas { ГВ> void addShape(Shape); ГВ> void drawAll(); // Рисуем всё, что накопилось ГВ> void beginDraw(); ГВ> void endDraw(); ГВ> void putPixel(Color c, Coordinate X, Coordinate Y); ГВ>private: ГВ> Shape m_allShapes[]; // Не суть важно, как именно выглядит этот массив. Главное - что это одномерный массив. ГВ>} ГВ>[/code] ГВ>Собственно, дальше пирожки врозь - детализация Canvas будет проводиться отдельно, а детализация Shape - отдельно. Отмечу один момент: Canvas содержит два метода - beginDraw и endDraw. Эти два метода должны вызываться "фигурой" в начале и конце цикла рисования соответственно. Соответствующее требование вносится в список требований Shape: ГВ>[code]class Shape { ГВ> ... ГВ> // requires: Canvas.beginDraw / Canvas.endDraw call. ГВ> void draw(); ГВ> ... ГВ>} ГВ>[/code] ГВ>Пока что требование записано неформально. Чтобы привести его к формализованному виду, создадим некий базовый класс: ГВ>[code]class BasicShape : public Shape { ГВ> void setCanvas(Canvas canvas) { m_canvas = canvas; } ГВ> Canvas getCanvas() { return m_canvas; } ГВ> // Аналогично воплощаем здесь управление палитрой и базовыми точками. ГВ> void draw() { ГВ> m_canvas.beginDraw(); ГВ> drawPixels(); ГВ> m_canvas.endDraw(); ГВ> } ГВ> void drawPixels(); // Этот метод предназначен для перекрытия классами-наследниками BasicShape, ГВ> // Canvas про него ничего не знает ГВ>protected: ГВ> Canvas m_canvas; ГВ>}[/code] ГВ>Теперь можно переходить к кругам-треугольникам и прочим примитивам: ГВ>[code] ГВ>class Bar : public BasicShape { ... }; ГВ>class Circle : public BasicShape { ... }; ГВ>class Triangle : public BasicShape { ... }; ГВ>class Polygon : public BasicShape { ... }; ГВ>class ShapeAdapter : public Shape { ... }; ГВ>class SomethingSmart { ГВ> Shape anchorShape() { return m_shapeAdapter; } // Можно и так сделать ГВ>protected: ГВ> ShapeAdapter m_shapeAdapter; // Этот объект каким-то загадочным способом связывает интерфейс Shape и методы класса SomethingSmart ГВ>}; ГВ>[/code] ГВ>Как ты понимаешь, нам совершенно не важно, как именно технически связан интерфейс Shape с его реализацией. Наследование, агрегация, ещё как-нибудь. Важно, чтобы поведение реализации соответствовало [i]требованиям[/i], предъявляемым к Shape. Тогда мы автоматически удовлетворяем Liskov Substitution Principle, соответствующие классы находятся в отношении "тип-подтип" (т.е. - являются подтипами типа Shape) и у нас не возникает противоречий при чтении инструкции: ГВ>[code]class Bar : public BasicShape { ... };[/code] ГВ>Здесь наследование реализует отношение "тип-подтип" в LSP-смысле этого слова. Сие интуитивно понятно и не вызывает вопросов. ГВ>Сложности начнутся, если кому-то стукнет в голову, например, "сэкономить" на наследовании от BasicShape, вместо этого перенеся соответствующий код в Canvas: ГВ>[code] ГВ>class CrazyShape : public Shape { // Наследование очень похоже на предыдущее... ГВ> void draw() { ГВ> m_canvas.putPixel(0, 1, 1); // ...но только внешне. ГВ> } ГВ>} ГВ>// В Canvas придётся добавить такую фишку: ГВ>class Canvas { ГВ> void drawAll() { ГВ> ... ГВ> if (typeOf(someShape) == CrazyShape) { ГВ> // Для этого идиота придётся делать исключение ГВ> beginDraw(); ГВ> someShape.draw(); ГВ> endDraw(); ГВ> } else { ГВ> // Остальные - в норме ГВ> someShape.draw(); ГВ> } ГВ> } ГВ>} ГВ>[/code] ГВ>В данном случае формальная часть, соблюдение которой требуется компилятором, вполне удовлетворена: новый класс унаследован от интерфейса Shape и экземпляр CrazyShape может быть подставлен туда, где должен использоваться Shape. А вот остальная часть контракта, которая была реализована в BasicShape - нарушена. Вместе с ней нарушен LSP, поскольку новый класс (внимание!) не обладает [i]свойством[/i]: "вызывает пару beginDraw()/endDraw()". То есть не смотря на "правильное" наследование класс CrazyShape [b]не является LSP-compliant подтипом[/b] класса Shape. ГВ>Это нюанс, о котором часто забывают в пылу споров о Глубоком Смысле того или иного сугубо синтаксического приёма. Например, часто ломают копья по вопросу о том, является ли public- (protected, private) -наследование [i]реализацией[/i] отношения "тип-подтип" или нет. Проблема состоит в том, что синтаксическая конструкция "наследование" не является сама по себе ничем таким, что позволяло бы сделать вывод о более общих характеристиках наследуемых классов. LSP-compliant отношение "тип-подтип" можно как соблюсти с использованием private-наследования, так и нарушить при public-наследовании. То есть подобные споры, в общем-то, есть споры ни о чём. ГВ>Возвращаясь к твоему вопросу: с точки зрения LSP не бывает правильного и неправильного наследования, поскольку LSP определён на более общих характеристиках, нежели наследование. Единственная причина, по которой в современных языках программирования иной раз необходимо пользоваться наследованием для соблюдения LSP состоит в том, что компилятор разрешит подстановку класса B на место класса A только в том случае, если B является наследником A. Но повторюсь, само себе это никак не гарантирует соблюдения LSP, то есть наследование - это условие нередко необходимое, но отнюдь не достаточное. Например типы, используемые в качестве аргументов шаблона могут просто содержать методы с нужными сигнатурами, при этом наследниками какого-то типа они быть вовсе не обязаны. И с другой стороны, помним о таком приёме: ГВ>[code]class ShapeAdapter : public Shape { ... }; ГВ>class SomethingSmart { ГВ> Shape anchorShape() { return m_shapeAdapter; } // Можно и так сделать ГВ>protected: ГВ> ShapeAdapter m_shapeAdapter; // Этот объект каким-то загадочным способом связывает интерфейс Shape и методы класса SomethingSmart ГВ>}; ГВ>[/code] ГВ>Здесь SomethingSmart не унаследован прямо от Shape, но тем не менее, посредством m_shapeAdapter и метода anchorShape() он может быть использован там, где ожидается Shape. Отличие такой агрегации от наследования становится пренебрежимо малым, если учесть, что создание класса часто упаковывается в "фабрику": ГВ>[code]Shape createShape(string typeName) { ГВ> if (typeName == "SomethingSmart") { ГВ> SomethingSmart obj = new SomethingSmart(...); ГВ> return obj.anchorShape(); ГВ> } ГВ> ... ГВ>}[/code] ГВ>Так что, наследование - это только один из возможных приёмов.
Теги:
Введите теги разделенные пробелами. Обрамляйте в кавычки словосочетания с пробелами внутри, например:
"Visual Studio" .NET
Имя, пароль:
Загрузить
Нравится наш сайт?
Помогите его развитию!
Отключить смайлики
Получать ответы по e-mail
Проверить правописание
Параметры проверки …