• Открыта запись на вторую часть курса по анонимности и безопасности в сети интернет "Paranoid II" от команды codeby. Анонимные роутеры, Подъём, настройка и администрирование Tor-ноды, Работа с железом ПК, Удаление аппаратных закладок, Минимизация рисков, Авторские разработки и многое другое. Подробнее ...

Статья [Что нам стоит покер-бот построить] Каркас для сервера

mrOkey

mrOkey

Red Team
14.11.2017
632
722
[Что нам стоит покер-бот построить] Введение
[Что нам стоит покер-бот построить] Взгляд на проблему с высоты

Рассказываю о фундаменте серверного решения, основных типах, удобстве расширения и SOLID. О рефлексии, дженериках и прочей нечисти. Добро пожаловать в интересный мир enterprise.

Доброго времени суток, codeby.

Вся история программирования – это борьба со сложностью. Разработка софта – это не только решение конкретной задачи, это также вечное противостояния между сложностью программного кода, удобством расширения, оптимизацией и так далее. Это поиск компромиссов и эмпирических правил. Я приглашаю вас посмотреть, как это делал я.

Где начинается хороший софт? Со знания предметной области и хорошего технического задания. Наверное, сделаю статью по его написанию, сегодня же мы разбираем только код. Изучить предметную область вам не составит труда use google.

Но, допустим, вас бесполезно учить и вы не понимаете, зачем вам 20 страниц текста и картинок, когда «Итак всё понятно», поэтому я буду показывать написания кода по мере поступления мысли. Не стоит так делать, я серьёзно, это неадекватный подход. Что бы убедиться в этом отправлю вас к Макконелу, посмотрите, сколько стоит исправление ошибки на этапе составления тз и на этапе написания кода.

Давайте вспомним, что наш сервер должен делать. Он должен:

А вот сервер, должен-таки, думать и принимать решения, основываясь на предоставленной информации. Кроме того, он должен сообщать о них клиенту.
Я выделил вам главное.

· Сущность – 2 штуки (Решение, информация)

· Действия – 3 штуки. (Принимать решение, думать (обрабатывать информацию), сообщать клиенту)

Только что мы вместе с вами воспользовались текстологическим анализом, это очевидное действие, весьма полезно в работе, особенно в тех случаях, когда надо с чего-то начать, а в голове ноль идей (относится к задачам любой сложности). Вот только анализировали мы мысль, а в нормальном подходе так анализируют тз.

Теперь у нас есть с чем работать, давайте начнём:

Решение. Обратимся к предметной области: что в покере является решением? Решение в покере это одно из игровых действий: сбросить карты, уравнять ставку, поднять ставку, чек. Соответственно нам нужно что-то, что однозначно кодировало бы данные действия, в языке C# есть такая вещь как Enum (перечисление)
[Что нам стоит покер-бот построить] Каркас для сервера

Но это ведь ещё не всё. С Fold и Check всё понятно, а что делать с Bet(в данном случае, под Bet подразумевается: поднять ставку(raise) и просто поставить). Ведь размер ставки, так же определяется решением и логично что принимать решение о размере ставки должен сервер. Поэтому, наше перечисление ThisAction должно стать частью чего-то большего:
[Что нам стоит покер-бот построить] Каркас для сервера

Думаю, тут всё ясно и без комментария.
Фух… не, дальше я не буду так всё разжёвывать.

Информация.
Аналогично подумаем про информацию. Обратимся к предметной области. Проведём текстологический анализ информации из предметной области. (Давайте сами).
Получим что-то такое:
[Что нам стоит покер-бот построить] Каркас для сервера

Где Player
[Что нам стоит покер-бот построить] Каркас для сервера

Table
[Что нам стоит покер-бот построить] Каркас для сервера

TableStage тоже Enum, описывающий стадии раздачи.
[Что нам стоит покер-бот построить] Каркас для сервера

Уже на этом небольшом этапе мы плавно подошли к принципам хорошего проектирования.

Не используйте строки. Нигде. Серьёзно - по минимуму. Особенно в больших проектах (а наш бот будет большим проектом). Почему?
На самом деле за значениями в Enum скрываются цифры 0…n. Допустим, мы определили бы стадии игры через строку. В таком случае при разработки клиента мы не застрахованы от опечаток и в случае опечатки мы бы потратили очень много времени на поиск и устранении ошибки. Enum же мало того что исключает опечатки, благодаря IntelliSense показывает все возможные варианты, если мы вдруг забудем.
Благодаря Enum мы предотвращаем большинство ошибок на этапе компиляции, а это очень хорошо.
Короче, Enum – бро, String – не бро.



