C#. Паттерны проектирования. Стратегия. Часть 1.
В данной серии уроков мы реализуем на C# паттерны проектирования, взятые из книги «Эрик Фриман, Элизабет Робсон. Паттерны проектирования». Начнем с паттерна «Стратегия». Но сначала (прежде чем я дам вам конкретные примеры кода) небольшое изложение главы этой книги, где описан данный паттерн.
Итак, в некой организации был разработан симулятор «Утиное озеро» — прикольная такая программа, которая показывала на экране озеро с плавающими утками. И тут топ менеджеру пришла в голову гениальная идея «А пусть утки еще и летают». Программист Джо сказал:
— Да без проблем, я специалист по ООП, вмиг это реализую.
И точно, не прошло и недели, как программист бодро отрапортовал, что все готово. Он сдал программу и с чувством выполненного долга пошел пить пиво. И тут звонит разгневанный тим-лид и орет в трубку:
— Это что за приколы, Джо? Я показываю нашу программу топ менеджеру, а там резиновые утки по экрану летают! Это типа шутка такая?
— Ладно, разберусь, — отвечает программист. В его голосе звучат нотки досады от того, что его оторвали от увлекательного процесса употребления пенного напитка.
Выдать глюк за фичу не прокатило. Пришлось исправлять. Проанализировав код, Джо понял, где ошибка. В его архитектурном решении спрайты уток отображали объекты классов наследников от абстрактного класса Duck (утка). В частности, RubberDuck – резиновая тука, AliveDuck – живая утка и WoodenDuck – деревянная утка. Класс Duck включал в себя абстрактный метод, который отображал утку и наследовался каждым дочерним классов, где был реализован по-своему. Для класса RubberDuck отображалась резиновая утка, для AliveDuck – настоящая, для WoodenDuck – деревянная. Так как каждая утка умела плавать, и у всех процесс плаванья одинаков, то у абстрактного класса Duck был реализован метод Swim(). Он просто наследовался, без переопределения.
Что сделал Джо? Он просто тупо добавил в класс Duck метод Fly(). И его унаследовали все дочерние классы, в том числе RubberDuck, отчего резиновые утки тоже стали летать. Как исправить? Сначала программист решил просто сделать метод Fly() только у класса AliveDuck. Но от этой идеи пришлось отказаться. Все утки хранились в списке элементов типа Duck, которые при реализации перемещений (плавать, летать) перебирались итератором и вызывался соответствующий метод (Swim или Fly). Если оставить Fly() только у AliveDuck, элемент Duck придется проверять, не является ли он AliveDuck, и если да, то конвертировать в AliveDuck и вызывать Fly(). Надо ли говорить, что это признак дурного тона в программировании, и нарушение фундаментальных принципов ООП.
Если кто не понял, почему первая идея Джо – дурной тон в ООП? Давайте представим, что топ-менеджер захотел, чтобы утки еще и крякали. Нужно добавить метод Quack(). И тоже только для AliveDuck. Ну допустим, вызов этого метод мы добавили в уродливый кусок кода (костыль), где проверяется тип и преобразуется к AliveDuck. А потом руководство захотело добавить других крякающих животных. И тут мы влипли. Потому что костылей придется добавить еще много, что быстро приведет к тому, что наш красивый (до этого момента) ООП код превратиться в уродливое спагетти костылей, которое будет очень трудно сопровождать.
Возможно, кому-то все еще непонятно. Ладно, тогда привожу конкретные примеры кода. Мы сейчас будем писать игру. Какой будет геймплей, подумаем потом, сначала напишем прототип. В игре у нас будет два типа юнитов: Gamer и Monster.
Вот у нас абстрактный класс Unit:
/// <summary>
/// Класс юнита
/// </summary>
public abstract class Unit
{
/// <summary>
/// Имя файла
/// </summary>
private string _file_name;
/// <summary>
/// Изображение юнита
/// </summary>
private Image _image;
/// <summary>
/// Ссылка на ядро
/// </summary>
protected Core _core;
/// <summary>
/// Координата x
/// </summary>
protected int _x;
/// <summary>
/// Координата y
/// </summary>
protected int _y;
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_core">Ссылка на ядро</param>
public Unit(Core a_core)
{
_core = a_core;
_file_name = get_file_name();
_image = Image.FromFile(_core.get_path()+_file_name);
_x = Core.rnd.Next(_core.width-_image.Width);
_y = Core.rnd.Next(_core.height-_image.Height);
}
/// <summary>
/// Получить имя файла
/// </summary>
/// <returns>Имя файла</returns>
protected abstract string get_file_name();
/// <summary>
/// Случайное перемещение юнита
/// </summary>
public virtual void walk()
{
_x += Core.rnd.Next(6) - 3;
_y += Core.rnd.Next(6) - 3;
if (_x < 0) _x = 0;
if (_x >= _core.width) _x = _core.width - 1;
if (_y < 0) _y = 0;
if (_y >= _core.height) _x = _core.height - 1;
}
/// <summary>
/// Прорисовка юнита
/// </summary>
/// <param name="gr">Графическое поле</param>
public virtual void draw(Graphics gr)
{
gr.DrawImage(_image, _x, _y, _image.Width, _image.Height);
}
}
Как видим, у него реализован виртуальный метод walk() – идти, и draw() – нарисовать, который в данный момент просто отображает в виде спрайта ранее загруженную маленькую картинку. Эта картинка храниться в файле с именем, который возвращает метод get_file_name(), у класса юнита он тоже абстрактный.
Теперь реализуем класс игрок (Gamer):
/// <summary>
/// Игрок
/// </summary>
public class Gamer : Unit
{
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_core">Ссылка на ядро</param>
public Gamer(Core a_core) : base(a_core)
{
}
/// <summary>
/// Получить имя файла картинки
/// </summary>
/// <returns>Имя файла</returns>
protected override string get_file_name()
{
return "mainunit.bmp";
}
}
Как видим, у него мы только реализовали get_file_name(), ибо все остальное у нас уже реализовано в родительском классе Unit.
Аналогично реализовываем Monster:
/// <summary>
/// Класс монстра
/// </summary>
public class Monster : Unit
{
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_core">Ссылка на ядро</param>
public Monster(Core a_core) : base(a_core)
{
}
/// <summary>
/// Получить имя файла кратинки
/// </summary>
/// <returns>Имя файла</returns>
protected override string get_file_name()
{
return "spider.bmp";
}
}
В качестве движка игры создадим класс Core:
/// <summary>
/// Класс движка
/// </summary>
public class Core
{
/// <summary>
/// Юниты
/// </summary>
private List<Unit> _units;
/// <summary>
/// Ширина
/// </summary>
private int _width;
/// <summary>
/// Высота
/// </summary>
private int _height;
/// <summary>
/// Ширина
/// </summary>
public int width
{
get
{
return _width;
}
}
/// <summary>
/// Высота
/// </summary>
public int height
{
get
{
return _height;
}
}
/// <summary>
/// Генератор случайных чисел
/// </summary>
public static Random rnd = new Random();
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_width">Ширина</param>
/// <param name="a_height">Высота</param>
public Core(int a_width, int a_height)
{
_height = a_height;
_width = a_width;
_units = new List<Unit>();
for(int i=1; i<=10; i++)
{
if (rnd.NextDouble() > 0.5)
{
_units.Add(new Gamer(this));
}
else
{
_units.Add(new Monster(this));
}
}
}
/// <summary>
/// Шаг моделирования
/// </summary>
public void step()
{
foreach (Unit unit in _units) unit.walk();
}
/// <summary>
/// Нарисовать юнитов
/// </summary>
/// <param name="gr">Графическое поле</param>
public void draw(Graphics gr)
{
foreach (Unit unit in _units) unit.draw(gr);
}
/// <summary>
/// Получить путь к изображениям
/// </summary>
/// <returns>Путь</returns>
public string get_path()
{
return @"D:\Самообразование\Паттерны\Спрайты\";
}
}
Теперь идем на форму, кидаем туда PictureBox:

Я обозвал PictureBox: pbGameField, а его обработчик события Paint:
pbGameField_Paint. Еще на форме есть компонент Timer вот с такими настройками:

Его обработчик timer_Tick назван timer_Tick.
А вот и сам код формы:
using DemoPattern.General;
using DemoPattern.Units;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace DemoPattern
{
public partial class MainForm : Form
{
/// <summary>
/// Класс ядра
/// </summary>
private Core _core;
public MainForm()
{
InitializeComponent();
_core = new Core(pbGameField.Width, pbGameField.Height);
timer.Enabled = true;
}
private void pbGameField_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(Color.White);
_core.draw(e.Graphics);
}
private void timer_Tick(object sender, EventArgs e)
{
_core.step();
pbGameField.Refresh();
}
}
Результат работы программы – хаотично движущиеся по экрану пауки и человечки:

А теперь о том, почему данное решение плохое. Предположим, мы захотели добавить в игру еще один тип юнитов Frog, который умеет прыгать (jump). Ок, создаем класс:
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_core">Ссылка на ядро</param>
public Frog(Core a_core) : base(a_core)
{
}
/// <summary>
/// Быстрое случайное перемещение юнита
/// </summary>
public virtual void jump()
{
if (Core.rnd.Next(10) == 1)
{
_x += Core.rnd.Next(50) - 25;
_y += Core.rnd.Next(50) - 25;
if (_x < 0) _x = 0;
if (_x >= _core.width) _x = _core.width - 1;
if (_y < 0) _y = 0;
if (_y >= _core.height) _x = _core.height - 1;
}
}
/// <summary>
/// Получить изображение
/// </summary>
/// <returns>Изображение</returns>
protected override string get_file_name()
{
return "frog.png";
}
Теперь добавим юнит в начальный экран, переписав конструктор Core:
/// <summary>
/// Конструктор
/// </summary>
/// <param name="a_width">Ширина</param>
/// <param name="a_height">Высота</param>
public Core(int a_width, int a_height)
{
_height = a_height;
_width = a_width;
_units = new List<Unit>();
for(int i=1; i<=10; i++)
{
int sel = rnd.Next(3);
switch(sel)
{
case 0:
_units.Add(new Gamer(this));
break;
case 1:
_units.Add(new Monster(this));
break;
case 2:
_units.Add(new Frog(this));
break;
}
}
}
Запустим и посмотрим:

Лягушка у нас не прыгает. Почему? Нужно еще в методе step() класса Core предусмотреть вызов jump(). И тут опаньки… Вставить то некуда. Мы перебираем все элементы _units, которые представляют собой элементы абстрактного класса Unit. А у него нету метода jump(), он объявлен у класса Frog. Как быть? Перенести метод jump() в класс Unit, а потом вызвать в цикле опроса (это ошибка, но ее я допускаю намеренно в целях демонстрации):
/// <summary>
/// Шаг моделирования
/// </summary>
public void step()
{
foreach (Unit unit in _units)
{
unit.jump();
unit.walk();
}
}
И теперь прыгают у нас все. Не только лягушки, но и монстры, и человечки. Как решить эту проблему? Переносим метод jump() обратно в класс Frog, и делаем в step проверку на вид класса:
/// <summary>
/// Шаг моделирования
/// </summary>
public void step()
{
foreach (Unit unit in _units)
{
Frog frog = unit as Frog;
if(frog!=null) frog.jump();
unit.walk();
}
}
Теперь работает правильно. Но то, что мы сделали – это кривой, нет, даже кривущий костыль. Так делать нельзя!!! Почему? А если мы создадим еще один вид юнита, со своими методами? Опять будем добавлять в код костыль? А если мы решили создать еще один тип поведения для конкретного юнита? Например, захотели, чтобы лягушка могла ловить мух. Добавим метод catch_fly() в класс Frog и таким же костыльным способом пропишем его вызов в методе step? Так у нас вскоре костыли разрастутся в плохо управляемый запутанный спагетти-код. Тогда как быть? А об этом вы узнаете в следующей части статьи.

Comments
So empty here ... leave a comment!