Принцип подстановки Лисков

Spread the love

Перевод: Amr SaeedLiskov Substitution Principle

Вы знаете, когда я впервые услышал название принципа замещения Лисков, я подумал, что он будет самым сложным из всех принципов SOLID. Название принципа показалось мне очень странным. Я судил о книге по обложке и убедил себя, что не пойму его. В конце концов, оказалось, что это был один из самых простых и понятных принципов SOLID.

Итак, давайте начнем наше путешествие с простого определения принципа подстановки Лисков:

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

Я знаю, это звучит для вас странно, но давайте разберемся с этим по частям. Предположим, у нас есть программа, у которой есть родительский класс. От родительского класса наследуются дочерние классы. Если мы решили создать некоторые объекты от родительского класса в нашей программе, мы должны иметь возможность заменить любой из них любым объектом любого дочернего класса, и программа должна работать должным образом без каких-либо ошибок.

Другими словами, мы должны иметь возможность заменять объекты родительского класса объектами дочерних классов, не вызывая прерывания программы. Вот почему в названии принципа есть ключевое слово “подстановка”. Что касается Лисков, то это имя ученой Барбары Лисков, которая разработала научное определение этого принципа. Вы можете прочитать эту статью Принцип подстановки Лискова в Википедии для получения дополнительной информации об этом определении.

Теперь давайте попробуем связать определение, которое мы только что обсудили, с известным примером, чтобы понять принцип.

Bird – это класс, в котором есть два метода – eat () и fly (). Он представляет собой базовый класс, который может расширять любой тип птиц.

public class Bird {

    public void eat() {
        System.out.println("I can eat.");
    }

    public void fly() {
        System.out.println("I can fly.");
    }
}

Swan (Лебедь) – это птица, которая умеет есть и летать. Следовательно, он может наследоваться от класса Bird.

public class Swan extends Bird {

    @Override
    public void eat() {
        System.out.println("OMG! I can eat pizza!");
    }

    @Override
    public void fly() {
        System.out.println("I believe I can fly!");
    }
}
bird-swan-2

Main – это основной класс нашей программы, который содержит все логику. У него есть два метода: letBirdsFly (List birds) и main (String [] args). Первый метод принимает список птиц в качестве параметра и вызывает их методы полета. Второй создает список и передает его первому.

public class Main {

    public static void letBirdsFly(List<Bird> birds) {
        for(Bird bird: birds) {
            bird.fly();
        }
    }

    public static void main(String[] args) {
        List<Bird> birds = new ArrayList<Bird>();
        birds.add(new Bird());
        letBirdsFly(birds);
    }
}

Программа просто создает список птиц и позволяет им летать. Если вы попытаетесь запустить эту программу, она выдаст следующий результат:

I can fly.

Теперь давайте попробуем применить определение этого принципа к нашему основному методу и посмотрим, что произойдет. Мы собираемся заменить объект Bird на объект Swan.

public static void main(String[] args) {
    List<Bird> birds = new ArrayList<Bird>();       
    birds.add(new Swan());
    letBirdsFly(birds);
}

Если мы попытаемся запустить программу после применения изменений, она выдаст следующий оператор:

I believe I can fly!

Мы видим, что этот принцип отлично применим к нашему коду. Программа работает как положено, без ошибок и проблем. Но что, если мы попытаемся расширить класс Bird новым типом птиц, которые не умеют летать?

public class Penguin extends Bird {

    @Override
    public void eat() {
        System.out.println("Can I eat taco?");
    }

    @Override
    public void fly() {
        throw new UnsupportedOperationException("Help! I cannot fly!");
    }
}
bird-swan-penguin-1

Мы можем проверить, применим ли этот принцип к нашему коду или нет, добавив объект Penguin в список птиц и запустив код.

public static void main(String[] args) {
    List<Bird> birds = new ArrayList<Bird>();       
    birds.add(new Swan());
    birds.add(new Penguin());
    letBirdsFly(birds);
}
I believe I can fly!
Exception in thread "main" 
java.lang.UnsupportedOperationException: Help! I cannot fly!

Опс! это не сработало, как ожидалось

Мы видим, что с объектом Swan код работал отлично. Но с объектом Penguin код выдал исключение UnsupportedOperationException. Этот код нарушает принцип подстановки Лисков, поскольку у класса Bird есть дочерний элемент, который неправильно использовал наследование и, следовательно, вызвал проблему. Пингвин пытается расширить логику полета, но он не умеет летать!

Мы можем решить эту проблему, если проверим следующее:

public static void letBirdsFly(List<Bird> birds) {
    for(Bird bird: birds) {
        if(!(bird instanceof Penguin)) {
            bird.fly();
        }
    }
}

Но это решение считается плохой практикой, и оно нарушает принцип Open Closed. Представьте, что мы добавим еще три типа птиц, которые не умеют летать. Код превратится в мешанину. Также обратите внимание, что одно из определений принципа замещения Лисков, разработанное Робертом Мартином:

Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

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

Одно из хороших решений для решения этой проблемы и изменения принципа – выделить логику полета в другой класс.

public class Bird {

    public void eat() {
        System.out.println("I can eat.");
    }
}
public class FlyingBird extends Bird {

    public void fly() {
        System.out.println("I can fly.");
    }
}
public class Swan extends FlyingBird {

    @Override
    public void eat() {
        System.out.println("OMG! I can eat pizza!");
    }

    @Override
    public void fly() {
        System.out.println("I believe I can fly!");
    }
}
public class Penguin extends Bird {

    @Override
    void eat() {
        System.out.println("Can I eat taco?");
    }
}
bird-swan-penguin-flyingbird-3

Теперь мы можем отредактировать метод letBirdsFly для поддержки только летающих птиц.

public class Main {

    public static void letBirdsFly(List<FlyingBird> flyingBirds) {
        for(FlyingBird flyingBird: flyingBirds) {
            flyingBird.fly();
        }
    }

    public static void main(String[] args) {
        List<FlyingBird> flyingBirds = new ArrayList<FlyingBird>();     
        flyingBirds.add(new Swan());
        letBirdsFly(flyingBirds);
    }
}

Причина, по которой мы заставили метод letBirdsFly принимать только летающих птиц, заключается в том, чтобы гарантировать, что любая замена FlyingBird сможет летать. Теперь программа работает должным образом и выводит следующий результат:

I believe I can fly!

Как видите, принцип замещения Лисков заключается в правильном использовании отношения наследования. Вы должны создавать подтипы какого-либо родителя тогда и только тогда, когда они собираются правильно реализовать его логику, не вызывая никаких проблем.

Мы подошли к концу этой статьи, но нам нужно рассмотреть еще два принципа. Поэтому не торопитесь, читая об этом принципе, и убедитесь, что вы его понимаете, прежде чем двигаться дальше. Следите за обновлениями!

Была ли вам полезна эта статья?
[10 / 4.6]

Spread the love
Подписаться
Уведомление о
guest
3 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Илья П.
Илья П.
1 год назад

Отличный пример! Спасибо за объяснение!

Last edited 1 год назад by Илья П.
Юрий
Юрий
8 месяцев назад

Топ

Алексей
Алексей
18 дней назад

Хороший и понятный пример!