Игра Сапер. Паттерн Mediator. Использование doxygen

Недавно помогал какому-то студенту писать лабораторную – игру “Сапер”, и получилась вполне вменяемая штука, которую решил описать на блоге. В статье рассмотрены:

  1. паттерн проектирования Mediator (посредник);
  2. цифровой таймер с использованием QLCDNumber (в нашей игре будут отображаться часики);
  3. автоматическая генерация документации с использованием doxygen.

В результате получится игрушка, снимок окна которой показан на рис. 1.

рис. 1 окно игры "Сапер"

рис. 1 окно игры “Сапер”

1 Паттерн Mediator (посредник)

При анализе технического задания на разработку игры “Сапер” (которого у нас нет) мы наверняка бы обнаружили ряд сущностей, таких как “часы”, “клетка поля”, “игровое поле”. Каждой такой сущности в соответствие можно поставить класс:

  1. Clock – часы; начинают отсчет сразу после запуска игры, завершают – при проигрыше или выигрыше;
  2. Dot – клетка поля; имеет состояние, определяемое комбинацией флажков (открыта/закрыта, содержит/не содержит мину, установлен/не установлен флаг) и счетчиком мин, расположенных вокруг этой клетки. Клетка ничего не знает о состоянии игры, поэтому не должна им напрямую управлять, однако, при нажатии определяет тип кнопки (левая/правая) мыши и генерирует соответствующий сигнал;
  3. Field – игровое поле; хранит состояние игры, и набор клеток (Dot), обрабатывает от них сигналы. При запуске игры расставляет на поле мины (может изменять состояние клеток).

На рис. 2, 3 приведены графы связей описанных выше классов, сгенерированные при помощи doxygen по исходному коду, фрагменты которого приведены на листинг 1.

рис. 2 граф связей класса Clock

рис. 2 граф связей класса Clock

рис. 3 граф связей класса Field

рис. 3 граф связей класса Field

//! одна клетка поля
class Dot : public QPushButton {
signals:
  void clicked_left();
  void clicked_right();
protected:
  int m_i, m_j;
          //!< координаты клетки
  int m_value;
          //!< значение, хранимое в клетке
  bool m_isMine;
          //!< является ли клетка миной?
  bool m_isOpen;
          //!< открыта ли клетка?
  bool m_isFlag;
          //!< помечена ли клетка флагом
};
//! игровое поле
class Field :public QWidget {
  // ...
public:
  Field(QWidget *parent = 0);
protected:
  Dot* m_field[m_n][m_n];
          //!< игровое поле
  bool m_isGameActive;
          //!< состояние игрового поля
protected slots:
  void on_DotClickedLeft();
          //!< слот обработки нажатия клетки левой кнопкой мыши
  void on_DotClickedRight();
          //!< слот обработки нажатия клетки правой кнопкой мыши
signals:
  void finished();
          //!< сигнал завершения игры
};
Field::Field(QWidget *parent): QWidget(parent) {
  m_isGameActive = true;
  QGridLayout *layout = new QGridLayout(this);
  for (int i = 0; i < m_n; ++i) {
    for (int j = 0; j < m_n; ++j) {
      m_field[i][j] = new Dot(i, j, this);
      layout->addWidget(m_field[i][j], i, j, 1, 1);
      connect(m_field[i][j], SIGNAL(clicked_left()), this, SLOT(on_DotClickedLeft()));
      connect(m_field[i][j], SIGNAL(clicked_right()), this, SLOT(on_DotClickedRight()));
    }
  }
//...
}

В doxygen (по умолчанию) синей стрелкой на графе связей отображается открытое наследование, а сиреневой – отношение агрегации/композиции (агрегация и композиция на этой диаграмме не различаются). В конце статьи описано как получить при помощи doxygen UML-диаграмму классов.

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

Если мы хотим связать таймер с игровым полем, то можем использовать маттерн Mediator [3] – нам надо создать еще один класс (назовем его Mediator), который будет инкапсулировать связи поля и таймера.

