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:

 

 

Как видим, у него реализован виртуальный метод walk() – идти, и draw() – нарисовать, который в данный момент просто отображает в виде спрайта ранее загруженную маленькую картинку. Эта картинка храниться в файле с именем, который возвращает метод get_file_name(), у класса юнита он тоже абстрактный.

Теперь реализуем класс игрок (Gamer):

 

Как видим, у него мы только реализовали get_file_name(), ибо все остальное у нас уже реализовано в родительском классе Unit.

Аналогично реализовываем Monster:

В качестве движка игры создадим класс Core:

Теперь идем на форму, кидаем туда PictureBox:

Я обозвал PictureBox: pbGameField, а его обработчик события Paint:

pbGameField_Paint. Еще на форме есть компонент Timer вот с такими настройками:

 

Его обработчик timer_Tick назван timer_Tick.

А вот и сам код формы:

 

Результат работы программы – хаотично движущиеся по экрану пауки и человечки:

 

А теперь о том, почему данное решение плохое. Предположим, мы захотели добавить в игру еще один тип юнитов Frog, который умеет прыгать (jump). Ок, создаем класс:

 

Теперь добавим юнит в начальный экран, переписав конструктор Core:

Запустим и посмотрим:

Лягушка у нас не прыгает. Почему? Нужно еще в методе step() класса Core предусмотреть вызов jump(). И тут опаньки… Вставить то некуда. Мы перебираем все элементы _units, которые представляют собой элементы абстрактного класса Unit. А у него нету метода jump(), он объявлен у класса Frog. Как быть? Перенести метод jump() в класс Unit, а потом вызвать в цикле опроса (это ошибка, но ее я допускаю намеренно в целях демонстрации):

 

И теперь прыгают у нас все. Не только лягушки, но и монстры, и человечки. Как решить эту проблему? Переносим метод jump() обратно в класс Frog, и делаем в step проверку на вид класса:

Теперь работает правильно. Но то, что мы сделали – это кривой, нет, даже кривущий костыль. Так делать нельзя!!! Почему? А если мы создадим еще один вид юнита, со своими методами? Опять будем добавлять в код костыль? А если мы решили создать еще один тип поведения для конкретного юнита? Например, захотели, чтобы лягушка могла ловить мух. Добавим метод catch_fly() в класс Frog и таким же костыльным способом пропишем его вызов в методе step? Так у нас вскоре костыли разрастутся в плохо управляемый запутанный спагетти-код. Тогда как быть? А об этом вы узнаете в следующей части статьи.

Comments

So empty here ... leave a comment!

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

Sidebar