С# и концепция ООП. Продолжение

На прошлом уроке   я рассказал основы концепции ОПП и мы начали писать игру «Мафия» на C#.  Продолжаем.

Напомню краткое содержание: мы познакомились с различными парадигмами программирования (императивное, декларативное, функциональное ОПП и др.).  Было раскрыто преимущество ООП. Мы решили что-нибудь запрограммировать на C#, например, компьютерную игру, в качестве игры выбрали «Мафию». Но еще решили писать ее так, чтобы потом на основе этой игры можно было создать игру «Вторжение рептилоидов». Была предложена структура классов игры, и мы начали программировать, создали проект и заготовки нескольких классов.

Давайте продолжим разработку ядра игры:

Итак, что мы сделали? Объявили перечисление (enum),  в которое забили всевозможные шаги. Добавили булевы переменные, которые будут у нас связаны с флажками настройками (чтобы мы могли опционально выбрать, будет ли у нас маньяк, доктор и путана). Теперь осталось в методе step реализовать нечто вроде конечного автомата. Но стоп! Вам нравиться то, что мы сейчас сделали? Подумайте хорошенько. С учетом того, что мы хотим сделать универсальное ядро игры и на его основе создать потом игру «Вторжение рептилоидов». Лично мне это решение уже не нравиться. Почему? Ну, например, решили мы добавить в игру еще одного персонажа, например «Продажный судья» или «Коррумпированный чиновник». Тогда в игре надо предусмотреть шаг «Судья сажает невиновного» или «Судья вставляет палки в колеса комиссару». Стоит заметить, что у персонажа может быть и два шага. В частности, добавив «Продажного судью» мы добавляем мафии еще один шаг «Мафия дает взятку судье».  И что, тогда добавлять новое поле «Есть продажный судья» и переписывать ядро игры?

Но как сделать алгоритм шагов более универсальным? Концепция ООП позволяет это. Например, можно предусмотреть в ядре список абстрактных объектов «Шаг», как мы сделали это для персонажей. Перебрать эти шаги в списке и вызвать у них, например, метод execute(), который у каждого шага будет реализован по-своему. Ну а сами шаги можно жестко задать где-нибудь в методе init(), который просто переписать, если мы решили изменить шаги игры. Тут надо будет переписать всего лишь один метод. Или даже не переписать, а просто вставить в нужное место код добавления объекта в список. И все. Проиллюстрирую это на примере одной моей программы, которую я писал для своей магистерской диссертации:

Тут я использовал обычный список List, типизированный абстрактным классом. Все добавляемые в данном коде объекты являются потомками этого абстрактного класса. Для перехода к следующему шагу я просто увеличивал на единицу номер текущего шага и вызывал у соответствующего объекта метод выполнения. И вот как выглядит код выполнения очередного шага:

Все просто и лаконично, никаких запутанных конечных автоматов и «спагетти кода».  Нечто аналогичное можно сделать и в нашей игре. Но давайте еще раз немного подумаем. Каждый персонаж может участвовать в более чем одном шаге. Это раз. Добавляя нового персонажа, нам нужно добавлять для него хотя бы один объект наследник от некого абстрактного шага. А что если сделать как-то так по-хитрому, чтобы добавляя нового персонажа, мы автоматом создавали для него шаги? Например, у персонажа можно предусмотреть поле, которое показывает, чей следующий ход. Например, если у нас был ход мафии, а по правилам после того, как мафия засыпает, просыпается комиссар, мы просто указываем в поле «Следующий»  персонажа «Мафия» значение «Комиссар».  Но стоп. Мы забыли, что у нас может быть несколько персонажей одного типа. В таком случае, мы в поле «Следующий» будем указывать не конкретный объект, а тип. И следующий шаг «будит» все объекты заданного типа. А еще лучше предусмотреть некий абстрактный объект или даже интерфейс «Фильтр», который после окончания шага активирует объекты по определенному правилу (задел на будущее).

И создадим интерфейс «Фильтр»:

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

Итак, шаблон готов. Сейчас мы будем наполнять его программой. Какие там в мафии есть персонажи? Мафия, комиссар, город… Давайте начнем с мафии. Так и назовем его MafiaUnit:

Непонятно, а как мафия выбирает жертву? Из кого? У данного класса нет никакой ссылки на список персонажей, из которого выбирать. У нас полный список персонажей есть в классе Core. Можно, конечно, предусмотреть в AbstractUnit ссылку на Core, но стоит ли так делать? Тем более, что выбор нужно производить не из полного списка, из него нужно исключить других мафиозиев, а также тех, кто уже убит. В принципе, мы вполне можем получить список живых персонажей из Core, предусмотрев у него соответствующий метод. Сам список у нас имеет модификатор protected, и, поэтому, ничего страшного, если мы объявим в AbstractUnit ссылку на Core, так как к полному списку персонаж все равно не сможет получить доступ. Итак, давайте сначала у класса Core предусмотрим метод get_alive_units:

Так, опять заглушка. Потому что непонятно, как определить, что юнит живой. Можно предусмотреть у него поле is_alive булевого типа, если мы убиваем персонажа, то просто присваиваем этому полю false. Но это будет не совсем верно. Не стоит давать возможность классу персонажа напрямую изменять поля других персонажей. Почему? Во-первых, может так случиться, что при изменении какого-то поля необходимо произвести какие-либо действия. Например, у нас персонаж показывается на экране в виде картинки, при изменении значения поля надо перерисовать эту картинку. Как тут быть? Можно вместо поля сделать свойство. Тогда при присваивании автоматически будет вызвал программный код, «повешенный» на это свойство. Он и выполнит нужные действия. Во-вторых, вдруг мы заходим сделать игру  без пользователей, только с ботами, и понаблюдать, как боты будут играть между собой. А потом захотим устроить соревнования между программистами, кто напишет лучшего бота. Только вот где гарантия, что программисты не воспользуются «читом» — кто им мешает, в нарушении правил игры, просто присвоить все противникам свойство is_alive значение false? Никто. Только если мы не придумаем какой-то промежуточный класс и не запретим такое прямое присваивание. В виду большой трудоемкости реализовать данную идею, мы это не будем делать, наша задача просто написать учебный вариант компьютерной игры. Сделаем проще, пусть будет свойство is_alive. Код на него вешать не будет, просто объявим is_alive как свойство, а не как поле, а потом, в случае необходимости, навешаем на него код. Объявим это поле мы в классе AbstractUnit:

 

get и set в фигурных скобках как раз и обозначает, что это свойство. После них должен быть программный код на чтение (после get) и на запись (после set). Если мы просто укажем get и set – то это зарезервированное свойство, то есть, это как бы поле, которое легким мановением руки преварщается… превращается поле …. В свойство! 

Все, теперь можно дописать метод get_alive_units:

Немного пояснений к коду. Что такое FindAll? Это метод списка (List) который возвращает все найденные по условию элементы списка (в виде нового списка). Условие поиска задается в делегате (delegate). Что такое делегат? Это программный код, оформленный как делегат. По сути анонимная функция. Здесь та анонимная функция (делегат) передается в качестве параметра метода FindAll. Когда мы передаем куда-то делегат, он, как правило, должен возвращаться значение, в нашем случае он должен возвращать значение булевого типа, что он и делает, флаг is_alive у нас как раз имеет булевый тип.

Теперь возвращаемся к методу step() класса MafiaUnit. Стоп, у нас же нет ссылки на Core. Давайте объявим ее в классе AbstractUnit:

А чтобы мы не забывали передать ссылку, изменим конструктор:

Теперь-то не забудем, иначе программа у нас просто не скомпилируется, напомнив нам вот таким вот сообщением об ошибке:

 

Исправляем конструктор класса MafiaUnit:

Теперь самое время вернутся к методу step класса MafiaUnit:

 

 

Мы получили список живых персонажей, а потом отфильтровали его, исключив мафию. Надо выбрать жертву из полученного списка. Как это сделать? Давай на начальном этапе поступим максимально просто: выбреем случайного человека. Потом, если что, усложним алгоритм. Нам понадобиться ГСЧ. Это класс Random, нам надо объявить экземпляр этого класса. Желательно, только один. А вот тут проблема. Где его объявить и как передать во все объекты, которые нуждаются в ГСЧ? Создать в каждом отдельном случае новый экземпляр класса не вариант, это расточительство какое-то. Как быть? К счастью, в C# есть статические поля и методы.  Что это такое? Это такие поля и методы, которые общие для класса, и не привязаны к его конкретному экземпляру. По сути, аналог переменой или функции императивного языка программирования. Вот как объявляется статическое поле (всего лишь добавляем к нему static):

 

Мы его объявили и сразу же инициализировали. Разумеется, мы объявить его моем не где попало, а в каком-то классе. Например, в классе Core. Вот так теперь будет выглядеть сам класс:

 

Как использовать? Через имя класса и точку, например, вот так:

Метод Next класса Random генерирует целое случайное число от нуля(включительно) до указанного параметра(не включительно). То есть, если мы укажем параметр 10, то ГСЧ будет генерировать числа от 0 до 9. Если в качестве параметра мы укажем размер списка, то рандомизатор сгенерирует случайный индекс элемента из этого списка. В списке или массиве счет элементов идет с нуля. Последний элемент имеет индекс количество элементов минус 1. Вот полный код метода step класса MafiaUnit:

Метода kill у класса Core еще нет, давайте его напишем:

И все? — Удивитесь вы. Нет, не все, тут еще надо бы какие-то действия сделать, пока непонятно какие. Ну, там сообщить, что кто-то убит. Кстати, насчет сообщить. Давайте сразу изучим такую возможность C#, как ивенты – обработчики событий. По сути, это такие подпрограммы, которые вызываются при наступлении определенного события. Например, когда убивают персонажа.

Для того, чтобы можно было создать такой обработчик, нужнее делегат:

Создадим этот делегат в том же файле, где у нас Core:

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

Чтобы при наступлении события был вызван этот обработчик, в методе kill обеспечим ему вызов:

Синтаксис связывания ивента с конкретной подпрограммой:

<переменная типа Core>.OnUnitKilled+=<Имя подпрограммы — обработчика>

Но более подробно об этом в следующей части, когда мы начнем писать интерфейсную часть.

Comments

So empty here ... leave a comment!

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

Sidebar



X

Ищешь разработчика 1С?
Оставь заявку на консультацию

X

Ищешь разработчика?