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? Так у нас вскоре костыли разрастутся в плохо управляемый запутанный спагетти-код. Тогда как быть? А об этом вы узнаете в следующей части статьи.
Продолжение тут: C#. Паттерны проектирование. Стратегия. Часть 2. — Библиотека разработчика Programming Store (programstore.ru)
Comments
So empty here ... leave a comment!