Top.Mail.Ru

C#. Паттерны проектирование. Стратегия. Часть 2.

Начало здесь: C#. Паттерны проектирования. Стратегия. Часть 1. — Библиотека разработчика Programming Store (programstore.ru)

На прошлом уроке мы с вами начали разбирать паттерн «Стратегия». Напомню краткое содержание: В некой организации был разработан симулятор «Утиное озеро». Программист по имени Джо получает задание расширить функционал симулятора, заставив виртуальных уток летать. Он решил, что все это «как два байта об асфальт». Но не тут то было. Разраб сделал все быстро, но допустил баг, который просочился в продакшн. Баг заключался в том, что летали не только живые утки, но резиновые в том числе. А такого быть не должно. Согласитесь, не могут резиновые утки летать. Пришлось исправлять. Как оказалось, причина была в том, что программист у родительского класса Duckреализовал метод Fly() для реализации полета, его унаследовали не только AliveDuck,но и все дочерние классы, в том числе и RubberDuck. Программист хотел было оставить метод только у AliveDuck, а в цикл, где идет вызов метода Fly()вставить проверку, не является ли этот класс AliveDuck, а если да то вызвать у него метод Fly(). Но это дурной тон. Это страшный костыль. Чтобы убедиться в том, что данное решение очень плохое, мы решили повторить опыт Джо, но только вместо эмулятора озера написали заготовку для игры, где у нас есть такие юниты, как Геймер (Gamer) и Монстр (Monster), все они наследники класса Unit. Каждый из этих юнитом может ходить, и ходит каждый юнит одинаково. Поэтому в родительском классе реализован метод walk(). Но заем мы решили добавить класс Frog (лягушка), который умеет прыгать. Но ни Gamer, ни Monster прыгать не умеют. Таким образом, у нас такая же проблема, как и у Джо. Посмотрим, как тот решил ее. И мы решим точно так же.

Сначала Джо решил создать интерфейс IFlyable с единственным методом Fly(). И класс AliveDuck сделать наследником данного интерфейса. Затем в цикле, где идёт вызов методов поведения классов, сделать проверку, поддерживает ли данный объект интерфейс IFlyable. Казалось бы, в чем разница? А в том, что мы теперь не привязаны к конкретному классу. Допустим, у нас появился новый класс AliveDuckOtherKind (живые утки другой породы). Они тоже могут летать. Не проблема, наследуем их от IFlyable и реализуем метод Fly(). Что характерно, в цикл, где у нас вызывается поведение объектов, мы уже ничего больше не добавляем. По сути, мы изменили код в одном месте, то и требует принцип хорошего ООП.  Более того, появись у нас в «озере» н только утки, но и еще какие-нибудь птицы, которые могут летать, мы тоже можем наследовать их от IFlyable и реализовать метод Fly().

Только вот боссу «гениальная» идея программиста не понравилась.

— AliveDuck и AliveDuckOtherKind летают абсолютно одинаково, — сказал он, — у тебя в двух местах будет абсолютно идентичный код. Это недопустимо. Думай дальше.

И Джо стал думать. Сначала он решил ввести еще один уровень иерархии FlyableDuck, у которой реализовать метод Fly(). Сам FlyableDuckбудет наследником Duck, а AliveDuck наследником FlyableDuck. Если у нас появиться AliveDuckOtherKind, то мы тоже унаследуем его от FlyableDuck. Да, FlyableDuck будет реализовывать интерфейс IFlyable. «Таким образом», — рассуждал программист, — «мне удастся избежать дублирования кода». Но как быть, если появиться другая птица, например, чайка (Gull), которая может летать и не умеет плавать. Она не является потомком Duck, значит, не может наследовать и его потомков, в том числе  FlyableDuck. Создавать FlyableGull? Дублирование кода…Эх, надо было C++ учить, — вздохнул Джо, там хоть есть множественные наследование. Я мог бы объявить класса Flyableс единственным методом Fly() и наследовал бы его у любого класса, где нужен Fly().

— Так эмулируй множественное наследование, делов то, — сказал его коллега.

— Точно! – воскликнул Джо, — не я же могу создать объект типа Flyable, и если ему что-то присвоено, вызывать метод Fly().

— Вот вот. Вместо поддерживает ли класс интерфейс IFlyable, ты просто будешь проверять на null поле flyBehaviour.

— Более того, я могу сделать это поле типа абстрактный класс или интерфейс. И на конкретную птицу навешиваешь свое поведение. Если несколько птиц летают одинаково, то у мы используем один и тот же класс наследник, если какая-то птица летает по-другому, реализуем для нее свой класс поведения. Это гениально!