Думаю, многие из вас знакомы с понятием полиморфизм. Однако немногие умеют им пользоваться и плохо понимают его. Давайте обратимся к простому определению:
Полиморфизм – один интерфейс множество реализаций.

В прошлых статьях я много раз делал акцент на масштабируемости решения и для данной предметной области это очень важно. У нас есть множество вариантов реализации игрока и стола. Допустим, мы решили расширить функционал нашего бота, добавить ему возможность думать в MTT(многостоловые турниры). Ну и понятно, что стратегия игры для мтт будет отличаться от стратегии игры в Cash не только на стадиях флоп\тёрн\ривер, но и на этапах самого турнира (эти этапы характеризуются оставшимся количеством игроков) : бабл, призы(финал), начало турнира. Тогда для принятия решения нам нужно будет больше информации о столе, например о том какой это этап. В таком случае нам нужно будет реализовать отдельный стол. Так как у них будет общее поведение, мы просто реализуем интерфейс ITable и всё. Во всех наших старых классах мы можем работать с новым столом не напрягаясь (почти). Главное не забыть про один из принципов SOLID – Принцип подстановки Барбары Лисков (LSP) (хорошее объяснении которого можно прочесть на хабре (не реклама)), который по простому звучит так: Наследующий класс должен дополнять, а не замещать поведение базового класса Или Замена базового типа субтипом не должна отражаться на работе программы
Соответственно:
· Программирование для интерфейса, а не для реализации упрощает расширение ваших программ.
· При программировании для интерфейса ваш код будет работать со всеми субклассами этого интерфейса, даже с теми, которые ещё не созданы.

Интерфейсы и абстрактные классы – бро.

С теорией пока закончим. Вернёмся к коду. Таким образом, у нас уже есть – входные и выходные данные. Давайте думать дальше. Мы с вами хорошо понимаем следующие: разные решение принимается на разных стадиях игры, более того такие решения вдобавок зависят и от типа игры. Абстрагируемся пока от того как эти решения мы будем принимать, давайте подумаем как сделать так чтоб нам было удобно разрабатывать. Мой бывший учитель матана всегда говорил: «Сделайте сегодня себя плохо, чтобы завтра вам было хорошо» умный был дядька.

Фабрика\фабричный метод.
Разные, разные, разные. Такая разность чаще всего приводит к реализациям фабрик. Что такое фабрика? Это очень популярный паттерн проектирования, знать про который обязан каждый. Реализаций этого подхода уйма. Я покажу интересный, основанный на рефлексии, дженериках и атрибутах.
Фабрикой я решаю следующую проблему. Понятно, что для каждой игры (Cash, MTT, SnG..) существуют разные тактики игры на разных стадиях раздачи (префлоп\флоп\тёрн\ривер). Задача такова: как можно максимально избежать дублирования кода, сохранив при этом расширяемость и сделать её максимально удобной, а также реализовать различные алгоритмы работы с входными данными по-разному.

It’s easy.

Смотрите. Мы знаем, что наша фабрика должна возвращать ResponseAction. Так же мы знаем, что на вход ей подаётся GameInfo. Давайте внимательно взглянем на входные параметры: в интерфейсе ITable есть свойство, которое указывает на перечисление TableStage. Следовательно задав switch по этому полю мы бы могли определить для него стратегию. Но давайте пойдём дальше, напишем стратегию игры на префлопе\флопе\тёрне\ривере, делегируя её отдельным классам.
Начнём с интерфейса: IStrategy
[Что нам стоит покер-бот построить] Каркас для сервера


Собственно, мы описали вот это предложение:
Мы знаем, что наша фабрика должна возвращать ResponseAction. Так же мы знаем, что на вход ей подаётся GameInfo.
Теперь сделаем его реализации для Cash-игр в виде:
[Что нам стоит покер-бот построить] Каркас для сервера


Ну и собственно сам метод для получения экземпляра стратегии.

[Что нам стоит покер-бот построить] Каркас для сервера


Метод GetStageStratagy возвращает экземпляр CashFlop\CashPreflop\CashTurn\CashRiver. Но как этот метод определяет какой из классов CashFlop\CashPreflop\CashTurn\CashRiver нужно вернуть?
Я уже говорил, что новый функционал должно быть удобно добавлять, так вот я решил использовать для разметки нужных мне классов такую конструкцию как атрибуты:
[Что нам стоит покер-бот построить] Каркас для сервера


Теперь покажу как реализован класс CashPreflop
[Что нам стоит покер-бот построить] Каркас для сервера


не обращайте внимание на возвращаемое значение – это «заглушка». Уловили логику? Мы помечаем класс, который реализует стратегию для определённой стадии раздачи, атрибутом этой стадии, и в конструкторе указываем: к какому типу игры относится данная стратегия. Удобно, не правда ли?
Ну а теперь нам надо «спарсить» этот атрибут. Делается это с помощью рефлексии.
[Что нам стоит покер-бот построить] Каркас для сервера


Не волнуйтесь, она всегда выглядит, как магия, пока не начинаешь с ней работать. Очень хороший инструмент, позволяющий делать интересные вещи, но за это удобство приходится платить производительностью. Дальше я расскажу, как оптимизируют рефлексию, но в других статьях.
Итак, вроде со стадиями разобрались. Я упоминал вот это
в конструкторе указываем, к какому типу игры относится данная стратегия.

Давайте разбираться! Я уже заявил о том, что хочу обеспечить себе возможность маштабировать своё решение. Поэтому очевидно, что я хочу добавлять разные типы игры. Я хочу сделать это максимально удобно. Давайте пока я вам скажу, что от типа игры меняется стратегия, полностью.
Следовательно, мы имеем следующий интерфейс, описывающий тип игры
[Что нам стоит покер-бот построить] Каркас для сервера


Тогда для его реализаций мы получим:
[Что нам стоит покер-бот построить] Каркас для сервера



Свойство Strategy имеет тип IStrategy и отвечает за стратегию. Получаем мы его как раз с помощью методов SayMeStrategy и GetStageStrategy реализованных в классе, полный код которого выглядит
[Что нам стоит покер-бот построить] Каркас для сервера

Ну и собственно сама реализация класса Cash
[Что нам стоит покер-бот построить] Каркас для сервера


Так как на верхнем (контроллеров и пользовательских интерфейсов) уровне мы должны указать для какого типа игры мы хотим реализовать контролер, мы реализуем слудующий дженерик класс. Выглядит он просто:
[Что нам стоит покер-бот построить] Каркас для сервера

И опять немного маги
[Что нам стоит покер-бот построить] Каркас для сервера

Тут мы вызываем конструктор одной из реализации IGameTypeStratage и передаём в качестве параметра информацию о столе.

Вообще-то мы с вами создали хорошую инфраструктуру, поэтому в дальнейшем мы всё это закроем (модификаторами доступа) для других сборок – предоставим чёрный ящик. А для пользователя библиотекой оставим следующий интерфейс:
[Что нам стоит покер-бот построить] Каркас для сервера


Теперь где-нибудь на пользовательском уровне нам достаточно сделать следующее:
[Что нам стоит покер-бот построить] Каркас для сервера



Всё. Больше пользовательскому уровню ничего знать не требуется. Это называется Инкапсуляцией, о ней позже.

Благодаря, нашим манипуляциям мы теперь можем

Легко расширять и добавлять новый функционал в бота: для этого нам достаточно реализовать интерфейс IGameTypeStratage и интерфейс IStrategy, реализации последнего пометить аттрибутами. Всё остальное сделает наша экосистема. На сегодня, думаю достаточно, переварите информацию её получилось много. Про другие принципы расскажу в статье про тз (опять же если наберётся 20 лайков, я не хочу просто так всё это разжевывать, поймите меня).

Напоследок хочу вот что сказать: научиться пользоваться готовыми решениями не сложно вообще-то. В хакерстве должна привлекать исследовательская часть: поиск новых уязвимостей, разработка эксплойтов, методов анонимизации и прочее. Мир enterpris’a жесток и изменчив, если вам достаточно пользоваться всем готовым, искать маны по дырам, пусть. Я вас не осуждаю, но мир разработки призывает постоянно думать, совершенствоваться, изобретать, в конце концов, поэтому он не для вас, дорогие скрипт-кидди. Я при всём желании не смог бы разжевать в этой статье все аспекты такого ремесла как программирования. Читайте, господа, гуглите, расширяйте кругозор. В статье есть много новых для вас слов, изучите их, вникните и возвращайтесь ко мне. Дальше мы займёмся не такими тривиальными вещами.
 

Вложения

  • 9,8 КБ Просмотры: 294
Мы в соцсетях: