Рубрики
Без рубрики

Принцип замещения Лискова

Вы знаете, когда я впервые услышал название Принципа замещения Лискова, я подумал об этом… С тегами java, ооп, программирование, информатика.

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

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

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

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

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

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

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

public class Bird {

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

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

Лебедь – это вид птиц, которые могут есть и летать. Следовательно, он должен расширить класс 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!");
    }
}

Main – это основной класс нашей программы, который содержит ее логику. У него есть два метода, let Birds Fly(Список<Птица> птицы) и main(строка[] аргументы) . Первый метод принимает список птиц в качестве параметра и вызывает их методы fly. Второй создает список и передает его первому.

public class Main {

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

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

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

I can fly.

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

public static void main(String[] args) {
    List birds = new ArrayList();       
    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!");
    }
}

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

public static void main(String[] args) {
    List birds = new ArrayList();       
    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 имеет дочерний элемент, который неправильно использовал наследование, что вызвало проблему. Пингвин пытается расширить логику полета, но он не может летать!

Мы можем устранить эту проблему, используя следующую проверку if:

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

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

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

Это не относится к нашему решению, поскольку мы пытаемся узнать тип объекта 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?");
    }
}

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

public class Main {

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

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

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

I believe I can fly!

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

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

Оригинал: “https://dev.to/amrsaeedhosny/liskov-substitution-principle-ofc”