На рис. 4 приведен граф связей класса Mediator, сгенерированный при помощи doxygen по листинг 2.

//! посредник между полем и таймером
class Mediator :public QWidget {
  Q_OBJECT
public:
  Mediator();
public slots:
  void on_gameReset();
protected:
  Field *m_field;
                //!< игровое поле
  Clock *m_time;
                //!< отображает время игры
  QGridLayout *m_layout;
                //!< раскладка виджета
};
Mediator::Mediator(): m_field(0), m_time(0) {
  m_layout = new QGridLayout;
  setLayout(m_layout);
  on_gameReset();
}

void Mediator::on_gameReset() {
  if (m_field) delete m_field;
  if (m_time) delete m_time;
  m_time = new Clock(this);
  m_field = new Field(this);

  connect(m_field, SIGNAL(finished()), m_time, SLOT(stop()));

  m_layout->addWidget(m_time, 0, 0, 1, 1, Qt::AlignCenter);
  m_layout->addWidget(m_field, 1, 0, 1, 1);
}

рис. 4 граф связей класса Mediator

рис. 4 граф связей класса Mediator

Класс Mediator позволяет таймеру получать сигнал finished() от игрового поля. К сожалению, doxygen не отображает на диаграммах такие связи.

В этом примере применение посредника позволило сократить количество связей между классами, а значит, улучшить повторное использование кода.

2 Цифровой таймер с QLCSNumber

В каждом объекте Qt (QObject) есть встроенный таймер, мы могли бы его использовать, но вместо этого наши часы будут агрегировать экземпляр QTimer – это более гибкий вариант. Другой пример использования QTimer можно найти на официальном сайте [2].

#ifndef CLOCK_H
# define CLOCK_H
# include <QLCDNumber>
# include <QTime>

//! класс отображения цифровых часов
class Clock :public QLCDNumber {
    Q_OBJECT
public:
  Clock(QWidget *parent = 0);
private slots:
  void on_tick();
                //!< слот обработки сигнала таймера
  void stop();
                //!< слот остановки таймера
protected:
  QTime m_time;
                //!< текущее время
  QTimer *m_timer;
                //!< таймер
};

#endif // CLOCK_H

В отличии от таймера из примера nokia наш таймер отображает не системное время, а отсчитывает время с определенного момента. Для хранения времени используется экземпляр класса QTime. Кроме того, наш таймер умеет останавливаться (слот stop). Реализация методов класса Clock приведена на листинг 4.

Clock::Clock(QWidget *parent) : QLCDNumber(parent), m_time(0, 0, 0) {
  setSegmentStyle(Filled);

  m_timer = new QTimer(this);
  connect(m_timer, SIGNAL(timeout()), this, SLOT(on_tick()));
  m_timer->start(1000);

  display(m_time.toString("hh:mm:ss"));

  setFixedSize(300, 30);
}

void Clock::on_tick() {
  m_time = m_time.addSecs(1);
  display(m_time.toString("hh:mm:ss"));
}

void Clock::stop() { m_timer->stop(); }

В 8 строке листинг 4 создается таймер, сигнал которого соединяется со слотом on_tick в 9 строке. В 10 строке задается период выдачи сигналов таймером (1000мс). По приходу сигнала значение времени, хранимое полем m_time увеличивается на 1 секунду (18 строка).

3 Генерация документации с помощью doxygen

Разработка документации – один из стандартных элементов жизненного цикла программного обеспечения, однако, существуют инструменты, позволяющие сгенерировать документацию автоматически по исходному коду. К таким инструментам относится doxygen [4].

Doxygen позволяет получить документацию по исходному коду программ на С/С++, Python и ряде других языков. Исходный код программы должен быть снабжен комментариями, оформленными определенным образом. Есть несколько стилей написания комментариев, поддерживаемых doxygen. В статье я опишу лишь те возможности, и тот стиль оформления комментариев программ на С++, который сам использую.

Комментарий, который должен быть учтен в документации начинается с символов //! (если он однострочный) и //* (если комментарий многострочный). При этом doxygen не различает многострочный комментарий и несколько многострочных, т.е. следующие фрагмента для него эквивалентны:

