Комментарии 20
пример с MicroCustomer, это другой объект, с новыми условиями, почему нельзя дополнительные проверки устраивать для объектов этого типа?
пример с MicroCustomer, это другой объект, с новыми условиями, почему нельзя дополнительные проверки устраивать для объектов этого типа?
В том-то и дело, это не совсем другой объект, это наследник. Представьте, что вы работаете с классом Customer
через какую-нибудь такую функцию (сорри, в пхп не силен, буду на псеводязыке):
void processCustomer(Customer customer) {
customer.putMoneyIntoAccount(200);
}
Так ведь можно, никто не запрещает по контракту. А потом кто-то вызывает эту функцию так:
var customer = MicroCustomer();
processCustomer(customer);
Никакого подвоха никто не ожидает, в том числе и компилятор, ведь MicroCustomer
– это тоже Customer
. Только в результате этого у вас будет runtime exception. И это еще самый простой пример, а если у вас какой-нибудь массив:
List<Customer> customers = [Customer(), MicroCustomer(), Customer()];
customers.map(processCustomer);
Что с этим делать?
В этом и есть суть принципа подстановки Лисков – вы можете передать в функцию, принимающую базовый класс, любой класс-наследник, и ничего не должно сломаться. Отсюда уже всякие следствия про пред- и пост-условия и инварианты.
И это еще самый простой пример, а если у вас какой-нибудь массивКстати, с массивами (контейнерами вообще) там свои сложности возникают, в разных языках по-разному решаемые.
Нельзя MicroCustomer класть туда, где ожидается Customer. Только и всего.
Дается ссылка на другую статью Хабра про квадраты и прямоугольники, тот автор также облажался: не смог придумать SOLID-решение для такого простого примера. У меня возникло сиюминутное желание навалять статью для Хабра)))
в SOLID вроде как наоборот, нежели рассказывается в статье: родители должны описывать поведение более обще, абстрактно, а потомки должны лишь уточнять поведение.
А можно узнать, где об этом написано?
Дочерние классы могут как угодно уточнять реализацию базового класса покуда это не накладывает дополнительных предусловий. Если это происходит, то дочерний класс больше нельзя безопасно использовать вместо родительского, и принцип подстановки нарушен. Это не смертный грех, и на практике мы нередко встречаемся с такими ситуациями, поэтому, возможно, вы и воспринимаете это как вариант нормы, но написание кода это усложняет.
Чтобы не быть голословным, в.NET есть класс Stream, у которого есть изменяемое свойство Position. Но при попытке изменить Position у наследника типа HttpResponseStream получаем NotSupportedException. Т.е. принцип подстановки нарушается. Но реализовать абстракцию Stream, не нарушающую этот принцип для всех возможных наследников, часть из которых read only, часть write only, и при этом сохранить его полезность - очень непростая задача. Отсюда компромиссы и вынужденное нарушение принципа Лисков.
Так разве SOLID заставляет вас искать абстракцию у кошки и лимона?
Мне кажется в данном контексте вы путаете с DRY, но там тоже есть свои оговорки, что его надо использовать с умом.
Что касается примеров, то действительно интуитивно хочется сделать наоборот, но в действительности это может сломать клиентский код.
Если у вас есть источники с альтернативной информацией - поделитесь, очень интересно другое мнение.
Речь о том, что наследник не должен требовать больше и делать меньше, чем родитель, соблюдать его контракт — таким образом родителя везде можно заменить на потомка и все будет хорошо.
А вообще хорошо спроектировать хорошее наследование довольно сложно. Поэтому для кода в целом лучше пользуйтесь интерфейсами, композицией и правилом abstract or final — выйдет лучше.
Гляньте ещё и этот разбор: https://m.habr.com/ru/post/521258/
Так объяснение в самом начале. Наследующий класс должен дополнять, а не замещать поведение базового класса.
А далее описано как этого добиться.
Может есть более конкретный вопрос, который стоит раскрыть?
В тоже время, если бизнес приходит и просит создать новый тип клиента, который не сможет пополнять свой счет более чем на 100 единиц — в чем проблема создать показанный класс MicroCustomer?
P.S.
Если же говорить о реальной практичности и надежности написанного кода — то здесь в первую очередь стоит заморачиваться не над принципом подстановки Барбары Лисков, а на привязку к интерфейсам (а не к конкретным реализациям) и покрытию кода авто-тестами.
Интересное замечание, я даже залез в оригинальный доклад самой Лисков. Не уверен, что всё так однозначно. В оригинальной статье везде используются понятия "subtype" и "supertype", а также "inherited methods", где уточняется, что "We do not mean that the subtype inherits the code of these methods but simply that it provides methods with the same behavior as the corresponding supertype methods."
Да и в статье, что вы привели Мартин пишет "People (including me) have made the mistake that this is about inheritance. It is not. It is about sub-typing. All implementations of interfaces are subtypes of an interface." То есть он не сказал "It is about implementations of interfaces", возможно как раз потому, что не только к наследованию (которое также заставляет вас использовать интерфейс супертипа), но и к имплементации интерфейсов это применимо.
Но круто, если есть еще интересные источники - кидайте, интересно почитать.
Как без нарушения этого принципа добавлять динамическое поведение объекту, о котором нам изначально ничего неизвестно?
Есть у нас человек (Human), который познаёт некий объект.
interface Human
{
public function touch(SomeObject $object);
}
class SomeObject
{
public function is(); // ¯\_(ツ)_/¯
}
Он его трогает, понимает что оно круглое
class SomeRoundObject extends SomeObject
{
public function is(); // some round object
public function roll(): bool; // roll some round object
}
Потом узнаёт что оно мягкое
class SomeSoftObject extends SomeObject
{
public function is(); // some round object
}
Потом узнаёт что у него есть сосок
class SomeObjectWithNipple extends SomeObject
{
public function is(); // some round object
public function isHumanPart(): bool; //no
}
Потом узнаёт что это колёсная камера
class TireCamera extends SomeObjectWithNipple
{
public function is(); // some round object
public function matchesToWheel(Wheel $wheel): bool
}
Правильно ли я понимаю, что принцип не позволяет мне передавать все эти объекты человеку на «потрогать», и внутри метода «потрогать» обращаться к методам конкретного подтипа?
class Human
{
public function touch(SomeObject $object){
if($object instanceof SomeObjectWithNipple){
$object->isHumanPart(); // барбара не довольна!
}
}
}
И что делать, если нам в зависимости от конкретного подтипа SomeObject нужно обратиться к его методу (например, к
isHumanPart()
класса SomeObjectWithNipple
)?Заводить человеку по отдельному методу
touchSome***Object(Some***Object)
на каждый подтип, чтобы не расстраивать Барбару?Огорчитесь Вы сами через некоторое время. Потому что окажется, что класс Human знает обо всех текущих реализациях, но не может работать ни с одной новой. Каждое добавление и выпиливание наследников будет приводить вас к классу Human дабы подшаманить его. И приводить к риску поломки (т.е. нарушать принцип Открытости-Закрытости).
Ваш класс Human собирает информацию о SomeObject. Непонятно зачем. Если этот SomeObject является зависимостью Human, значит Human делегирует часть работы SomeObject.
Делегирует, а не играет в угадайку «сможет-не-сможет». А если и запрашивает какую-то информацию, то в унифицированном виде, не печалась о том, как объект эту информацию сформирует:
interface SomeObject {
public function __toString(): string
public function toArray(): array
public function doWorkForHuman(array $taskDetails): void
}
Принцип подстановки Барбары Лисков (предусловия и постусловия)