— Это называется паттерн «Стратегия», — сказал коллега.

И так, Джо открыл для себя паттерн «Стратегия». Давайте мы сделаем то же самое в нашей игре: применим паттерн «Стратегия». Для начала создадим интерфейс IUnitBehavior:

    /// <summary>
    /// Поведение юнита
    /// </summary>
    public interface IUnitBehavior
    {
        /// <summary>
        /// Выполнить действие
        /// </summary>
        void ExecAction();
    }

И тут возникает первый вопрос: а каким образом мы сможем, например реализовать поведение walk:

        /// <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>
    public interface IUnitBehavior
    {
        /// <summary>
        /// Выполнить действие
        /// </summary>
        void ExecAction(Unit unit);
    }

И следующий вопрос: а как быть с координатами, которые в данный момент объявлен как protected:

        /// <summary>
        /// Координата x
        /// </summary>
        protected int _x;

        /// <summary>
        /// Координата y
        /// </summary>
        protected int _y;

Сделать их public (или internal)? Нехорошо. Но если поведение меняет только координаты, почему бы тогда координаты не вынести в отдельный класс, который и передавать в «поведение»? Давайте сделаем:

    /// <summary>
    /// Класс координат
    /// </summary>
    public class Coordinates
    {
        /// <summary>
        /// Координата X
        /// </summary>
        public int X;

        /// <summary>
        /// Координата Y
        /// </summary>
        public int Y;

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="x">Координата x</param>
        /// <param name="y">Координата y</param>
        public Coordinates(int x, int y)
        {
            X = x;
            Y = y;
        }
     }

И в который раз мы меняем интерфейс «поведение»:

    /// <summary>
    /// Поведение юнита
    /// </summary>
    public interface IUnitBehavior
    {
        /// <summary>
        /// Выполнить действие
        /// </summary>
        /// <param name="coordinates">Координаты</param>
        void ExecAction(Coordinates coordinates);
    }

Все теперь приступим к абстрактному классу Unit:

    /// <summary>
    /// Класс юнита
    /// </summary>
    public abstract class Unit
    {
        /// <summary>
        /// Имя файла
        /// </summary>
        private string _FileName;

        /// <summary>
        /// Изображение юнита
        /// </summary>
        private Image _Image;

        /// <summary>
        /// Ссылка на ядро
        /// </summary>
        protected Core _Core;

        /// <summary>
        /// Координаты
        /// </summary>
        protected Coordinates _Coordinates;

        /// <summary>
        /// Поведение
        /// </summary>
        protected IUnitBehavior _UnitBehavior;

        /// <summary>
        /// Конструктор 
        /// </summary>
        /// <param name="core">Ссылка на ядро</param>
        /// <param name="unitBehavior">Поведение</param>
        public Unit(Core core, IUnitBehavior unitBehavior) 
        {
            _Core = core;
            _FileName = GetFileName();
            _Image = Image.FromFile(_Core.get_path()+_FileName);
            _UnitBehavior = unitBehavior;
            _Coordinates =new Coordinates(Core.rnd.Next(_Core.Width-_Image.Width),Core.rnd.Next(_Core.Height-_Image.Height));
        }

        /// <summary>
        /// Получить имя файла
        /// </summary>
        /// <returns>Имя файла</returns>
        protected abstract string GetFileName();

        /// <summary>
        /// Шаг 
        /// </summary>
        public virtual void Step()
        {
            _UnitBehavior.ExecAction(_Coordinates);
        }

        /// <summary>
        /// Прорисовка юнита
        /// </summary>
        /// <param name="gr">Графическое поле</param>
        public virtual void draw(Graphics gr)
        {
            gr.DrawImage(_Image, _Coordinates.X, _Coordinates.Y, _Image.Width, _Image.Height);
        }
}

Теперь приступим к реализации WalkBehavior. И тут выясняется, что в объект «поведение» нам надо еще и притащить ссылку на ядро, чтобы иметь доступ к ширине и высоте. О’к, добавляем:

    /// <summary>
    /// Поведение юнита
    /// </summary>
    public interface IUnitBehavior
    {
        /// <summary>
        /// Выполнить действие
        /// </summary>
        /// <param name="coordinates">Координаты</param>
        /// <param name="core">Ссылка на ядро</param>
        void ExecAction(Coordinates coordinates, Core core);
    }

Слегка меняем метод Step класса Unit:

        /// <summary>
        /// Шаг 
        /// </summary>
        public virtual void Step()
        {
            _UnitBehavior.ExecAction(_Coordinates,_Core);
        }

