C# Паттерны Проектирования - Пример
Primary tabs
Forums:
Подробнее о теории паттернов здесь
Итак - пусть у нас есть программа:
using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace CheckersPatternGame { static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }
мы будем играть в шашки
Вот код адаптера - адоптируем данные в команду:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; namespace CheckersPatternGame { public class Adapter : myCheckersCommand1 { public TextBox tx1, ty1, tx2, ty2; // текст в этих вот друзьях мы будем адаптировать в команду. public Adapter(Facade Adrr, TextBox tx12,TextBox ty12,TextBox tx22,TextBox ty22) { this.Addressee = Adrr; tx1 = tx12; ty1 = ty12; tx2 = tx22; ty2 = ty22; this.Update(); } public void Update() // проверка и запоминание координат. { int a; if (Int32.TryParse(tx1.Text, out a)) { if ((a>=1)&&(a<=8)) this.x1 = a; } else throw new UnexpectedTypeExeption("В качестве координат указывайте целые числа от одного до восьми!"); if (Int32.TryParse(ty1.Text, out a)) { if ((a >= 1) && (a <= 8)) this.y1 = a; } else throw new UnexpectedTypeExeption("В качестве координат указывайте целые числа от одного до восьми!"); if (Int32.TryParse(tx2.Text, out a)) { if ((a >= 1) && (a <= 8)) this.x2 = a; } else throw new UnexpectedTypeExeption("В качестве координат указывайте целые числа от одного до восьми!"); if (Int32.TryParse(ty2.Text, out a)) { if ((a >= 1) && (a <= 8)) this.y2 = a; } else throw new UnexpectedTypeExeption("В качестве координат указывайте целые числа от одного до восьми!"); } } //---------далее немного исклбчений------------------- public class UnexpectedTypeExeption : ChackersExeption // базовый класс исключений для нашей коллекции { private const string SelfMessage = " Unexpected Type -> "; public UnexpectedTypeExeption() : base(String.Format("{0}", SelfMessage)) { } public UnexpectedTypeExeption(string s) : base(String.Format("{0}{1}", SelfMessage, s)) { } // формируем Message-свойство -сообщение. } }
а вот это фасад - он будет хранить состояние игрового поля:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace CheckersPatternGame { public abstract class Facade { public abstract void Make_a_Move(int x1,int x2,int y1,int y2); // даже для шахмат такой вот обобщённый формат хода подойдёт. } public class ChakersFacade1 : Facade { // ?????? почему в конструкторе нельзя void в данном случае ? public Shashka[] FigMass; // Фасад будет хранить два массива фигур - одщий для обоих игров, те фигуру, что будут иметь индекс больше 12-ого - это "чёрные" Наблюдатель нарисует их на форме сверху. public ChakersFacade1() // в этом конструкторе мы займёмся "подготовкой" игры - "расставим " фигуры , определим их цвет - это "программа минимум" { int x = 1, y = 1; // начальные координаты для расстановки. bool b=false; // проверка чётности номера строки клеток (не помню функцию , позволяющую определить отстаток от деления в си шарп) перва я строка - чётная. this.FigMass= new Shashka[24]; // выделяем память для массива фигур. for (int i=0;i<=23;i++) // инициаллизируем каждую фигуру. { // в цикле мы определяем цвет фигуры и её координаты ,причём перва яполучает координаты [1;1] FigMass[i] = new ProstoShashka(); FigMass[i].X = x; // запоминаем координаты. FigMass[i].Y = y; if (i <= 11) FigMass[i].isWhite = true; // то есть если это белая фигура. (чёрной фигура является по-умолчанию) // далее ряд опереций, связанных с вычислением очередной координаты if ((x == 7) && (y == 3)) // выполнение этого условия будет означать, что все белые уже расставлены. { y = 6; x = 2; b = true; // определяем координаты первой чёрной фигуры. } else // если все белые ещё не расставлены или раставлены более одной итерации цикла назад. { if ((x != 7) && (x != 8)) x = x + 2; // расставляем через клетку потому и увеличиваем икс на два. else { y = y + 1; // переходим на следующую строка , если заполнена предыдущая. if (b == false) b = true; else b = false; // меняем чётность/нечётность. if (b == false) x = 1; else x = 2; // определяем начальную координату клетки на очередной сторе в зависимости от чётности/нечётности последней. } } } this.UpdateObservers();// предлагаем наблюдателям обновиться - показать все шашки на исходных позициях. // на этом расстановка и покраска фигур завершается. теперь пишем обработчик хода. } Shashka ActiveFig; // фигура, которой собираются ходить, та, которая выбрана для хода. public override void Make_a_Move(int x1, int y1, int x2, int y2) // обработчик хода. { Shashka[] figuresOnThePathMass = null; // фигуры, которые находятся на траектории хода. Cell[] curcellmass; // координаты траектории передаваемого хода -получаем этот массив от фигуры. Cell[] cellmassWithoutActiveFig = null; // массив координат без тех , на которых фигура итак размещена - и без последней, которая должна быть свободной для хода. this.ActiveFig = this.GetSpecFigure(x1, y1); if (ActiveFig != null) { curcellmass = ActiveFig.CanMoveTo(x2,y2); if (curcellmass != null) // если фигура теоретически может так походить ,то начинаем изучать ситуацию на поле { /* далее мы описываем фасад исходя из тех принципов, что любая шашка - как дамка так и не дамка должна за один ход либо не переступить * ни одной шашки либо переступить только одну - при элементарном ударе, при этом клетка с конечным адресом должна быть свободой. * вот и вся логика*/ if (IsCellFree(x2, y2)) // проверяем доступность конечной координаты. - то , что клетка свободна. { if (curcellmass.Length == 1) throw new NoPathExeption("шашка уже итак стоит на этой клетке."); else if (curcellmass.Length == 2) // если это просто ход на соседнюю по-диагонали , тогда "ходим" { ActiveFig.X = x2; ActiveFig.Y = y2; } else { // далее выделяем из массива координат те , которые не являются начальными и конечными. cellmassWithoutActiveFig = new Cell[curcellmass.Length - 2]; // int i=0; foreach (Cell c in curcellmass) // переписываем интересующие нас координаты в новый массив. { cellmassWithoutActiveFig[i] = curcellmass[i + 1]; } // for (int i = 1; i <= curcellmass.Length - 1; i++) // { // cellmassWithoutActiveFig[i - 1] = curcellmass[i]; // переписываем интересующие нас координаты в новый массив. // } figuresOnThePathMass = GetFigMassOnThePath(cellmassWithoutActiveFig); // получаем фигуры на интересующем нас отрезке траектории. if (figuresOnThePathMass != null) { if ((figuresOnThePathMass.Length == 1) && (this.ActiveFig.isWhite != figuresOnThePathMass[0].isWhite)) // если на пути только одна шашка противника. { figuresOnThePathMass[0].IsKilled = true; // шашка противника побита. ActiveFig.X = x2; ActiveFig.Y = y2; } else if ((figuresOnThePathMass.Length == 1) && (this.ActiveFig.isWhite == figuresOnThePathMass[0].isWhite)) throw new CantBeatFriendlyUnitExeption(" своих не бьют!"); else if (figuresOnThePathMass.Length > 1) throw new CantMakeSoMoreSacrificesExeption("Не всё сразу. Бейте по-одной."); } } } } } this.UpdateObservers();// предлагаем наблюдателям обновиться. } // здесь обработка хода заканчивается - можно приступать к системе визуализации - Наблюдателю и Адаптеру, если говорить о паттернах. public bool IsCellFree(int x, int y) // вспомогательная функция, проверяет - является ли указнная ячейка свободной. { bool b = true;// ответ функции. foreach (Shashka curfigure in FigMass ) { if ((curfigure.X == x) && (curfigure.Y == y)) b = false; // вот и вся проверка. } return b; } public Shashka GetSpecFigure(int x, int y) // вспомогательная функция возвращает фигуру , если такова я имеется на пересечении указанных координат. { Shashka shash = null; foreach (Shashka curfigure in this.FigMass) { if ((curfigure.X == x) && (curfigure.Y == y)) { shash = curfigure; break; } // вот и вся проверка. } return shash; } public Shashka[] GetFigMassOnThePath(Cell[] cellmass) // возвращает массив фигур, размещённых намножестве переданных клеток. { Shashka[] rez= new Shashka[0] ; // function result foreach (Shashka shash in this.FigMass) // перебираем фигуры { foreach (Cell cell in cellmass) // перебираем клетки траектории. { if ((shash.X == cell.x) && (shash.Y == cell.y)) { Array.Resize(ref rez, rez.Length + 1); rez[rez.Length - 1] = shash; } } } return rez; } private Observer[] Observers = new Observer[0]; public void UpdateObservers() { foreach (Observer v in Observers) { v.UpdateView(); } } public void AddObserver(Observer nv ) { Array.Resize(ref this.Observers, this.Observers.Length + 1); this.Observers[this.Observers.Length-1] = nv; // добавляем ещё одного наблюдателя, реализующего интерфейс, опять же, наблюдателя. } } //--------далее набор всевозможных исключительных ситуаций - чтобы играть было веселее.)))-------------- public class ChackersExeption : ApplicationException // базовый класс исключений для нашей коллекции { private const string ChackersEceptionMessage = "(!) Problem with the checkers game-> "; public ChackersExeption() : base(String.Format("{0}", ChackersEceptionMessage)) { } public ChackersExeption(string s) : base(String.Format("{0}{1}", ChackersEceptionMessage, s)) { } // формируем Message-свойство -сообщение. } public class NoPathExeption :ChackersExeption // базовый класс исключений для нашей коллекции { private const string SelfMessage = " path doesn't exist -> "; public NoPathExeption() : base(String.Format("{0}", SelfMessage)) { } public NoPathExeption(string s) : base(String.Format("{0}{1}", SelfMessage, s)) { } // формируем Message-свойство -сообщение. } public class CantMoveExeption : ChackersExeption // базовый класс исключений для нашей коллекции { private const string SelfMessage = " can't move -> "; public CantMoveExeption() : base(String.Format("{0}", SelfMessage)) { } public CantMoveExeption(string s) : base(String.Format("{0}{1}", SelfMessage, s)) { } // формируем Message-свойство -сообщение. } public class CantBeatFriendlyUnitExeption : ChackersExeption // базовый класс исключений для нашей коллекции { private const string SelfMessage = "It's friendly shachka -you can't beat it-> "; public CantBeatFriendlyUnitExeption() : base(String.Format("{0}", SelfMessage)) { } public CantBeatFriendlyUnitExeption(string s) : base(String.Format("{0}{1}", SelfMessage, s)) { } // формируем Message-свойство -сообщение. } public class CantMakeSoMoreSacrificesExeption : ChackersExeption // базовый класс исключений для нашей коллекции { private const string SelfMessage = " can't beat so more units at one motion -> "; public CantMakeSoMoreSacrificesExeption() : base(String.Format("{0}", SelfMessage)) { } public CantMakeSoMoreSacrificesExeption(string s) : base(String.Format("{0}{1}", SelfMessage, s)) { } // формируем Message-свойство -сообщение. } }
а вот код для фигуры - он сделает её независимой от игрового мира:
using System; using System.Collections.Generic; using System.Linq; using System.Text; /*шашки паттерны ооп*/ namespace CheckersPatternGame // лучше всег начать с описания класса Фигура. // начинаем с фигуры -главного юнита нашей игры . { /* 1)Волевым решением создатель этого далеко не самого совершенного кода постоновляет - мы будем узнавать возможность сделать ход у фигуры ,но так как фигура ничего не должна знать о фасаде по-идее (это сделает её универсальной) то фигура будет просто отвечать типа "в общем случае со своей клетки я могу походить на предлагаемую клетку" или же "не могу", * в свою очередь Фасад, который, как ,опять же - предполагает автор данного кода ,"знает" о фигурах , которыми он двигает, и может перемещать их, или * же убирать с поля "побитые" - "спрашивает" у фигуры - может ли она чисто теоретически достигнуть указанной клетки, * если может - он проводит ряд собственных проверок и действий, связанных с расположением наигровом уже других фигур * и если проверки закачиваются успехом(всё соответствует правилам) игры фасад производит соответствующие ходу преобразования игрового поля. * Вот, собственно, и всё задумка, отностильно взаимодействия фигуры и фасада - * фасад спрашивает может ли фигура чисто теоретически * (то есть , не учитывая, например того, что на данной клетке уже кто-то стоит) совершить элементарный ход (или * элементарный удар) на эту позицию (например фасад шашек, который мы здесь реализуем * спросит просто возможность однократного удара или хода - сам при этом проверив -есть ли кого бить в случае удара и не занята ли клекта которыю * предполагается занять после хода/удара) то есть - ключевой момент частого решения проектирования здесь состоит именно в проверке * элементарных хода и удара у фигуры - все остальные решения же будет принимать фасад - причём некая в определённым смысле, "частная" его версия, * заточенная для работы именно с данным типом фигур в данной игре (шашки и дамки в шашках, различные фигуры в шахматах, лиса и четыре гуся а игре * "Лиса и гуси") * 2) В ходе дополнительных размышлений автор приходит к выводу о том, что фигура должна возврашать не только ответ о способности * совершить ход но и МАССИВ КООРДИНАТ ПРИНАДЛЕЖАЩИХ ЕЁ ТРАЕКТОРИИ ДАННОГО ХОДА. это сделает фигуру полностью самостоятельной , а фасад * в свою очередь уже будет отвечать только за взаимодействие фигур на поле. * */ public abstract class Figure // это класс фигуры для "движка" игры, то есть Фасада. ????? - почему здесь нужен паблик перед классом - иначе появится Error 1 Inconsistent accessibility: base class 'CheckersPatternGame.Figure' is less accessible than class 'CheckersPatternGame.Shashka' { private int XFiled, YFiled; //целочисленные координаты фигуры на поле. public int X // { get { return this.XFiled; } set { this.XFiled = value; } } public int Y // { get { return this.YFiled; } set { this.YFiled = value; } } public bool IsKilled = false; // при инициализации фигура жива. public string Name; // наименование фигуры (это поле инициализировать в игре не обязательно.) - может пригодится например, при выводе ошибки public abstract Cell[] CanMoveTo(int x2, int y2); // этот метод размышляет над тем - можно ли походить или нет - если может - возвращает "true", иначе "false" } //------------------------------ // далее -классы для реализации паттерна Состояния фигуры. public abstract class Shashka : Figure // просто наследуемся от фигуры. { public bool isWhite = false; // добавим для шашек цвет - по-умолчанию=чёрный - хотя на самом деле - просто один из двух. public Shashka(int x, int y) // создавать фигуру без начального размещения на поле вроде как бессмыслено - поэтому заводим вот такой вот конструктор. { this.X = x; this.Y = y; } public Shashka() // явный конструктор по умолчанию, чтобы наследоваться. {} } public class ProstoShashka : Shashka { public override Cell[] CanMoveTo(int x2, int y2) { Cell[] rez=null; // ответ фигуры на вопрос о способности совершить ход. if (this.isWhite == true) { if ((((this.X - x2) == 1) || ((this.X - x2) == -1)) && ((this.Y - y2) == -1)) // с помощью этого выражения фигура убеждается что ей предлагают просто походить вперёд(при этом вправо или влево.) rez = GetPath(this.X, this.Y, x2, y2); // получаем траекторию else if ((((this.X - x2) == 2) || ((this.X - x2) == -2)) && (((this.Y - y2) == -2) || ((this.Y - y2) == 2))) // так по поей мысли определяет элементарный удар в четырёх направлениях по диагонали ,причём если бы я знал функцию модуля , то написал бы красивее, но функции модуляы я не знаю, а интернет не работает. rez = GetPath(this.X, this.Y, x2, y2); // получаем траекторию return rez; } else { if ((((this.X - x2) == 1) || ((this.X - x2) == -1)) && ((this.Y - y2) == 1)) // с помощью этого выражения фигура убеждается что ей предлагают просто походить вперёд(при этом вправо или влево.) rez = GetPath(this.X, this.Y, x2, y2); // получаем траекторию else if ((((this.X - x2) == 2) || ((this.X - x2) == -2)) && (((this.Y - y2) == -2) || ((this.Y - y2) == 2))) // так по поей мысли определяет элементарный удар в четырёх направлениях по диагонали ,причём если бы я знал функцию модуля , то написал бы красивее, но функции модуляы я не знаю, а интернет не работает. rez = GetPath(this.X, this.Y, x2, y2); // получаем траекторию return rez; } } static Cell[] GetPath(int x1, int y1, int x2, int y2) // эту функцию можно будет использовать и для дамки. { // bool b = false; //используется при определении траектории. Cell[] rez = new Cell[0]; int xz, yz,x,y; x = x1; y = y1; Array.Resize(ref rez, 1); rez[0] = new Cell(x,y); // rez[0].x = x; rez[0].y = y; // траектория как минимум будет выражденной -только исходная клетка. if ((x1 != x2) || (y1 != y2)) { if (x1 == x2) xz = 1; else // определяем - возрастает или убывает x; xz = Math.Abs(x2 - x1) / (x2 - x1); if (y1 == y2) yz = 1; else // определяем - возрастает или убывает y; yz = Math.Abs(y2 - y1) / (y2 - y1); /* возврат траектории может потребоваться тодько в случае , если фигура "счиатает" , * что она можнет так походить - сответственно, в шашках достаточно просто "провести прямую" из начала в конец хода - это и будет траетория*/ while ((x != x2) || (y != y2)) { x = x + xz; // движемся по прямой (а может и нет если второе слогаемое равно нулю.) y = y + yz; // движемся но прямой (а может и нет если второе слогаемое равно нулю.) Array.Resize(ref rez, rez.Length + 1); // выделяем память для ещё одного элемента массива. rez[rez.Length - 1] = new Cell(x, y); // запоминаем координаты очередной клетки // rez[rez.Length - 1].x = x; // запоминаем координату очередной клетки // rez[rez.Length - 1].y = y; // запоминаем координату очередной клетки } // на этом вычисление траектории закончено. } return rez; // возвращаем массив значений. } // на этом описание "просто шашки" (не "дамки") можно прекратить и заняться фасадом, который , собственно, и будет руководить игрой. } //--------------------------------------------------далее - //вспомогательный класс, описывающий клетку - содержит только две координаты -необходим для передачи траектории хода в виде массива клеток. public class Cell { public Cell(int x, int y) { this.x = x; this.y = y; } public int x; public int y; } }
А вот это Наблюдатель - он будет рисовать для нас игровое поле - отображая для пользователя внутренне состоянием игрового мира
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; using System.Windows.Forms; namespace CheckersPatternGame { public abstract class Observer // шаблон для всех наблюдателей { public abstract void UpdateView(); // метод который призывает наблюдателя обновиться в соответствии с возможным изменением наблюдаемого. } public class GameFiledView1 : Observer // наш конкретный наблюдатель. { public GameFiledView1(Graphics bm, ChakersFacade1 cfd) // наш наблюдатель теряет смысл существования без места, где он может рисовать и натуры, образ которой можно запечатлеть на плоскости. { this.BitMap = bm; this.UpdateSourse = cfd; // this.PaintArgs = e; } Graphics BitMap;// то, на чём мы будем рисовать. ChakersFacade1 UpdateSourse; // то, с чем мы будем согласовывать графическое представление (рисование). // PaintEventArgs PaintArgs; public override void UpdateView() { Brush brush1 = new SolidBrush(Color.Chocolate); Brush brush2 = new SolidBrush(Color.White); Brush brush5 = new SolidBrush(Color.BurlyWood); Brush brush3 = new SolidBrush(Color.Black); Brush brush4 = new SolidBrush(Color.DarkOliveGreen); Pen blackPen = new Pen(Color.BurlyWood, 3); Brush brush7 = new SolidBrush(Color.DarkSlateGray); // залём фон BitMap.FillRectangle(brush4, 0, 0, 1000, 1000); BitMap.FillRectangle(brush7, 45, 45, 325, 325); // рисуем поле. int d = 40; // параметр для рисования. int с = 25; // параметр для рисования. int h = 8; // параметр для рисования. int x = 1, y = 1; // -координаты. bool b = false; for (int i = 1; i <= 64; i++) { if (b == true) BitMap.FillRectangle(brush2, x * d, y * d, d, d); else BitMap.FillRectangle(brush3, x * d, y * d, d, d); if (x == 8) { y = y + 1; x = 1; } else { x = x + 1; if (b == true) b = false; else b = true; } } foreach (Shashka sha in this.UpdateSourse.FigMass) { // Rectangle rect = new Rectangle(sha.X * 30, sha.Y * 20, sha.X * 20 + 100, sha.Y * 20 + 100); //Rectangle rect = new Rectangle(50, 50, 158, 158); // PaintArgs.Graphics.DrawEllipse(blackPen, sha.X * 20, sha.Y * 20, 20,20); if (sha.IsKilled == false) { if (sha.isWhite == false) BitMap.FillEllipse(brush1, sha.X * d + h, sha.Y * d + h, с, с); else BitMap.FillEllipse(brush5, sha.X * d + h, sha.Y * d + h, с, с); } } } } }
вот в этом файле вы используем паттерн Команда - а также создадим класс игрока (кстати число игроков можно было бы ограничить паттерном Одиночка - но мы это тут делать не станем):
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace CheckersPatternGame // здесь мы соберём набор классов, используемых в патерне "Команда" и те. что относятся и интерфейсу Player. { // далее паттерн КОМАНДА public abstract class Command // общий интерфейс для всех команд приложения. { public abstract void Execute(); } public class myCheckersCommand1 : Command // команда выполнения хода. { private Facade AddresseeFiled; // доступ только через свойство + адресат , то есть собственно тот класс , которому и будет передаваться команда - это просто наследник от фасада. private int x1Filed, y1Filed, x2Filed, y2Filed; public Facade Addressee // используем это свойство для получения и смены адресата. { get { return this.AddresseeFiled; } set { this.AddresseeFiled = value; } } public int x1 // координата начального положения. { get { return this.x1Filed; } set { this.x1Filed = value; } } public int y1 // координата начального положения. { get { return this.y1Filed; } set { this.y1Filed = value; } } public int x2 // предполагаемая координата после выполнения хода. { get { return this.x2Filed; } set { this.x2Filed = value; } } public int y2 //предполагаемая координата после выполнения хода. { get { return this.y2Filed; } set { this.y2Filed = value; } } public myCheckersCommand1(int x1,int x2,int y1,int y2) // конструктор для создания команды выполнения хода. { // запоминаем координаты хода. this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; } public myCheckersCommand1() // явный конструктор по-умолчанию. { } public override void Execute() // собственно метод посылает команду тому, кто её будет исполнять. { this.Addressee.Make_a_Move(this.x1,this.y1,this.x2,this.y2); // приказываем фасаду выполнить ход - то есть изменить данные о расположении фигур на поле. } } //---------------------------------------------------------- // Далее определение объекта типа PLAYER public abstract class Player // итак, некий "интерфейс" для класса игрок, который и будет посылать команды игре, а конктренто - Фасаду. { private Command MakeAMoveCurCommandFiled; // доступ только через свойство + адресат , то есть собственно тот класс , которому и будет передаваться команда - это просто наследник от фасада. public abstract void Make_a_Move(); // наш игрок пока что может сделать одно единственное действие - отменить ход. public Command MakeAMoveCurCommand // используем это свойство для получения и смены адресата. { get { return this.MakeAMoveCurCommandFiled; } set { this.MakeAMoveCurCommandFiled = value; } } } public class myCheckersPlayer1 : Player { public myCheckersPlayer1(Command cmd) { this.MakeAMoveCurCommand = cmd; } public override void Make_a_Move() { this.MakeAMoveCurCommand.Execute(); } } }
- Log in to post comments
- 7897 reads