Привет!
В предыдущих лекциях мы уже встречались с таким понятием как «паттерн проектирования».
Если ты забыл, напомним: этим термином обозначают некое стандартное решение распространенной в программировании задачи.
На JavaRush мы часто говорим, что ответ практически на любой вопрос можно загуглить. Поэтому задачу, похожую на твою, наверняка уже кто-то успешно решил.
Так вот, паттерны — это проверенные временем и практикой решения наиболее распространенных задач или методы разрешения проблемных ситуаций.
Это те самые «велосипеды», которые ни в коем случае не нужно изобретать самому, но нужно знать, как и когда их применить :)
Другая задача паттернов — приведение архитектуры к единому стандарту.
Читать чужой код — задача не из легких! Все пишут его по-разному, ведь одну и ту же задачу можно решить многими способами. Но использование паттернов позволяет разным программистам понять логику работы программы, не вникая в каждую строку кода (даже если они видят его впервые!)
Сегодня мы рассмотрим один из наиболее распространенных паттернов под названием «Стратегия».
Представим, что мы пишем программу, активно работающую с объектом Автомобиль. В данном случае даже не особо важно, что именно делает наша программа.
Для этого мы создали систему наследования с одним родительским классом
Auto
и тремя дочерними классами: Sedan
, Truck
и F1Car
.
public class Auto {
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
public class Sedan extends Auto {
}
public class Truck extends Auto {
}
public class F1Car extends Auto {
}
Все три дочерних класса наследуют от родительского два стандартных метода — gas()
и stop()
Программа у нас совсем простая: машины умеют только ехать вперед и тормозить.
Продолжая нашу работу, мы решили добавить машинам новый метод — fill()
(заправить топливо).
Добавим его в родительский класс Auto
:
public class Auto {
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
public void fill() {
System.out.println("Заправить бензин!");
}
}
Казалось бы, разве могут в такой простой ситуации возникнуть проблемы? Ну, на самом деле, они уже возникли…
public class ChildrenBuggies extends Auto {
public void fill() {
//хм... Это детский багги, его не надо заправлять :/
}
}
В нашей программе появился автомобиль, который не вписывается в общую концепцию — детский багги. Он может быть с педалями или радиоуправляемым, но одно ясно точно — бензин в него заливать некуда.
Наша схема наследования привела к тому, что мы раздали общие методы даже тем классам, в которых они не нужны.
Что нам делать в такой ситуации? Ну, например, можно переопределить метод fill()
в классе ChildrenBuggies
, чтобы при попытке заправить багги ничего не происходило:
public class ChildrenBuggies extends Auto {
@Override
public void fill() {
System.out.println("Игрушечную машину нельзя заправить!");
}
}
Но это решение сложно назвать удачным как минимум из-за дублирования кода. К примеру, большая часть классов будет использовать метод из родительского класса, но другая часть классов будет вынуждена его переопределить.
Если у нас будет 15 классов, и в 5-6 мы будем вынуждены переопределять поведение, дублирование кода станет довольно обширным.
Может, нам смогут помочь интерфейсы?
Например, вот такой:
public interface Fillable {
public void fill();
}
Мы создадим интерфейс Fillable
с одним методом fill()
. Соответственно, те машины, которые необходимо заправлять, будут имплементировать этот интерфейс, а другие машины (к примеру, наш багги) — не будут.
Но и этот вариант нам не подойдет.
Наша иерархия классов может в будущем разрастись до очень большого числа (представь, сколько разных видов автомобилей есть на свете).
Мы отказались от предыдущего варианта с наследованием, потому что не хотели много раз переопределять метод fill()
. Здесь же нам придется реализовывать его в каждом классе! А что если их у нас будет 50?
И если в нашу программу будут вноситься частые изменения (а в реальных программах почти всегда так и будет!), нам придется носиться с высунутым языком между всеми 50 классами и менять поведение каждого из них вручную.
Так как же нам в итоге поступить?
Чтобы решить нашу проблему, выберем иной путь. А именно — отделим поведение нашего класса от самого класса.
Что это значит?
Как ты знаешь, любой объект имеет состояние (набор данных) и поведение (набор методов).
Поведение нашего класса машин состоит из трех методов — gas()
, stop()
и fill()
.
С первыми двумя методами все в порядке. А вот третий метод мы вынесем за пределы класса Auto
. Это и будет отделением поведения от класса (точнее, мы отделяем только часть поведения — два первых метода остаются на месте).
Куда же мы должны перенести наш метод fill()
? Сходу ничего не приходит в голову :/ Он, вроде как, был вполне на своем месте.
Мы перенесем его в отдельный интерфейс — FillStrategy
!
public interface FillStrategy {
public void fill();
}
Зачем нам нужен этот интерфейс? Все просто.
Теперь мы сможем создать несколько классов, которые будут этот интерфейс реализовывать:
public class HybridFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Заправляем бензином или электричеством на выбор!");
}
}
public class F1PitstopStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Заправляем бензин только после всех остальных процедур пит-стопа!");
}
}
public class StandartFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Просто заправляем бензин!");
}
}
Мы создали три стратегии поведения — для обычных машин, для гибридов и для болидов Формулы-1.
Каждая стратегия реализует отдельный алгоритм заправки. В нашем случае это просто вывод в консоль, но внутри метода может быть и какая-то сложная логика.
Что же нам делать с этим дальше?
public class Auto {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
Мы используем наш интерфейс FillStrategy
в качестве поля в родительском классе Auto
. Обрати внимание: мы не указываем конкретную реализацию, а используем именно интерфейс.
А конкретные реализации интерфейса FillStrategy
понадобятся нам в дочерних классах-автомобилях:
public class F1Car extends Auto {
public F1Car() {
this.fillStrategy = new F1PitstopStrategy();
}
}
public class HybridAuto extends Auto {
public HybridAuto() {
this.fillStrategy = new HybridFillStrategy();
}
}
public class Sedan extends Auto {
public Sedan() {
this.fillStrategy = new StandartFillStrategy();
}
}
Посмотрим, что у нас получилось:
public class Main {
public static void main(String[] args) {
Auto sedan = new Sedan();
Auto hybrid = new HybridAuto();
Auto f1car = new F1Car();
sedan.fill();
hybrid.fill();
f1car.fill();
}
}
Вывод в консоль:
Просто заправляем бензин!
Заправляем бензином или электричеством на выбор!
Заправляем бензин только после всех остальных процедур пит-стопа!
Отлично, процесс заправки работает как надо!
Кстати, ничто не мешает нам использовать стратегию в качестве параметра в конструкторе!
Например, вот так:
public class Auto {
private FillStrategy fillStrategy;
public Auto(FillStrategy fillStrategy) {
this.fillStrategy = fillStrategy;
}
public void fill() {
this.fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
public class Sedan extends Auto {
public Sedan() {
super(new StandartFillStrategy());
}
}
public class HybridAuto extends Auto {
public HybridAuto() {
super(new HybridFillStrategy());
}
}
public class F1Car extends Auto {
public F1Car() {
super(new F1PitstopStrategy());
}
}
Запустим наш метод main()
(он остался без изменений), и получим тот же результат!
Вывод в консоль:
Просто заправляем бензин!
Заправляем бензином или электричеством на выбор!
Заправляем бензин только после всех остальных процедур пит-стопа!
Паттерн «Стратегия» определяет семейство алгоритмов, инкапсулирует каждый из них и обеспечивает их взаимозаменяемость. Он позволяет модифицировать алгоритмы независимо от их использования на стороне клиента
(это определение взято из книги “Изучаем паттерны проектирования” и кажется мне крайне удачным).
Мы выделили интересующее нас семейство алгоритмов (виды заправки машин) в отдельных интерфейс с несколькими реализациями. Мы отделили их от самой сущности автомобиля. Поэтому теперь, если нам понадобится внести какие-то изменения в тот или иной процесс заправки, это никак не затронет наши классы машин.
Что касается взаимозаменяемости, то для ее достижения нам достаточно добавить один метод-сеттер в наш класс Auto
:
public class Auto {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
public void setFillStrategy(FillStrategy fillStrategy) {
this.fillStrategy = fillStrategy;
}
}
Теперь мы можем менять стратегии на ходу:
public class Main {
public static void main(String[] args) {
ChildrenBuggies buggies = new ChildrenBuggies();
buggies.setFillStrategy(new StandartFillStrategy());
buggies.fill();
}
}
Если вдруг детские машины-багги начнут заправлять бензином, наша программа будет готова к такому варианту развития событий :)
Вот, собственно, и все! Ты выучил еще один паттерн проектирования, который, несомненно, тебе понадобится и еще не раз выручит в работе над реальными проектами :)
До новых встреч!