Здесь я хотел бы рассказать о паттерне стратегии.
В качестве примера, рассмотрим следующий класс:
abstract class Duck
{
public void quack()
{
Console.Out.WriteLine("Quack!");
}
public void swim()
{
Console.Out.WriteLine("Bulk!");
}
abstract public void display();
}
Это наш класс Утка, которая умеет крякать и плавать, при этом все утки, которые наследуют этот класс умеют тоже самое. Метод
display у нас является абстрактным, так как каждая утка имеет свой образ и требует конкретной реализации.
Ниже я показал диаграмму классов (у нас две новых утки:
MallarDuck и
RedHeadDuck, которые наследуют абстрактный класс
Duck):
Класс MallarDuck:
using System;
class MallarDuck : Duck
{
public override void display()
{
Console.Out.WriteLine("I am Mallar Duck!");
}
}
Класс RedHeadDuck:
using System;
class RedHeadDuck : Duck
{
public override void display()
{
Console.Out.WriteLine("I am Red Head Duck!");
}
}
Запускаем наш первый пример:
using System;
class Program
{
static void Main(string[] args)
{
MallarDuck MyDuckMallar = new MallarDuck();
RedHeadDuck MyDuckRedHead = new RedHeadDuck();
MyDuckMallar.display();
MyDuckRedHead.display();
MyDuckMallar.quack();
MyDuckRedHead.quack();
Console.In.ReadLine();
}
}
Результатом запуска будет:
Какие проблемы сразу видны?
Если мы заходим дать возможность нашим уткам летать, то необходимо будет добавить в наш абстрактный класс
Duck метод
fly(), при этом его конкретную реализацию унаследуют все классы, т.е. даже те утки, которые не должны летать. Для них нам придется переписывать данный метод и делать его пустым (например деревянная утка не должна уметь летать). При добавлении других возможностей в будущем, каждый раз придется реализовывать пустые методы.
Какой Выход?
Возможно, стоит вынести метод
fly() в интерфейс? Тогда мы достигним того, что деревянная утка не должна летать, так как она не будет реализовывать интерфейс метода
fly().
Но тут возникает другая проблема: дублирование кода. Так как интерфейс не может содержать конкретную реализацию метода, то нам придется реализовывать метод
fly() для каждого класса. При каких-либо изменениях, необходимо будет внести их в каждый класс.
Основным принципом проектирования является: Выделить части приложения, которые могут изменяться и отделить их от тех, которые являются постоянными. Иными словами нам нужно выделить изменяющийся код так, чтобы не изменять постоянную часть в дальнейшем.
Принцип проектирования: Программируйте на уровне интерфейсов.
Руководствуясь этими двумя принципами переделаем наш пример.
Мы знаем, что метод
fly() может быть не у всех субклассов класса
Duck, поэтому нам необходимо выделить его в отдельный набор классов, так же как и метод кряканья
quack().
Руководствуясь первый принципом мы должны создать интерфейс для методы
fly()
Получаем следующую диаграмму классов:
Давайте посмотрим на изменившийся класс
Duck:
abstract class Duck
{
//переменные интерфейсов,
//через которые будет происходит делегирование действий
protected IFlyBehavior flyBehavior;
protected IQuackBehavior quackBehavior;
//создаем динамическое изменение поведения уток
public void setFlyBehavior(IFlyBehavior fb)
{
flyBehavior = fb;
}
public void setQuackBehavior(IQuackBehavior qb)
{
quackBehavior = qb;
}
public void performQuack()
{
quackBehavior.quak(); //делегируем
}
public void performFly()
{
flyBehavior.fly(); //делегируем
}
public void swim()
{
Console.Out.WriteLine("Bulk!");
}
abstract public void display();
}
Мы вынесли методы полета и кряканья в интерфейсы, а также сделали набор классов, которые являются реализацией разных алгоритмов полета и кряканья наших уток. Далее мы объявили две переменных в абстрактном классе
Duck()
protected IFlyBehavior flyBehavior;
protected IQuackBehavior quackBehavior;
Которые ссылаются на классы, реализующие данные методы. Т.е. мы можем присвоить любой алгоритм кряканья или полета.
Вызов делегирование происходит через методы:
public void performQuack()
{
quackBehavior.quak(); //делегируем
}
public void performFly()
{
flyBehavior.fly(); //делегируем
}
С помощью двух сеттеров мы можем динамически на лету присваивать разные алгоритмы нашим классам.
Метод
display() мы оставили абстрактным, так как для каждой утки он будет разным, а метод
swim() для всех будет одинаков.
Ниже приведу описание остальных классов:
//интерфейс метода Полета
interface IFlyBehavior
{
void fly();
}
// интерфейс метода кряканья
interface IQuackBehavior
{
void quak();
}
//класс, реализующий невозможность летать
class FlyNoWay: IFlyBehavior
{
public void fly()
{
Console.Out.WriteLine("I can not fly!");
}
}
//Класс, реализующий алгоритм полета ракеты
class FlyRocket: IFlyBehavior
{
public void fly()
{
Console.Out.WriteLine("I fly as rocket!");
}
}
//Класс, реализующий алгоритм обычного кряканья
class Quack : IQuackBehavior
{
public void quak()
{
Console.Out.WriteLine("quack!");
}
}
//Класс, реализующий алгоритм бесшумного кряканья
class MuteQuack: IQuackBehavior
{
public void quak()
{
Console.Out.WriteLine("Empty Quack!");
}
}
}
//Класс дикой утки
public MallarDuck()
{
quackBehavior = new Quack();
flyBehavior = new FlyRocket();
}
public override void display()
{
Console.Out.WriteLine("I am Mallar Duck!");
}
}
//Класс красноголовой утки
class RedHeadDuck : Duck
{
public RedHeadDuck()
{
quackBehavior = new Quack();
flyBehavior = new FlyRocket();
}
public override void display()
{
Console.Out.WriteLine("I am Red Head Duck!");
}
}
Давайте теперь сделаем класс деревянной утки, для этого сделаем класс-модель утки, которая по умолчанию не умеет ничего делать:
class ModelDuck : Duck
{
public ModelDuck()
{
quackBehavior = new MuteQuack();
flyBehavior = new FlyNoWay();
}
public override void display()
{
Console.Out.WriteLine("I am model!");
}
}
Теперь давайте вызовем все наши классы, а также посмотрим как можно динамически изменять алгоритмы:
class Program
{
static void Main(string[] args)
{
MallarDuck MyDuckMallar = new MallarDuck();
RedHeadDuck MyDuckRedHead = new RedHeadDuck();
//Наша дикая утка
MyDuckMallar.display();
MyDuckMallar.performFly();
MyDuckMallar.performQuack();
//красноголовая утка
MyDuckRedHead.display();
MyDuckRedHead.performFly();
MyDuckRedHead.performQuack();
//модель утки, которой мы поменяли алгоритм
//кряканья и алгоритм полета
ModelDuck myThreeDuck = new ModelDuck();
myThreeDuck.display();
myThreeDuck.performFly();
myThreeDuck.performQuack();
myThreeDuck.setFlyBehavior(new FlyRocket());
myThreeDuck.setQuackBehavior(new Quack());
myThreeDuck.performFly();
myThreeDuck.performQuack();
Console.In.ReadLine();
}
}
Вот это да! Мы смогли избавиться от дублировния кода, теперь мы можем независимо разрабатывать алгоритмы и присваивать их любым уткам, а также мы смогли уйти от наследования, которое принудительно добавляло все методы во все субклассы!
Подобные связи между двумя классами называются композицией, т.е. поведение не наследуется, а предоставляется правилами выбора.
Принцип проектирования: Отдавайте предпочтение композиции перед наследованием.
Композиция позволяет не только инкапсулировать семейство алгоритмов, но и изменять поведение во время выполнения.
Паттерн СТРАТЕГИЯ определяет семейство алгоритмов, инкапсулирует каждый из них и обеспечивает их взаимозаменяемость. Он позволяет модифицировать алгоритмы независимо от их использования на стороне клиента.