Шаблон Стратегия

В этой статье рассмотрим шаблон «Стратегия». Он используется (например) тогда, когда композиция выгоднее наследования. А также, когда наследнику нужна возможность менять поведение время от времени.

Чтобы понять шаблон, необходимо знать, что такое наследование, полиморфизм, абстрактный класс и интерфейс, а также быть готовым пустить всё это в ход в процессе разработки.

Пример №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.

Шаблон Стратегия: 1 комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *