Принципы SOLID: принцип замещения Лискова
Знаете, когда я впервые услышал название принципа замещения Лискова, я подумал, что это будет самый сложный принцип 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!");
}
}
Main
— это основной класс нашей программы, содержащий ее логику. У него есть два метода: letBirdsFly(List<Bird> birds)
и main(String[] args)
. Первый метод принимает в качестве параметра список птиц и вызывает их методы fly. Второй создает список и передает его первому.
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);
}
}
Программа просто создает список птиц и позволяет им летать. Если вы попытаетесь запустить эту программу, она выведет следующее утверждение:
Теперь попробуем применить определение этого принципа к нашему основному методу и посмотрим, что получится. Мы собираемся заменить объект 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!"); } }
Мы можем проверить, применим ли этот принцип к нашему коду или нет, добавив объект 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
есть дочерний элемент, который неправильно использовал наследование, что вызвало проблему. Penguin
пытается расширить логику полета, но не может летать!
Мы можем решить эту проблему, используя следующую проверку if:
public static void letBirdsFly(List<Bird> 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?"); } }
Теперь мы можем отредактировать метод 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!
Вы можете видеть, что принцип замещения Лискова заключается в правильном использовании отношения наследования. Вы должны создавать подтипы какого-либо родителя тогда и только тогда, когда они собираются правильно реализовать его логику, не вызывая никаких проблем.
Мы подошли к концу этого пути, но нам еще предстоит рассмотреть еще два принципа. Так что не торопитесь читать об этом принципе и убедитесь, что вы понимаете его, прежде чем двигаться дальше. Быть в курсе!
Первоначально опубликовано на https://amrsaeed.com 25 октября 2020 г.