JavaRush /Java блог /Java Developer /Паттерн проектирования “Стратегия”
Автор
John Selawsky
Senior Java-разработчик и преподаватель в LearningTree

Паттерн проектирования “Стратегия”

Статья из группы Java Developer
Привет! В предыдущих лекциях мы уже встречались с таким понятием как «паттерн проектирования». Если ты забыл, напомним: этим термином обозначают некое стандартное решение распространенной в программировании задачи. Паттерн проектирования “Стратегия” - 1На JavaRush мы часто говорим, что ответ практически на любой вопрос можно загуглить. Поэтому задачу, похожую на твою, наверняка уже кто-то успешно решил. Так вот, паттерны — это проверенные временем и практикой решения наиболее распространенных задач или методы разрешения проблемных ситуаций. Это те самые «велосипеды», которые ни в коем случае не нужно изобретать самому, но нужно знать, как и когда их применить :) Другая задача паттернов — приведение архитектуры к единому стандарту. Читать чужой код — задача не из легких! Все пишут его по-разному, ведь одну и ту же задачу можно решить многими способами. Но использование паттернов позволяет разным программистам понять логику работы программы, не вникая в каждую строку кода (даже если они видят его впервые!) Сегодня мы рассмотрим один из наиболее распространенных паттернов под названием «Стратегия». Паттерн проектирования “Стратегия” - 2Представим, что мы пишем программу, активно работающую с объектом Автомобиль. В данном случае даже не особо важно, что именно делает наша программа. Для этого мы создали систему наследования с одним родительским классом 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("Заправить бензин!");
   }
}
Казалось бы, разве могут в такой простой ситуации возникнуть проблемы? Ну, на самом деле, они уже возникли… Паттерн проектирования “Стратегия” - 3

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() (он остался без изменений), и получим тот же результат! Вывод в консоль:

Просто заправляем бензин!
Заправляем бензином или электричеством на выбор!
Заправляем бензин только после всех остальных процедур пит-стопа!
Паттерн «Стратегия» определяет семейство алгоритмов, инкапсулирует каждый из них и обеспечивает их взаимозаменяемость. Он позволяет модифицировать алгоритмы независимо от их использования на стороне клиента (это определение взято из книги “Изучаем паттерны проектирования” и кажется мне крайне удачным). Паттерн проектирования “Стратегия” - 4Мы выделили интересующее нас семейство алгоритмов (виды заправки машин) в отдельных интерфейс с несколькими реализациями. Мы отделили их от самой сущности автомобиля. Поэтому теперь, если нам понадобится внести какие-то изменения в тот или иной процесс заправки, это никак не затронет наши классы машин. Что касается взаимозаменяемости, то для ее достижения нам достаточно добавить один метод-сеттер в наш класс 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();
   }
}
Если вдруг детские машины-багги начнут заправлять бензином, наша программа будет готова к такому варианту развития событий :) Вот, собственно, и все! Ты выучил еще один паттерн проектирования, который, несомненно, тебе понадобится и еще не раз выручит в работе над реальными проектами :) До новых встреч!
Комментарии (75)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #3193220 Уровень 1
10 июля 2023
А я так и не понял, как решилась проблема дублирования кода, если теперь для каждого класса с заправляемыми машинами надо создать еще по одному классу. Бред -.-
Kurama Уровень 50
26 ноября 2022
Очень понятно и интересно, но я так и не понял, как решилась проблема с отсутствием метода fill() у детского багги
Alexander Уровень 40
6 сентября 2022
Это лекция - пересказ первых 70 страниц книги Head First Design Patterns. Если хотите больше узнать про паттерны, советую прочитать эту книгу. В ней всё объясняется намного понятнее и раскладывается прям по полочкам. Книга классная, так и хочется читать (я уже на половине)
Сонмониус Уровень 39
21 июля 2022
Хорошая стратегия, но игрушечную машинку наследовать от Car все равно варварство, если программа предполагает что там не только игрушечные машины есть)
25 мая 2022
наследование игрушечной машинки от Car это прямое нарушение правила подстановки Барбары Лисков
imik Уровень 35
30 декабря 2021
Безусловно интересный паттерн, но в некоторых случаях, мне кажется, проще было сделать дефолтный метод в интерфейсе.
Akhmarzhan Islambek Уровень 36
16 марта 2021
Получается, что к концу лекций ранее созданные классы Sedan, HybridAuto , F1Car становятся лишними
Михаил Уровень 37
6 февраля 2021
В приведенном примере проблема для багги на батарейках не решена. Багги наследуется от Auto, получает поле fillStrategy. Дальше что? fillStrategy = null? Ну круто, чо.
Pig Man Уровень 41
30 декабря 2020

Это те самые «велосипеды», которые ни в коем случае не нужно изобретать самому
Но если никто не будет изобретать велосипеды, кто же их изобретет?)