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!

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

Sidebar