В этой статье рассмотрим шаблон «Стратегия». Он используется (например) тогда, когда композиция выгоднее наследования. А также, когда наследнику нужна возможность менять поведение время от времени.
Чтобы понять шаблон, необходимо знать, что такое наследование, полиморфизм, абстрактный класс и интерфейс, а также быть готовым пустить всё это в ход в процессе разработки.
Пример №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, а животных в фабрику.