Найти в Дзене
Nikolay Matveychuk

Проектирование кода (на примере шахмат)

"Неделя кодирования может уберечь Вас от нескольких часов проектирования"
Оглавление

Как говорится "Неделя кодирования может уберечь Вас от нескольких часов проектирования". Осознание этого приходит не ко всем и далеко не сразу, но проектирование является очень важным этапом разработки чего либо, позволяющим построить код так, чтобы он:

  1. Решал поставленные задачи
  2. Был легко поддерживаемым и дорабатываемым
  3. Приносил удовольствие от работы с ним, а не желание убиться

Чего нужно пытаться достичь при проектировании

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

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

И это всё! Многие наверняка заметят, что это 2 из 5 принципов SOLID, и в целом так и есть, но лишь потому, что из этих двух следуют все остальные, и они настолько общие для всего программирования, что использование их в ООП - это лишь частный случай.

Ещё важно: не надо пытаться построить всё правильно с первого раза - гарантированно получится плохо. Например проект, который я предложу к рассмотрению, делался в 4 этапа, когда на первом проектировались интерфейсы, на втором конкретные классы, а потом было ещё 2 подхода с правками и дополнениями.

Что и в чём будем проектировать

Для рисования схем я использую самый простейший uml редактор из найденных: plantuml. В целом можно использовать любой другой, но этот мне понравился в силу функциональности и стабильности, а также доступности полной справки в сети на сайте https://plantuml.com/ (редактор текстовый, он по текстовым объявлениям рисует изображение).

Рассмотреть я в этой статье хочу проект для написания игры в шахматы, но только не совсем их, ведь как я писал выше, если спроектировать всё правильно, то код станет универсальным и всё не будет завязано на конкретные реализации. Вот и у меня в итоге получился скорее проект пошаговой стратегии, чем просто шахмат.

Определение основных элементов и их зоны ответственности

Разумеется первым делом в реальной работе нужно понять задание и разобраться из каких элементов будет состоять решение и какие из них будут изменяемыми. В случае с шахматами есть 3 основных элемента: сама логика шахмат, компонент по обработке действий пользователя, компонент по выводу результата на экран. То есть самый обычный MVC. Но так, как последних 2 компонента особой ценности не представляют, детально рассмотрим именно первый уровень - саму логику.

Основные элементы шахмат:

  • Поле - отвечает за состояние игры (даёт доступ ко всем объектам игры) и контролирует соблюдение правил
  • Фигуры - отвечает за состояние и поведение фигур
  • Игроки - предоставляет доступ к данным игроков

Многие бы на этом и остановились, но давайте обратимся к принципам заявленным в начале статьи, а именно к принципу единственной ответственности. Это значит, что любое И в заявленной сфере ответственности является потенциальным проблемным местом, потому избавимся от них и добавим ещё 2 элемента

  • Правила игры - контролирует соблюдение правил
  • Действие фигур - определяет поведение фигур

Но вот незадача, запрет на ход фигуры в случае, если это приведёт к шаху своему королю - это относится к правилам игры или к поведению фигур? - вроде и к тому и к другому, а значит надо выделить ещё одну сферу ответственности, чтобы избежать конфликта

  • Фильтр действий фигур - определяет особые правила влияющие на поведение фигур

Кроме того, очень хотелось бы предусмотреть возможность изменения структуры поля: его формы, размеров, формы клеточек (количества соседей у клетки), размерности (2d, 3d например). Следовательно нужно бы добавить ещё 2 элемента:

  • Форма - отвечает за размеры поля и форму (доступность координат)
  • Метрика - отвечает за систему координат

Вроде добились единственной ответственности для всего.
Полученный в результате полный список:

  • Поле - отвечает за состояние игры (даёт доступ ко всем объектам игры)
  • Фигуры - отвечает за состояние фигур
  • Игроки - предоставляет доступ к данным игроков
  • Правила игры - контролирует соблюдение правил
  • Действие фигур - определяет поведение фигур
  • Фильтр действий фигур - определяет особые правила влияющие на поведение фигур
  • Форма - отвечает за размеры и форму поля (доступность координат)
  • Метрика - отвечает за систему координатеречь интерфейсов разделённых по зонам ответственности
перечь интерфейсов разделённых по зонам ответственности
перечь интерфейсов разделённых по зонам ответственности

полноразмерное изображение

Проектирование интерфейсов

Самое время детализировать каждый интерфейс. На этом этапе ничего сложного нет, но требуется некоторое воображение. Нужно продумать что должна уметь каждая из этих абстракций, и какие данные ей нужны для выполнения тех или иных действий. Это и определит набор методов и их параметров для каждого из интерфейсов. Основное правило, о котором надо помнить - любая абстракция должна быть полной. Это означает, например, что нет смысла делать функцию сложения векторов, но не делать функцию их вычитания, или предусматривать метод добавления фигуры на поле, но не предусматривать метод для её вычитания и т.д.

перечень интерфейсов с указанием методов
перечень интерфейсов с указанием методов

полноразмерное изображение

Как видно на рисунке, в следствии перечисления методов, у нас появился ещё один интерфейс - коллекция фигур (IUnitsCollecton), а также оказалось, что имеет смысл отнаследовать интерфейс поля от интерфейса правил. Итак по порядку:

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

Проектирование основных абстракций классов

Это один из самых простых этапов. На нём необходимо предусмотреть каким из интерфейсов нужны классы с общими реализациями методов (которые просто бессмысленно реализовать как-то иначе), а также заявить конструкторы для всех таких классов, так как конструкторы в интерфейсах не заявляются. На данном этапе у нас появятся 4 типа связей, полагаю имеет смысл их расшифровать:

  • Наследование - эта связь была на прошлом изображении, обычная стрелочка
  • Реализация - это связь класса с интерфейсом, который этим классом реализуется. Обозначается пунктирной стрелочкой
  • Агрегация - это метод связи двух объектов, когда один из них содержит одним из своих атрибутов другой объект, который передаётся извне. Обозначается линией с белым ромбиком на конце (ромбик указывает на основной класс)
  • Композиция - это метод связи двух объектов, очень похожий на агрегацию, с тем лишь отличием, что включаемый объект существует исключительно как часть основного, создаётся вместе с ним и уничтожается при удалении основного (например как координаты фигуры, которые не имеют смысла в случае удаления этой фигуры). Обозначается как и агрегация, но с закрашенным ромбиком.

Для удобства я сразу разбил сущности по категориям (выделил более глобальные зоны ответственности)

Полная модель абстракции классов и интерфейсов для пошаговой стратегии типа шахмат
Полная модель абстракции классов и интерфейсов для пошаговой стратегии типа шахмат

Полноразмерное изображение

Как можно видеть на схеме, основная задумка удалась, так как даже уровень абстракций связан не жёстко, даже самый сложный объект имеет всего 4 зависимости, при чём глобальные зоны ответственности также имеют лишь редкие связи (не более 2 связей между каждой парой зон). то есть система получилась достаточно гибкой

Проектирование конкретных классов

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

Полная объектная модель игры в шахматы
Полная объектная модель игры в шахматы

Полноразмерное изображение

Как видно из рисунка, конкретные реализации вообще оказались друг с другом никак не связанными, то есть поставленная цель была достигнута в полной мере, и изменение любой реализации не будет требовать внесения изменений в другие части программы, благодаря чему код станет максимально простым и стабильным

Дополнительные плюсы от проделанной работы

Писали шахматы, а получилась универсальная пошаговая стратегия.

Нужно сделать из двухмерной трёхмерную? - определяем новые поведения для фигур и структуру поля (трёхмерные) и подключаем вместо двухмерных.

Нужно изменить форму поля? - определяем новую структуру и поля

Нужно не передавать ход после каждого перемещения фигуры, добавить новые типы фигур, сделать вместо шахмат космический бой, добавить кроме перемещения и атаки ещё и добычу золота? - запросто, достаточно заменить нужный объект не трогая всего остального.

В следующей статье я рассмотрю конкретные подходы к написанию кода.

Если Вам понравилась статья, подписывайтесь, ставьте лайки, комментируйте.