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, 21:37
А я так и не понял, как решилась проблема дублирования кода, если теперь для каждого класса с заправляемыми машинами надо создать еще по одному классу. Бред -.-
Mr.Selby
Уровень 18
13 сентября 2023, 12:22
1) Теперь при создании новых классов можно при необходимости создать новое поведение для метода fill(); 2) Если скажем поведение у HybridFill авто поменяется нам достаточно изменить поведение только в этом классе, а не искать в программе каждый класс которому мы писали такое поведение.
Kurama
Уровень 50
26 ноября 2022, 22:47
Очень понятно и интересно, но я так и не понял, как решилась проблема с отсутствием метода fill() у детского багги
NAUM
Уровень 40
11 декабря 2022, 16:41
Никак. Поле не инициализировано. Если кто-то захочет заправить дискую машину - получит ошибку.
YahveSmerciful Первый заместитель Главы в СБУ Expert
31 января 2023, 21:58
с помощью сеттера мы можем изменять поле FillStrategy , и после этого можем присваивать это поле разным другим дочерним объектам класса Auto
Kurama
Уровень 50
3 февраля 2023, 13:17
Проблема в была в том, что у детского багги нет топливного бака, а значит метод fill() ему не нужен. В итоге мы либо получаем ошибку, либо пишем какой-то код, который ничего не должен делать, но уведомить об этом пользователя...
YahveSmerciful Первый заместитель Главы в СБУ Expert
3 февраля 2023, 21:10
но все же детскому багги все равно можно присвоить это значение без всяких проблем )
Daniel CEO в BicycleInventionAcad
16 февраля 2023, 01:33
Вот так при помощи простых манипуляций мы научились из буханки белого (или черного) хлеба делать трамвай. Остался только один вопрос: зачем нам это нужно?
John Android Developer
6 сентября 2022, 18:43
Это лекция - пересказ первых 70 страниц книги Head First Design Patterns. Если хотите больше узнать про паттерны, советую прочитать эту книгу. В ней всё объясняется намного понятнее и раскладывается прям по полочкам. Книга классная, так и хочется читать (я уже на половине)
9 февраля, 15:39
Она у меня давно в коллекции валяется, вот из-за твоего коммента сейчас начал читать)
Сонмониус Full Stack Developer
21 июля 2022, 22:01
Хорошая стратегия, но игрушечную машинку наследовать от Car все равно варварство, если программа предполагает что там не только игрушечные машины есть)
25 мая 2022, 09:23
наследование игрушечной машинки от Car это прямое нарушение правила подстановки Барбары Лисков
Anonymous #3068853
Уровень 3
26 мая 2022, 23:04
Если Auto сделать абстрактным, то нарушения не будет (так как будет невозможно создать объект типа Auto).
imik
Уровень 35
30 декабря 2021, 08:58
Безусловно интересный паттерн, но в некоторых случаях, мне кажется, проще было сделать дефолтный метод в интерфейсе.
Akhmarzhan Islambek
Уровень 36
16 марта 2021, 05:32
Получается, что к концу лекций ранее созданные классы Sedan, HybridAuto , F1Car становятся лишними
Михаил
Уровень 37
6 февраля 2021, 06:24
В приведенном примере проблема для багги на батарейках не решена. Багги наследуется от Auto, получает поле fillStrategy. Дальше что? fillStrategy = null? Ну круто, чо.
Andrei Po
Уровень 41
18 февраля 2021, 12:52
ничто не мешает, сделать отдельный класс -реализацию FillStrategy, который будет подходить именно для "багги на батарейках", и в этой модели авто в переменную класса setter_ом установить именно подходящий класс, реализацию FillStrategy.
Михаил
Уровень 37
19 февраля 2021, 09:01
И какова будет реализация?
public void fill() {
    System.out.println("А, забыл, машинки на батарейках не заправляются же, точно.");
}
Можно под заправкой в данном контексте понимать замену батареек, но это уже лексическое извращение.
Andrei Po
Уровень 41
19 февраля 2021, 09:36
улыбнуло. это будет не бОльшим "извращением", чем считать заправкой .fill() зарядку батареек обычных электро-автомомобилей. P.S. но главное : "А, забыл, было бы желание спорить и несоглашаться, будучи увереным только в своей правоте,- и тогда считать "извращением" и придираться можно к чему угодно" )). P.P.S. просто шутка, не чтобы вызвать агрессию или продолжать спор.
Михаил
Уровень 37
19 февраля 2021, 11:21
Тут вы правы. Придираюсь к мелочам, такой дизайн нервной системы🤓
Nik Grape
Уровень 48
13 октября 2021, 01:52
А вопрос действительно открыт. Если поле FillStrategy все равно попадет к незаправляемым машинам, то чем это лучше переопределения метода? если
public void fill() {
    System.out.println("меняем ААА батарейки");
}
В таком случае наследование метода вполне подходит, наша же цель была избавиться от этого метода в некоторых наследниках, а в некоторых оставить как я понял получается fillStrategy = null единственное решение? тогда наверное лучше было бы
public void fill() {
    throw new RuntimeException("незаправляемая машина");
}
или вроде того. Чесно говоря приемуществ потерна не понял к сожалению((
Nik Grape
Уровень 48
13 октября 2021, 01:58
А вообще почитал комменты ниже, есть смысл чтоб быстро менять реализацию метода в некоторых классах, подсунул туда нужную стратегию и реализация метода fill() изменилась удобно согласен. Но надо сделать акцент что тут именно в скорости подмены реализации смысл
Михаил
Уровень 37
31 октября 2021, 13:31
Не только в скорости. Можно загружать классы в рантайме. То есть, можно изменить код класса стратегии, перекомпилировать его и перезагрузить на горячую, не останавливая программу.
SERGEY
Уровень 31
29 марта 2022, 17:25
Чтобы не было метода fill() у не заправляемых машин, достаточно создать новый класс FillableAuto и в него уже добавить поле FillStrategy. Подход напоминает абстрактный метод, но в случае с абстрактным методом, нам придется в каждом новом классе - наследнике дублировать код, а с помощью стратегии только выбирать стратегию реализации. Ну и в случае изменения класса, мы соответственно не переписываем реализацию, а меняем ее стратегию. Плюс в случае, когда мы поменяем стандарт процесса заправки, нам так-же нужно поменять ее реализацию в стратегии, тем самым 1. не вмешиваясь в логику клиента, 2. не переписывая код у всех дочерних классов.
Art09
Уровень 35
20 мая 2022, 04:37
public void fill(Class<? extends FillStrategy> clazz){ if (FillStrategy.class.isAssignableFrom(clazz)){ fillStrategy.fillFull(); }else { System.out.println("Не нужно заправлять ничего"); } } Проверяем наследника, но в мэйн нужно указывать стратегию класса, в методе fill. sedan.fill(StandartStrategy.class); hybrid.fill(HybridStrategy.class); f1.fill(F1Strategy.class); toys.fill(BuggiesCar.class); Если запустить все работает корректно.
Anonymous #3193220
Уровень 1
10 июля 2023, 21:42
Тогда непонятно зачем вообще нужны все эти стратегии. Проще сразу в методе fill тогда уж описать всю логику заправки детского багги.
Pig Man Главная свинья в Свинарнике
30 декабря 2020, 17:49
Это те самые «велосипеды», которые ни в коем случае не нужно изобретать самому
Но если никто не будет изобретать велосипеды, кто же их изобретет?)
Илья Backend Developer в СберТех
7 января 2021, 13:32
велосипед-то уже изобретен, зачем его еще раз изобретать) а вот если бы это бы был фушмалк, то тогда да)
Pig Man Главная свинья в Свинарнике
7 января 2021, 15:08
А новые модели велосипедов кто будет изобретать? А вообще, я к тому, что в java есть много библиотек и классов, позволяющих сделать одно и то же, просто что-то подходит лучше под одно, что-то под другое, а что-то просто лучше, потому что использует современный подход, а другое - устарело. Так что придумывать новые способы, сделать старые вещи - нужно
Илья Backend Developer в СберТех
8 января 2021, 17:57
это ты уже про другое говоришь, а здесь имеется ввиду делать с нуля тот же самый арайлист