Здравствуйте, SV., Вы писали:
Z>>LSP затрагивает не столько публичный интерфейс иерархии классов, сколько внешние методы с ней работающие. Именно они должны работать корректно с любым потомком. Если метод принимает на вход Printer, он должен точно так же корректно работать если ему передали ColorPrinter или MatrixPrinter.
SV.>Ослу понятно, что если конструкторы будут различаться (требовать разных инициализационных данных — connection string / console), то и методы, которые инстанцируют логгеры, тоже будут различаться.
SV.>Вопрос в чистом виде такой: классы, которые дают возможность одинаково себя использовать, но требуют по-разному порождать — удовлетворяют LSP или нет? Я для себя ответил так: It's OK.
Не совсем правильная постановка вопроса, потому что ты здесь упускаешь из виду, что LSP формулируется относительно некоторого базового типа, соответственно, классы могут удовлетворять LSP тоже только по отношению к какому-то базовому типу. Поясню на примере.
class Shape {
public:
virtual void draw() = 0;
virtual void setCanvas(Canvas *) = 0;
};
class Circle : public Shape {
public:
Circle(Point center, int radius);
void draw();
void setCanvas(Canvas *);
};
class Line : public Shape {
public:
Line(Point begin, Point end);
void draw();
void setCanvas(Canvas *);
};
Предположим для ясности, что реализации draw и setCanvas соответствуют всем соглашениям и т.п. Так вот, базовый тип (Shape) не содержит определения конструктора, следовательно способ порождения его наследников не должен учитываться при оценке LSP-compliancy. Собственно, наследники могут вводить ещё туеву хучу своих собственных методов по необходимости, но если соглашения Shape при этом не нарушаются, то иерархия остаётся LSP-compliant для базового Shape.
Конструкторы могут быть включены в оценку LSP-compliancy только в том случае, если язык программирования допускает наследование конструкторов и при использовании требуется определённая сигнатура конструктора. Иначе они всегда будут относиться к одному-единственному типу и вопрос о об LSP-compliant наследовании не стоит. Тут ещё есть такой парадокс, что имя конструктора нередко должно совпадать с именем типа, а значит, в точке использования конструктора почти всегда будет указан конкретный тип и вопрос о наследовании снова отпадает сам собой. Но если языки или технология позволяют выбрать тип по содержимому переменной (GUID, строковое имя, ссылка на метакласс...) и потом вызвать конструктор некоторым унифицированным образом — тут да, конструктор может быть включён в оценку LSP-compliancy. Вот такая вот путаница.
Кстати, если внимательно прочесть твой вопрос:
классы, которые дают возможность одинаково себя использовать, но требуют по-разному порождать
...то в нём можно найти скрытое противоречие: "одинаково использовать" vs. "по-разному порождать". Из этого противоречия следует, что порождение находится за пределами "использования" и в оценке LSP-compliancy, таким образом, автоматически не участвует. Так что, да, It's OK. Definitely.
Собственно, вне контекста использования (а базовый тип — это своеобразная квинтэссенция способов использования наследников) рассуждать об LSP-compliancy типов вообще нельзя. Уж коль скоро созданы разные классы, то хоть в чём-то они будут отличаться, а значит почти всегда можно найти такую точку зрения, с которой наследники окажутся нарушающими LSP, другое дело — надо ли её искать.
SV.>Пример с логгерами приведен. Другой пример: простенький векторный редактор. Вы щелкаете по кнопке "Рисую прямоугольник". Window [Controller + View] запоминает режим и в соответствии с ним показывает вам рамку после первого щелчка. После отпускания кнопки мыши отрабатывает switch, и в класс Document [Model] уходит команда "Новый прямоугольник". Тот инстанцирует Rect, добавляет его в коллекцию фигур, и только после этого разница между ним и каким-нибудь кругом исчезает.
Угу, верно. С одним "но". Разница запросто может исчезнуть гораздо раньше, если вместо "режима нового прямоугольника" рисование предварительной фигуры будет возложено на соответствующий класс. То есть рамку рисует Rect, овал изображается классом Ellipse и т.п. Ну а то, как эти классы будут пользоваться мышкой и какие параметры хранить — это уже их личное дело.
SV.>Если есть возражения по сути, welcome.
Да возражений особо нет, так — уточнения.
Я знаю только две бесконечные вещи — Вселенную и человеческую глупость, и я не совсем уверен насчёт Вселенной. (c) А. Эйнштейн
P.S.: Винодельческие провинции — это есть рулез!
У типов и у функций есть контракты. По сути это некие ограничения, накладываемые на них. Часть из них выражается явно и может быть проверена компилятором. Часть — не может быть выражена в рамках используемого языка программирования.
Для примера возьмем C#.
Если под контрактом типа подразумевать только набор сигнатур его методов и игнорировать эту неявную составляющую контракта, то любое наследование автоматом станет удовлетворять LSP и никакого смысла в этом принципе не будет.
В контракт функции входит, в частности, ее имя, типы ее параметров и тип возвращаемого значения. Проверка на соответствие передаваемых ей параметров ее контракту осуществляется статически на этапе компиляции.
class SomeBaseType
{...}
class SomeConcreteType: SomeBaseType
{...}
class SomeConcreteType2: SomeBaseType
{...}
void SomeFunc(SomeBaseType x)
{
//...
((SomeConcreteType)x).SomeMethod();
//...
}
Вопрос: что является контрактом этой функции и правильно ли он выражен синтаксически?
При передаче ей SomeConcreteType2 будет выброшен InvalidCastException. Если считать это ожидаемым поведением функции SomeFunc, то нарушения LSP нет.
Если считать это ошибочным поведением, то контракт функции указан некорректно — ее параметр должен иметь тип SomeConcreteType. Просто проверка компилятором была подавлена.
class SomeConcreteType3: SomeBaseType
{...}
void SomeFunc(SomeBaseType x)
{
//...if(x is SomeConcreteType)
{
((SomeConcreteType)x).SomeMethod();
}
else
{
((SomeConcreteType2)x).SomeMethodOfType2();
}
//...
}
Тут контрактом на параметр будет, что он не null и имеет тип SomeConcreteType или SomeConcreteType2. Такое условие невыразимо на C#, т.е. это неявный контракт.
Тут BadSort противоречит неявному контракту — он не делает сортировку.
В каких-то языках можно больше выразить явно, в каких-то меньше. В языках с зависимыми типами, например, можно даже требование сортировки выразить явно и BadSort вообще не скомпилируется...
Здравствуйте, igor-booch, Вы писали:
W>>Вы, когда наследуетесь от предка, специализируете поведение будущего объекта или, наоборот, обобщаете?
IB>специализирую
Вот Вам и ответ!
Здравствуйте, LaptevVV, Вы писали:
LVV>Кстати о "правильности" наследования: LVV>
LVV> спецификация. Дочерний класс реализует поведение, описанное в родительском классе. Ясно, что в С++ эта форма реализуется простым открытым наследованием от абстрактного класса;
LVV> конструирование. Класс-наследник использует методы базового класса, но не является его подтипом (принцип подстановки не выполняется). В С++ такую форму можно реализовать простым закрытым наследованием;
LVV> расширение. В класс-потомок добавляют новые методы, расширяя поведение родительского класса; принцип подстановки в такой форме выполняется;
LVV> обобщение. Дочерний класс обобщает поведение базового класса. Обычно такое наследование используется в тех случаях, когда мы не можем изменить поведение базового класса (например, базовый класс является библиотечным классом);
LVV> ограничение. Класс-наследник ограничивает поведение родительского класса. Очевидно, что в С++ такой вид наследования реализуется простым закрытым наследованием (пример — TUniversalDeque -> TStack );
LVV> варьирование. Базовый класс и класс-потомок являются вариациями на одну тему, однако связь «класс-подкласс» произвольна, например, «квадрат-прямоугольник» или «прямоугольник-квадрат». Эта форма фактически не отличается от «конструирования», так как класс-наследник, очевидно, «использует методы базового класса, но не является его подтипом»;
LVV> комбинирование. Дочерний класс наследует черты нескольких классов — это множественное наследование.
Что только народ не придумает, чтобы оправдать использование наследования не по назначению?!
А про возможность внутренние поля объявлять этот уважаемый автор не слышал?
ЗЫ
У меня иногда складывается впечатление, что ООП придумали для того, чтобы его использовали не по назначению.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, igor-booch, Вы писали:
IB>Предположим у нас есть класс SimpleLogger, у него виртуальный метод Log(string message), который просто выводит сообщения на консоль. IB>Есть класс DbLogger, который наследуется от SimpleLogger. В DbLogger перекрыт метод Log(string message) и добавлена функциональность логирования сообщения в БД. IB>Теперь меняем в программе SimpleLogger на DbLogger, опаньки, а баз данных то нет, комп от сети отключен, даже connection string нигде не указан. Логирование не работает.
LSP требует, что-бы контракты aka соглашения не ломались при смене реализации, в данном примере, если класс SimpleLogger гарантирует что сообщение будет записано всегда и никаких исключений брошено не будет(лог всегда доступен для записи), то класс DbLogger тоже должен это гарантировать, поэтому из принципа LSP следует, что класс DbLogger очень нетривиальная штука. Во первых, он должен как либо инициализироваться перед использованием и кидать свои исключения во время инициализации, если например не может подключиться к БД. Во вторых, он должен гарантировать непрерывную доступность для записи, даже если соединение с сервером БД по какой либо причине отвалилось. В этом случае он может например тупо выкидывать сообщения, либо временно откатиться на другой механизм логирования, он может буферизовать сообщения где нибудь, а потом записать их в БД, после того как соединение между ними будет восстановлено.
Здравствуйте, igor-booch, Вы писали:
IB>По-мойму это справедливо если классы наследники только добавляют новые методы. Если в классах наследниках происходит перекрытие методов базового класса, то такая подстановка сломает программу. Например базовый класс принтер, класс наследник цветной лазерный принтер. Метод печатать.
Принцип говорит, что при проектировании класса принтер, у которого будет еще много наследников, спецификация на его метод Print должна описывать этот метод достаточно обще, чтобы все наследники под него подпадали. Например, просто говорится, что "изображение будет распечатано на плоском листе бумаги". И программа, если использует именно класс Printer, а не его наследника, никак не должна делать каких-либо предположений, больших чем просто что после этой операции изображение попадет на бумагу. Не должно быть предположений, что оно будет цветным или чернобелым, высокой детализации или низкой, устойчивым к воде или нет. Если нужны какие-то более точные гарантии на получившийся результат, — используй другие базовые классы (или интерфейсы).
А если вдруг нужно что-то, что делает все же не в соответствии со спецификацией, ожидаемой в программе, то не надо использовать этот же базовый класс, а надо завести другой, даже если на первый взгляд поведение и сигнатура методов очень похожи. Опять же например — если для нашего принтера ожидается, что это будет распечатка на бумаге, то при необходимости создать принтер, печатающий на CD-болванках, или какой-нибудь 3D-принтер — для них надо создавать другие базовые классы.
Здравствуйте, VladD2, Вы писали:
VD>Здравствуйте, LaptevVV, Вы писали:
LVV>>Кстати о "правильности" наследования: LVV>>
LVV>> спецификация. Дочерний класс реализует поведение, описанное в родительском классе. Ясно, что в С++ эта форма реализуется простым открытым наследованием от абстрактного класса;
LVV>> конструирование. Класс-наследник использует методы базового класса, но не является его подтипом (принцип подстановки не выполняется). В С++ такую форму можно реализовать простым закрытым наследованием;
LVV>> расширение. В класс-потомок добавляют новые методы, расширяя поведение родительского класса; принцип подстановки в такой форме выполняется;
LVV>> обобщение. Дочерний класс обобщает поведение базового класса. Обычно такое наследование используется в тех случаях, когда мы не можем изменить поведение базового класса (например, базовый класс является библиотечным классом);
LVV>> ограничение. Класс-наследник ограничивает поведение родительского класса. Очевидно, что в С++ такой вид наследования реализуется простым закрытым наследованием (пример — TUniversalDeque -> TStack );
LVV>> варьирование. Базовый класс и класс-потомок являются вариациями на одну тему, однако связь «класс-подкласс» произвольна, например, «квадрат-прямоугольник» или «прямоугольник-квадрат». Эта форма фактически не отличается от «конструирования», так как класс-наследник, очевидно, «использует методы базового класса, но не является его подтипом»;
LVV>> комбинирование. Дочерний класс наследует черты нескольких классов — это множественное наследование.
VD>Что только народ не придумает, чтобы оправдать использование наследования не по назначению?!
VD>А про возможность внутренние поля объявлять этот уважаемый автор не слышал?
А ты про empty base class optimization, не слышал? Или про ADL? В C++ наследование, всё-таки, даёт некоторые возможности, которые с помощью аггрегации не получишь. Между прочим, кто сказал, что единственное назначение наследования это выражение отношения тип — подтип согласно LSP?
B. Liskov "Data Abstraction and Hierarchy"
We are using the words "subtype" and "supertype" here to emphasize that now we are talking about
a semantic distinction. By contrast, "subclass" and "superclass" are simply linguistic concepts in
programming languages that allow programs to be built in a particular way. They can be used to
implement subtypes, but also, as mentioned above, in other ways.
Здравствуйте, k.o., Вы писали:
KO>А ты про empty base class optimization, не слышал? Или про ADL?
Тип решил поразить широтой своих познаний?
Если я правильно понял первый базворд относится к борьбе за размеры экземпляров типов основанную на опитимизациях которые делают отдельные компиляторы — говоря проще занятие ерундой.
Второе ADL (Argument-dependent name lookup) вообще никакого отношения к делу не имеет, так как относится к алгоритму разрешения перегрузки операторов и функций.
Короче, это смешно. Нахватался умных базвордов решил этим похвастаться?
Здорово, но не в тему.
KO>В C++ наследование, всё-таки, даёт некоторые возможности, которые с помощью аггрегации не получишь. Между прочим, кто сказал, что единственное назначение наследования это выражение отношения тип — подтип согласно LSP?
В C++, как и в любом другом ООЯ можно использовать ООП по делу — для реализации иерархий классов предметной области и для реализации Абстрактных Типов Данных. А можно не по делу.
Забавно то, что когда кто-то видит как кто-то другой пытается вырезать гланды автогеном и через жопу, то он смеется или возмущается. А когда тоже самое делается в области программирования, то мало того, что никто не удивляется, но еще и поясняется товарищи которые с умным видом начинают защищать это увлекательное занятие.
KO>B. Liskov "Data Abstraction and Hierarchy" KO>
KO>We are using the words "subtype" and "supertype" here to emphasize that now we are talking about
KO>a semantic distinction. By contrast, "subclass" and "superclass" are simply linguistic concepts in
KO>programming languages that allow programs to be built in a particular way. They can be used to
KO>implement subtypes, but also, as mentioned above, in other ways.
И что же ты тут такого вычитал?
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Здравствуйте, k.o., Вы писали:
KO>>А ты про empty base class optimization, не слышал? Или про ADL?
VD>Тип решил поразить широтой своих познаний?
Нет, просто хотел напомнить, что в некоторых языках, всё, к сожалению, не так просто.
VD>Если я правильно понял первый базворд относится к борьбе за размеры экземпляров типов основанную на опитимизациях которые делают отдельные компиляторы — говоря проще занятие ерундой.
Имхо, для того чтобы называть программирование на C++ занятием ерундой есть другой форум. Для тех же кому этим приходится заниматься, размер занимаемой памяти иногда имеет очень важное значение.
VD>Второе ADL (Argument-dependent name lookup) вообще никакого отношения к делу не имеет, так как относится к алгоритму разрешения перегрузки операторов и функций.
Этот алгоритм будет искать функции в том числе и в пространствах имён всех предков класса, включая закрытых и защищенных. Поэтому, я думаю, это имеет отношение к использованию наследования.
KO>>В C++ наследование, всё-таки, даёт некоторые возможности, которые с помощью аггрегации не получишь. Между прочим, кто сказал, что единственное назначение наследования это выражение отношения тип — подтип согласно LSP?
VD>В C++, как и в любом другом ООЯ можно использовать ООП по делу — для реализации иерархий классов предметной области и для реализации Абстрактных Типов Данных. А можно не по делу.
ООП это одно, а конкретные языковые конструкции несколько другое. Можно использовать их для ООП, а можно и для других целей.
VD>Забавно то, что когда кто-то видит как кто-то другой пытается вырезать гланды автогеном и через жопу, то он смеется или возмущается. А когда тоже самое делается в области программирования, то мало того, что никто не удивляется, но еще и поясняется товарищи которые с умным видом начинают защищать это увлекательное занятие.
И правда, забавно, только я надеюсь ты меня к таким товарищам не причисляешь?
KO>>B. Liskov "Data Abstraction and Hierarchy" KO>>
KO>>We are using the words "subtype" and "supertype" here to emphasize that now we are talking about
KO>>a semantic distinction. By contrast, "subclass" and "superclass" are simply linguistic concepts in
KO>>programming languages that allow programs to be built in a particular way. They can be used to
KO>>implement subtypes, but also, as mentioned above, in other ways.
VD>И что же ты тут такого вычитал?
Что назначение такой языковой конструкции как наследование не сводится к выражению отношения тип — подтип. А что ещё тут можно вычитать?
Здравствуйте, LaptevVV, Вы писали:
LVV>Здравствуйте, igor-booch, Вы писали:
IB>>http://en.wikipedia.org/wiki/Liskov_substitution_principle
IB>>Насколько я понял принцип гласит, что если в программе заменить базовые классы классами наследниками, то ничего не должно сломаться. По-мойму бред. Или я что-то неправильно понял. LVV>Это динамически происходит. "На место объекта базового класса МОЖНО подставить объект производного класса". Обратно — нельзя. LVV>Будильник — это часы, но не всякие часы — будильник.
Не согласен, не всякий будильник — часы. Будильником может являться любой раздражитель.
Например на практике используются такие будильники по утрам как солнечный свет в окне, петух, уличный шум, голодная жена/дите/собака/кот.
Я программист, я Иван Помидоров, хватить трепатся — наш козырь error.
Здравствуйте, ankf, Вы писали:
IB>>>Насколько я понял принцип гласит, что если в программе заменить базовые классы классами наследниками, то ничего не должно сломаться. По-мойму бред. Или я что-то неправильно понял. LVV>>Это динамически происходит. "На место объекта базового класса МОЖНО подставить объект производного класса". Обратно — нельзя. LVV>>Будильник — это часы, но не всякие часы — будильник.
A>Не согласен, не всякий будильник — часы. Будильником может являться любой раздражитель. A>Например на практике используются такие будильники по утрам как солнечный свет в окне, петух, уличный шум, голодная жена/дите/собака/кот.
Тогда так: всякий квадрат — четырехугольник, но не всякий четырехугольник — квадрат...
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Здравствуйте, LaptevVV, Вы писали:
LVV>Здравствуйте, ankf, Вы писали:
IB>>>>Насколько я понял принцип гласит, что если в программе заменить базовые классы классами наследниками, то ничего не должно сломаться. По-мойму бред. Или я что-то неправильно понял. LVV>>>Это динамически происходит. "На место объекта базового класса МОЖНО подставить объект производного класса". Обратно — нельзя. LVV>>>Будильник — это часы, но не всякие часы — будильник.
A>>Не согласен, не всякий будильник — часы. Будильником может являться любой раздражитель. A>>Например на практике используются такие будильники по утрам как солнечный свет в окне, петух, уличный шум, голодная жена/дите/собака/кот. LVV>Тогда так: всякий квадрат — четырехугольник, но не всякий четырехугольник — квадрат...
Тоже не согласен, например в музыке есть понятие квадрат , к четырехугольникам не имеющий отношение.
Есть математическая операция , например квадрат числа, означающая возведение в степень.
Я программист, я Иван Помидоров, хватить трепатся — наш козырь error.
Здравствуйте, ankf, Вы писали:
LVV>>>>Будильник — это часы, но не всякие часы — будильник.
A>>>Не согласен, не всякий будильник — часы. Будильником может являться любой раздражитель. A>>>Например на практике используются такие будильники по утрам как солнечный свет в окне, петух, уличный шум, голодная жена/дите/собака/кот. LVV>>Тогда так: всякий квадрат — четырехугольник, но не всякий четырехугольник — квадрат...
A>Тоже не согласен, например в музыке есть понятие квадрат , к четырехугольникам не имеющий отношение. A>Есть математическая операция , например квадрат числа, означающая возведение в степень.
Если мы пишем некую прогу, то ограничиваемся одной предметной областью. И не лезем в другую...
Хочешь быть счастливым — будь им!
Без булдырабыз!!!