Теперь можно реализовать поведение Walk:

    /// <summary>
    /// Поведение: ходьба
    /// </summary>
    public class WalkBehavior : IUnitBehavior
    {
        /// <summary>
        /// Выполнить действия поведения
        /// </summary>
        /// <param name="coordinates">Координаты</param>
        /// <param name="core">Ядро</param>
        public void ExecAction(Coordinates coordinates, Core core)
        {
            coordinates.X += Core.rnd.Next(6) - 3;
            coordinates.Y += Core.rnd.Next(6) - 3;
            if (coordinates.X < 0) coordinates.X = 0;
            if (coordinates.Y >= core.Width) coordinates.Y = core.Width - 1;
            if (coordinates.Y < 0) coordinates.Y = 0;
            if (coordinates.Y >= core.Height) coordinates.Y = core.Height - 1;
        }
    }

Теперь займемся конкретными реализациями юниов. Игрок:

    /// <summary>
    /// Игрок
    /// </summary>
    public class Gamer : Unit
    {
        /// <summary>
        /// Конструктор 
        /// </summary>
        /// <param name="core">Ссылка на ядро</param>
        /// <param name="unitBehavior">Поведение</param>
        public Gamer(Core core, IUnitBehavior unitBehavior) : base(core, unitBehavior)
        {

        }


        /// <summary>
        /// Получить имя файла картинки
        /// </summary>
        /// <returns>Имя файла</returns>
        protected override string GetFileName()
        {
            return "mainunit.bmp";
        }
    }

Монстр:

    /// <summary>
    /// Класс монстра
    /// </summary>
    public class Monster : Unit
    {
        /// <summary>
        /// Конструктор 
        /// </summary>
        /// <param name="core">Ссылка на ядро</param>
        /// <param name="unitBehavior">Поведение</param>
        public Monster(Core core, IUnitBehavior unitBehavior) : base(core, unitBehavior)
        {

        }

        /// <summary>
        /// Получить имя файла картинки
        /// </summary>
        /// <returns>Имя файла</returns>
        protected override string GetFileName()
        {
            return "spider.bmp";
        }
    }

Лягушка:

    /// <summary>
    /// Лягушка
    /// </summary>
    public class Frog : Unit
    {
        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="core">Ссылка на ядро</param>
        /// <param name="unitBehavior">Поведение</param>
        public Frog(Core core, IUnitBehavior unitBehavior) : base(core, unitBehavior)
        {
        }


        /// <summary>
        /// Получить изображение
        /// </summary>
        /// <returns>Изображение</returns>
        protected override string GetFileName()
        {
            return "frog.png";
        }
    }

И, наконец, поведение «Прыжок»:

    /// <summary>
    /// Поведение "Прыжок"
    /// </summary>
    public class JumpBehavior : IUnitBehavior
    {
        /// <summary>
        /// Действие поведения
        /// </summary>
        /// <param name="coordinates">Координаты</param>
        /// <param name="core">Ядро</param>
        public void ExecAction(Coordinates coordinates, Core core)
        {
            if (Core.rnd.Next(10) == 1)
            {
                coordinates.X += Core.rnd.Next(50) - 25;
                coordinates.Y += Core.rnd.Next(50) - 25;
                if (coordinates.X < 0) coordinates.X = 0;
                if (coordinates.X >= core.Width) coordinates.X = core.Width - 1;
                if (coordinates.Y < 0) coordinates.Y = 0;
                if (coordinates.Y >= core.Height) coordinates.Y = core.Height - 1;
            }
        }
    }


Теперь нам осталось привязать поведение к юнитам, сделаем это конструкторе класса Core, где у нас создаются юниты:

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="width">Ширина</param>
        /// <param name="height">Высота</param>
        public Core(int width, int height)
        {
            _Height = height;
            _Width = 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, new WalkBehavior()));
                        break;
                    case 1:
                        _Units.Add(new Monster(this, new WalkBehavior()));
                        break;
                    case 2:
                        _Units.Add(new Frog(this, new JumpBehavior()));
                        break;
                }
            }
        }

Теперь мы можем красиво и лаконично переписать метод Step класса Core, удалив оттуда все кривые костыли:

        /// <summary>
        /// Шаг моделирования
        /// </summary>
        public void Step()
        {
            foreach (Unit unit in _Units)
            {
                unit.Step();
            }
        }

Запустим и убедимся, что все работает

Comments

So empty here ... leave a comment!

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

Sidebar