JavaRush /Java блог /Random UA /Паттерн проектування "Стратегія"
Professor Hans Noodles
41 рівень

Паттерн проектування "Стратегія"

Стаття з групи Random UA
Вітання! У попередніх лекціях ми вже зустрічалися з таким поняттям, як «патерн проектування». Якщо ти забув, нагадаємо: цим терміном позначають якесь стандартне рішення поширеного у програмуванні завдання. Паттерн проектування "Стратегія" - 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();
   }
}
Якщо раптом дитячі машини-баггі почнуть заправляти бензином, наша програма буде готова до такого варіанта розвитку подій. Ось, власне, і все! Ти вивчив ще один патерн проектування, який, безперечно, тобі знадобиться і ще не раз виручить у роботі над реальними проектами:) До нових зустрічей!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