/*! комментарий 1
 продолжение комментарий 1
 детали комментарий 1
*/
//! комментарий 2
//! продолжение комментарий 2
//!
//! детали комментарий 2

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

Doxygen поддерживает теги, такие как \brief, \class, \file, \enum и т.п., из всего их разнообразия я использую только \file (означает, что текущий комментарий относится к файлу, имя которого указывается сразу после тега) и \brief (короткое описание). При этом, тег \brief я использую только при описании файла, в остальных случаях он подразумевается в первом абзаце). Пример документирования файла приведен на листинг 6, фрагмент сгенерированной документации – на рис. 5.

//! \file example.h
//! \brief пример документирования файла
//! системой doxygen
//!
//! файл содержит описание перечисления Ex_1
//! и комментарии к нему
//! пример документирования перечисления
enum Ex_1 {
 	A,		//!< элемент перечисления А
	B,
			//!< элемент перечисления В
	C,
			//!< элемент перечисления С
			//!<
			//!< детальное описание элемента С
};

рис. 5 фрагмент сгенерированной документации листинг 6

рис. 5 фрагмент сгенерированной документации листинг 6

На листинг 6 помимо комментария к файлу приведен документаций к перечислению (enum), который оформляется аналогично комментарию к стуктуре, классу или объединению.

Комментарий к классу в doxygen размещается перед определением класса и может содержать короткое и подробное описания. Комментарий к элементу класса/структуры/перечисления/объединения начинается с символов //!< и размещается после описания элемента (как с текущей, так и со следующей строки). Я размещаю комментарии к членам на следующей строке чтобы не возникало трудностей с выравниванием комментариев в случаях, если описание члена очень длинное.

Чтобы собрать документацию по исходному коду необходимо создать конфигурационный файл doxygen, для этого можно использовать утилиту doxywizard (описывать работу с ней я не буду, т.к. она имеет мышкотыкательный интерфейс). В частности, doxywizard позволяет выбрать формат документации (pdf/html/man/…) и типы выводимых диаграмм. На мой взгляд, самыми полезными диаграммами в doxygen являются граф вызовов и граф связей.

Doxygen может сгенерировать UML-диграмму классов, для этого необходимо открыть конфигурационный файл doxygen и установить вручную опции следующим образом:

    EXTRACT_ALL            = YES
    HAVE_DOT               = YES
    UML_LOOK               = YES

Добиться аналогично результата использованием doxywizard у меня не получилось.

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

Исходный код игры “Сапер”: Сапер[Qt] (исходный код)

документация к исходному коду: Сапер[Qt] (документация)

Список рекомендуемой литературы

  1. Документация библиотеки Qt.- URL: http://doc.qt.io/
  2. Digital Clock Example на сайте Nokia.- URL: http://doc.qt.io/qt-5/qtwidgets-widgets-digitalclock-example.html
  3. Книги раздела “Проектирование”.- URL: https://pro-prof.com/books
  4. Официальный сайт doxygen. – URL: http://www.stack.nl/~dimitri/doxygen/

4 thoughts on “Игра Сапер. Паттерн Mediator. Использование doxygen

    1. admin Post author

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

    1. admin Post author

      Чтобы запустить программу Вам нужно ее скомпилировать. К статье приложен исходный код, а не исполняемый файл. Исполняемый файл не приложен потому, что для различных платформ он должен быть разным, у меня вот 32-разрядный openSuse, а у Вас может быть 64-разрядная Windows-7 – исполняемый файл, собранный у меня Вам ничем не поможет.

      Чтобы скомпилировать программу Вам нужно установить библиотеку Qt, компилятор C++ (наверное mingw Вам нужен), настроить пути к компилятору и библиотеке в переменных окружения операционной системы и вызвать qmake из каталога с файлом проекта.

      Либо можно прописать пути к компилятору в интегрированной среде разработки Qt Creator, которая идет в поставке с библиотекой Qt и собрать в нем (переменные окружения править не надо).

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