В этой статье рассмотрим шаблон «Стратегия». Он используется (например) тогда, когда композиция выгоднее наследования. А также, когда наследнику нужна возможность менять поведение время от времени.
Чтобы понять шаблон, необходимо знать, что такое наследование, полиморфизм, абстрактный класс и интерфейс, а также быть готовым пустить всё это в ход в процессе разработки.
Пример №1. Композиция лучше наследования
Сначала рассмотрим вариант, когда композиция выгоднее наследования.
Начало — переопределение методов
Допустим мы разрабатываем игру с животными (класс Animal). Животные могут двигаться (move) и есть (eat). Требования заказчика время от времени изменяются и дополняются, но сначала у нас есть животные Tiger и Bird. Они оба двигаются и едят, поэтому логично унаследовать их от класса Animal и реализовать в них методы move() и eat(). При этом класс Animal делаем абстрактным, потому что как такового «просто животного» нет:
public abstract class Animal {
public abstract void move();
public abstract void eat();
}
Класс Tiger:
public class Tiger extends Animal {
@Override
public void move() {
System.out.println("run");
}
@Override
public void eat() {
System.out.println("meet");
}
}
Класс Bird:
public class Bird extends Animal {
@Override
public void move() {
System.out.println("fly");
}
@Override
public void eat() {
System.out.println("grass");
}
}
В родительском классе Animal мог быть еще какой-нибудь метод log(), который одинаков у всех животных и который не надо переопределять. Это было бы нормально, но опустим это, чтобы сосредоточиться на проблеме.
Пока все идет хорошо, никаких проблем с кодом нет. Животное Animal проявляет полиморфизм — ведет себя по разному, в зависимости от того, кто оно: Tiger или Bird. Тигр бегает и есть мясо, птица летает и травоядна. При этом вызываем мы эти действия абсолютно одинаково как для тигра, так и для птицы — методами move() и eat() объекта Animal:
Animal bird = new Bird(); bird.move(); bird.eat(); Animal tiger = new Tiger(); tiger.move(); tiger.eat();
Но допустим, приходит требование добавить еще одно животное — оленя (Deer). Олень бегает и ест траву:
public class Deer extends Animal {
@Override
public void move() {
System.out.println("run");
}
@Override
public void eat() {
System.out.println("grass");
}
}
Вот тут и возникла проблема — получилось нехорошо, так как код дублируется.
Получили дублирующийся код
Стратегия движения «бег», продублирована в двух методах move() — у оленя (Deer) и у тигра (Tiger). То же самое с едой — стратегия травоядного питания продублирована в методах eat() у птицы (Bird) и у оленя (Deer):

Если стратегия (то, что внутри метода move() или run()) поменяется, то код придется менять в нескольких местах. У нас эта всего одна строка:
System.out.println(...)
У нас три класса, но в реальном проекте найти все эти участки может быть сложно. Надо вспоминать, где они, и на глазок проверять их на идентичность.
Какие-то наследники могут вообще не реализовывать некоторые стратегии. Например, появляется еще одно игрушечное животное — ToyAnimal. Оно не ест и не двигается:
public class ToyAnimal extends Animal {
@Override
public void move() {
}
@Override
public void eat() {
}
}
Появились методы, которые переопределены, чтобы ничего не делать. Выглядит ужасно.
Перейдем, наконец, к решению проблемы — к шаблону «Стратегия».
Рефакторинг с помощью шаблона «Стратегия»
Суть в том, чтобы вынести все стратегии делания (и неделания) в отдельные классы, реализующие эту стратегию. Например, стратегию движения с помощью бега, полета и стратегию полной неподвижности — вынести в классы Run, Fly и NoMove, реализующие интерфейс MoveBehaviour. А затем сделать эти стратегии полями Animal и вызывать опосредованно — не просто как сам метод класса Animal, а внутри делать еще обращение к методу стратегии. Ниже показано, как.
Задаем стратегии
Итак, выносим стратегию движения в серию классов, реализующих интерфейс MoveBehaviour:
public interface MoveBehaviour {
void move();
}
Имеем стратегии Run, Fly и NoMove:
public class Run implements MoveBehaviour {
@Override
public void move() {
System.out.println("run");
}
}
public class Fly implements MoveBehaviour {
@Override
public void move() {
System.out.println("fly");
}
}
public class NoMove implements MoveBehaviour {
@Override
public void move() {
}
}
Аналогично со стратегией питания EatBehaviour:
public interface EatBehaviour {
void eat();
}
Реализации стратегии питания EatBehaviour — стратегии EatMeet, EatGrass, NoEat:
public class EatMeet implements EatBehaviour {
@Override
public void eat() {
System.out.println("meet");
}
}
public class EatGrass implements EatBehaviour {
@Override
public void eat() {
System.out.println("grass");
}
}
public class NoEat implements EatBehaviour {
@Override
public void eat() {
}
}
Из конкретных классов-животных эти методы вынесены, теперь эти классы пусты:
public class Tiger extends Animal {}
public class Bird extends Animal {}
public class Deer extends Animal {}
public class ToyAnimal extends Animal {}
Зато методы вызываются опосредованно через методы Animal. Ниже показано, как.
Делаем стратегии полями родительского класса
В Animal добавляем поля, содержащие стратегии MoveBehaviour и EatBehaviour:
public abstract class Animal {
private MoveBehaviour moveBehaviour;
private EatBehaviour eatBehaviour;
public void doMove() {
this.moveBehaviour.move();
}
public void doEat() {
this.eatBehaviour.eat();
}
...тут сеттеры
}
И добавляем методы doEat(), doMove(), которые обращаются к стратегии и вызывают ее к действию.
Еще должна быть возможность назначить конкретную стратегию конкретному животному. Для этого в классе Animal предусмотрены сеттеры стратегий:
public abstract class Animal {
...см. выше
public void setMoveBehaviour(MoveBehaviour moveBehaviour) {
this.moveBehaviour = moveBehaviour;
}
public void setEatBehaviour(EatBehaviour eatBehaviour) {
this.eatBehaviour = eatBehaviour;
}
}
Воспользуемся этими сеттерами в следующем подразделе.
Назначаем нужные стратегии конкретным животным и вызываем их
Мы создаем животных и каждому назначаем свою стратегию движения и питания:
Animal tiger = new Tiger(); tiger.setMoveBehaviour(run); tiger.setEatBehaviour(eatMeet);
Вызов будет таким:
tiger.doEat(); tiger.doMove();
Конечно, стратегии надо предварительно создать. Весь код, включая создание и использование стратегий, выглядит так:
public class Main {
public static void main(String[] args) {
MoveBehaviour run = new Run();
MoveBehaviour fly = new Fly();
MoveBehaviour noMove = new NoMove();
EatBehaviour eatGrass = new EatGrass();
EatBehaviour eatMeet = new EatMeet();
EatBehaviour noEat = new NoEat();
Animal bird = new Bird();
bird.setEatBehaviour(eatGrass);
bird.setMoveBehaviour(fly);
bird.doEat();
bird.doMove();
Animal tiger = new Tiger();
tiger.setMoveBehaviour(run);
tiger.setEatBehaviour(eatMeet);
tiger.doEat();
tiger.doMove();
Animal deer = new Deer();
deer.setEatBehaviour(eatGrass);
deer.setMoveBehaviour(run);
deer.doEat();
deer.doMove();
Animal toyAnimal = new ToyAnimal();
toyAnimal.setEatBehaviour(noEat);
toyAnimal.setMoveBehaviour(noMove);
toyAnimal.doMove();
toyAnimal.doEat();
}
}
Обратите внимание, что стратегии теперь легко менять. Птице можно назначить стратегию движения «бег», а оленю — «полет». В данном примере это не целесообразно, но в следующем примере — просто необходимо.
Пример №2. Наследнику нужна возможность менять поведение
В этом примере у нас игра, в которой герои могут менять оружие.
Есть родительский класс Character (герой), от которого наследуются рыцарь (Knight), король (King), королева (Queen) и тролль (Troll). Сейчас они воюют с разными оружиями: топор (Axe), нож (Knife), меч (Sword) и лук (BowAndArrow):
Но в процессе игры возможен любой расклад. Например, король и королева в какой-то момент окажутся с мечами, а рыцарь и тролль — с ножами:

Потенциально все четыре оружия могут быть одинаковыми.
Шаблон «Стратегия» отлично решает проблему как дублирования поведения, так и его изменения. Второе тоже очень актуально для данного примера.
Создадим стратегию использования оружия, которую герои смогут менять:
public interface WeaponBehaviour {
void useWeapon();
}
Реализации четыре: топор, нож, меч и лук:
public class Axe implements WeaponBehaviour {
@Override
public void useWeapon() {
System.out.println("axe");
}
}
public class Knife implements WeaponBehaviour {
@Override
public void useWeapon() {
System.out.println("knife");
}
}
public class Sword implements WeaponBehaviour {
@Override
public void useWeapon() {
System.out.println("sword");
}
}
public class BowAndArrow implements WeaponBehaviour {
@Override
public void useWeapon() {
System.out.println("bowAndArrow");
}
}
Родительский класс Character содержит поле стратегии weaponBehaviour и сеттер для нее. Стратегия вызывается методом doUse():
public abstract class Character {
private WeaponBehaviour weaponBehaviour;
public void doUseWeapon() {
weaponBehaviour.useWeapon();
}
public void setWeaponBehaviour(WeaponBehaviour weaponBehaviour) {
this.weaponBehaviour = weaponBehaviour;
}
}
В конкретных классах-героях нет методов использования оружия:
public class Knight extends Character {}
public class King extends Character {}
public class Queen extends Character {}
public class Troll extends Character {}
Наконец, создание и вызов стратегий:
public class Main {
public static void main(String[] args) {
//создаем стратегии
WeaponBehaviour sword = new Sword();
WeaponBehaviour knife = new Knife();
WeaponBehaviour axe = new Axe();
WeaponBehaviour bowAndArrow = new BowAndArrow();
//создаем героя
Character knight = new Knight();
//назначаем ему стратегию
knight.setWeaponBehaviour(axe);
//вызываем стратегию
knight.doUseWeapon();
Character king = new King();
king.setWeaponBehaviour(knife);
king.doUseWeapon();
Character queen = new Queen();
queen.setWeaponBehaviour(sword);
queen.doUseWeapon();
Character troll = new Troll();
troll.setWeaponBehaviour(bowAndArrow);
troll.doUseWeapon();
}
}
В отличии от предыдущего примера, в этом примере будет актуально многократно присваивать разные стратегии одному герою, таким образом меняя его оружие.
Исходный код
Мы рассмотрели шаблон стратегия: привели два примера его использования.
Код примеров есть на GitHub.
Стратегии можно было бы логично обьединить в enum, а животных в фабрику.