Как стать автором
Обновить

Комментарии 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 вроде как наоборот, нежели рассказывается в статье: родители должны описывать поведение более обще, абстрактно, а потомки должны лишь уточнять поведение. Рекомендации статьи с Customer и Microcustomer говорят о противоположном, как будто родители и потомки поменялись местами. И пример кода, который в статье назван неправильным, наоборот, по мне, является корректным. Пример кода далее Foo-Bar наоборот некорректный. И так далее. Где правда?
Дается ссылка на другую статью Хабра про квадраты и прямоугольники, тот автор также облажался: не смог придумать SOLID-решение для такого простого примера. У меня возникло сиюминутное желание навалять статью для Хабра)))

в SOLID вроде как наоборот, нежели рассказывается в статье: родители должны описывать поведение более обще, абстрактно, а потомки должны лишь уточнять поведение.

А можно узнать, где об этом написано?

Дочерние классы могут как угодно уточнять реализацию базового класса покуда это не накладывает дополнительных предусловий. Если это происходит, то дочерний класс больше нельзя безопасно использовать вместо родительского, и принцип подстановки нарушен. Это не смертный грех, и на практике мы нередко встречаемся с такими ситуациями, поэтому, возможно, вы и воспринимаете это как вариант нормы, но написание кода это усложняет.

Чтобы не быть голословным, в.NET есть класс Stream, у которого есть изменяемое свойство Position. Но при попытке изменить Position у наследника типа HttpResponseStream получаем NotSupportedException. Т.е. принцип подстановки нарушается. Но реализовать абстракцию Stream, не нарушающую этот принцип для всех возможных наследников, часть из которых read only, часть write only, и при этом сохранить его полезность - очень непростая задача. Отсюда компромиссы и вынужденное нарушение принципа Лисков.

НЛО прилетело и опубликовало эту надпись здесь

Так разве SOLID заставляет вас искать абстракцию у кошки и лимона?

Мне кажется в данном контексте вы путаете с DRY, но там тоже есть свои оговорки, что его надо использовать с умом.

Что касается примеров, то действительно интуитивно хочется сделать наоборот, но в действительности это может сломать клиентский код.

Если у вас есть источники с альтернативной информацией - поделитесь, очень интересно другое мнение.

Примеры в стартье действительно могут быть неудачными, но то, что родители описывают более общее поведение, а потомки — более детальное, этому не противоречит. Предусловия и постусловия не про то, что они должны делать, а про то, что они требуют от других.

Речь о том, что наследник не должен требовать больше и делать меньше, чем родитель, соблюдать его контракт — таким образом родителя везде можно заменить на потомка и все будет хорошо.

А вообще хорошо спроектировать хорошее наследование довольно сложно. Поэтому для кода в целом лучше пользуйтесь интерфейсами, композицией и правилом abstract or final — выйдет лучше.
Когда говорят — «так нельзя делать» всегда должно быть объяснение — почему. Если его нет, то статья превращается в очередной пересказ/перевод и не несет никакой новой информации.

Так объяснение в самом начале. Наследующий класс должен дополнять, а не замещать поведение базового класса.

А далее описано как этого добиться.

Может есть более конкретный вопрос, который стоит раскрыть?

Это не объяснение. Я хочу получить ответ почему он должен. Что влечет нарушение этого принципа. Почему лучше делать так, а не иначе.

На мой взгляд автор как-то слишком уж извратил и усложнил принцип подстановки Барбары Лисков, который, в упрощенном виде, говорит о том, что мы не наследуемся от AbstractController и не делаем из него класс работы с базой данных.

В тоже время, если бизнес приходит и просит создать новый тип клиента, который не сможет пополнять свой счет более чем на 100 единиц — в чем проблема создать показанный класс MicroCustomer?

P.S.
Если же говорить о реальной практичности и надежности написанного кода — то здесь в первую очередь стоит заморачиваться не над принципом подстановки Барбары Лисков, а на привязку к интерфейсам (а не к конкретным реализациям) и покрытию кода авто-тестами.
LSP — не про наследование. А про имплементации интерфейса. И в том случае, когда он хорошо спроектирован, имплементации полностью взаимозаменяемы. Комментарий в блоге Мартина в ответ на нападки на SOLID.

Интересное замечание, я даже залез в оригинальный доклад самой Лисков. Не уверен, что всё так однозначно. В оригинальной статье везде используются понятия "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", возможно как раз потому, что не только к наследованию (которое также заставляет вас использовать интерфейс супертипа), но и к имплементации интерфейсов это применимо.

Но круто, если есть еще интересные источники - кидайте, интересно почитать.

Однозначно советую чистую архитектуру того же автора. В ней он подробно и системно разбирает сам SOLID, формулирует принципы связанности компонентов, да и весь свой многолетний опыт разработки и проектирования. Всего и не перечесть, на ресурсе по ссылке можно полистать оглавление книги.

Да, читал и периодически возвращаюсь, каждый раз обращая внимание на новые детали :)

Я имел ввиду по теме LSP

То ли принцип неправильно истолкован, то ли я вообще не понимаю ничего в программировании.
Как без нарушения этого принципа добавлять динамическое поведение объекту, о котором нам изначально ничего неизвестно?

Есть у нас человек (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
}
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории