Привет из ада трассировки требований

В предыдущей статье автор сформулировал набор принципов организации хранения и оформления документации ПО, позволяющих преодолеть пропасть между требованиями верхнего уровня (ТВУ) бизнеса к программному продукту и требованиями нижнего уровня (ТНУ) к программному коду, а именно:

  1. Каждое требование записывается в отдельный файл в формате Markdown.

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

  3. Используется система документирования кода, поддерживающая отдельные файлы в формате Markdown, например Doxygen. Это позволяет генерировать html и pdf, объединяющие ТВУ и ТНУ, в т.ч. документацию на код.

  4. Требования хранятся, как и код, в git. Это позволяет избежать путаницы между версиями требований и кода. Позволяет преобразовать директивные указания бизнеса в diff между версиями требований "as is" и "to be". Позволяет не терять ссылки при переносе файловой иерархии. При этом, часть требований можно выделить в отдельный субмодуль. Это позволит, например, предоставить аналитикам и программистам разные права доступа к ТВУ и коду.

Что изменилось с момента написания предыдущей статьи? Автор отказался от идеи загонять аналитиков в IDE. Есть способ лучше. Но обо всём по порядку, для начала создадим демонстрационный git-репозиторий и первые требования.

Репозиторий с требованиями и исходным кодом

Заведём git-репозиторий в собственном или любимом облачном (github, gitlab, gitea, gitflic, ...) хранилище кода. Создадим в созданном git-репозитории отдельный каталог req в файловой иерархии исходного кода для требований. Можно создать отдельный от кода модуль для требований, но для простоты изложения автор обойдётся без него. При каждом git-коммите сохраняйте каталоги с новыми и обновлёнными требованиями в репозиторий.

Заведём копию кода локально на своём персональном компьютере.

Требования верхнего уровня

Создадим подкаталог req/ТВУ и в нём следующие файлы.

Требование Назначение-программы.md:

Программа `hello` должна демонстрировать способность вывода приветственного
сообщения пользователю.

Требование Квалификация-пользователя.md:

Пользователь программы `hello` умеет работать из командной строки Linux/bash.

Требование Минимизация-усилий.md:

Программа `hello` должна быть реализована минимальными усилиями наличных
разработчиков кода.

Требование Наличные-ресурсы.md:

Проекту доступен единственный программист, который знает только язык Си.

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

Требования нижнего уровня

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

  • Во-первых, не повторяться при документировании, расставляя ссылки из кода на требования;

  • Во-вторых, находить места кода, ссылающиеся на общие требования, в случае изменения последних, то есть проводить импакт-анализ.

Простейшие учебные проекты не требуют выделения требований нижнего уровня отдельно от кода: достаточно писать комментарии. Мы, однако, для наглядности заведём каталог req/ТНУ и пару требований нижнего уровня.

В файле Программный-стек.md:

Программа `hello` должна быть реализована как приложение на языке Си.
Это обусловлено [требованием минимизации усилий](../ТВУ/Минимизация-усилий.md)
[наличных трудовых ресурсов](../ТВУ/Наличные-ресурсы.md)
и наличия проверенного временем
[открытого аналога](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program).

В файле Консольное-приложение.md:

Программа `hello` должна запускаться из
[командной строки](../ТВУ/Квалификация-пользователя.md) и
[выводить фразу "hello, world"](../ТВУ/Назначение-программы.md)
на стандартный вывод.

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

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

Исходный код

Создадим файл hello.c в каталоге src и запишем в него документированный в стандарте Doxygen исходный код нашей программы.

/*******************************************************************************
 * \file hello.c
 * \brief Вывести приветствие
 *
 * [Консольное приложение][1] [на языке Си][2], [выводящее приветствие][1].
 *
 * [1]: ../req/ТНУ/Консольное-приложение.md
 * [2]: ../req/ТНУ/Программный-стек.md
 ******************************************************************************/

#include <stdio.h>

/**
 * \brief Выводит приветствие на стандартный вывод[1] и
 * заканчивает работу программы.
 *
 * [1]: ../req/ТНУ/Консольное-приложение.md
 */
int main( ) {
  /// Выводит приветствие на стандартный вывод.
  printf("hello, world\n");
  /// Заканчивает работу с кодом возврата 0.
  return 0;
}

Obsidian и его хранилище

Мы записали требования в файлы формата Markdown. Их можно писать в обычном IDE, однако редактирование и просмотр исходного текста в данном формате требует определённого навыка, недоступного в полной мере для большинства людей. Кроме того, есть потребность в автоматическом отслеживании ссылок между требованиями.

Obsidian, если кто ещё не в курсе, позволяет писать заметки (notes) в файлах Markdown, читать их в удобном виде, раскладывать их по папкам и отслеживать взаимные ссылки между заметками. Одна заметка – один файл Markdown. Таким образом, Obsidian вполне подходит для разработки требований в рамках предлагаемых принципов.

Установим Obsidian. В Linux с поддержкой snap:

$ sudo snap install obsidian --classic
obsidian 1.8.10 от Obsidian (obsidianmd) installed

Запустим Obsidian и заведём обсидиановское хранилище (vault):

Форма с кнопкой формирования хранилища
Форма с кнопкой формирования хранилища

В открывшейся форме выбора файла следует выбрать путь к папке req. Obsidian создаст в ней набор служебных файлов в подкаталоге .obsidian. Их не следует сохранять в git, т.к. у каждого члена вашей команды могут быть свои собственные настройки и текущее рабочее состояние. Кроме того, могут появиться и альтернативы самому Obsidian.

После создания и открытия хранилища req в Obsidian мы получаем возможность смотреть и редактировать требования в более удобном виде, перемещаясь по ним с помощью привычного навигационного дерева:

Просмотр хранилища требований в окне Obsidian
Просмотр хранилища требований в окне Obsidian

Идентификация требований

Мы вынесли заголовки требований в имена файлов, их содержащих. Файлы требований объединили в папки по логическому признаку, в нашем случае — уровню требований.

Такое вот "естественное" именование файлов порождает несколько проблем:

  1. Записи ссылок на требования превращаются в длиннющих монстров, захламляющих комментарии в коде и требования в отдельных файлах.

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

  3. При смене названия и содержимого требования или папки, просмотр разницы между версиями в git может и не сработать должным образом: git может посчитать, что это разные файлы.

  4. Невозможно задать взаимный порядок следования требований, например при навигации по файлам в Obsidian.

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

Нумеровать требования следует с разбежкой, например через 10 (010, 020, 030, ...) или 50 (100, 150, 200, 250, ...). Это позволит, в случае необходимости, добавлять требования между уже существующими, не ломая уже установленную идентификацию. То же самое касается и папок.

Таким образом, относительный путь из комментария в коде или требования более низкого уровня будет выглядеть, например, так: ../../req/100/200.md . Кликнув каким-то образом на этот путь в приличном IDE, разработчик попадёт в Markdown файл требования, после чего сможет кликнуть на относительную ссылку на ещё более высокоуровневое требование.

Заголовки и папочные заметки

Замена заголовков в именах файлов требований и содержащих их папок на абстрактные номера имеет свою цену: навигация по файлам требований привычными средствами перестаёт быть очевидной, ведь абстрактные номера не соотносятся с заголовками внутри файлов. Более того, для заголовков групп требований и вовсе не остаётся места в "естественной" файловой иерархии.

Для решения этой проблемы автор использует Obsidian с набором его установленных и настроенных расширений (плагинов).

Front Matter Title

Будем указывать текстовые заголовки требований как заголовки первого уровня в начале тел самих файлов требований. Это позволит нам, в частности, увидеть их в навигационной панели Obsidian путём установки стороннего плагина Front Matter Title и задания следующих его настроек:

Настройка

Значение

Common main template

#heading

Processor->Выпадающий список

Function V2

Processor->Текст функции

return obj.file.basename + ". " + obj.title

Features

Включить Explorer, Graph, Header, Inline, Backlink, ...

Folder Notes

Группы файлов требований, объединённые в папки, тоже могут иметь свои осмысленные заголовки. Для размещения заголовков файловых папок рядом (на том же уровне файловой иерархии) с самими папками заводятся т.н. папочные заметки ("folder notes"): заметки, имеющие такое же название, как и сама папка (за исключением расширения .md), и синхронизированные с ней с помощью стороннего обсидиановского плагина Folder Notes.

Необходимые и полезные настройки плагина, отличные от исходных, следующие:

Группа настроек

Настройка

Значение

General

Storage location

In the parent folder

General

Enable front matter title plugin integration

Включено

General

Auto-create on folder creation

Включено

Backlinks

Ещё один полезный плагин "из коробки" — Backlinks. Включим в настройках "Show backlinks at the bottom of notes", после чего для каждого требования верхнего уровня сможем увидеть зависимые требования нижнего уровня. К сожалению, данный плагин не способен показывать ссылки на требования, проставленные непосредственно из кода, так что ссылки на требования в коде придётся искать отдельно поиском по шаблону.

Опять hello, world!

Вот и вся наука. Настроим расширения Obsidian указанным выше образом и произведём следующие действия с нашими файлами:

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

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

    Вид окна подтверждения преобразования ссылок
    Вид окна подтверждения преобразования ссылок
  3. Добавим папочную заметку к каждой из папок с заголовком папки. Это делается
    нажатием кнопки + напротив "Create folder notes for all folders" в
    настройках плагина Folder notes.

  4. Сменим имена папок и папочных заметок на трёхзначные числа.

  5. Возрадуемся:

Вид окна Obsidian с преобразованными заголовками в панели навигации
Вид окна Obsidian с преобразованными заголовками в панели навигации

Получившиеся файлы будут выглядеть примерно следующим образом.

Заголовок группы требований req/100.md:

# ТВУ

Требование req/100/100.md:

# Назначение программы
Программа `hello` должна демонстрировать способность вывода приветственного
сообщения пользователю.

Требование req/100/200.md:

# Квалификация пользователя
Пользователь программы `hello` умеет работать из командной строки Linux/bash.

Требование req/100/300.md:

# Минимизация усилий
Программа `hello` должна быть реализована минимальными усилиями наличных
разработчиков кода.

Требование req/100/400.md:

# Наличные ресурсы
Проекту доступен единственный программист, который знает только язык Си. 

Заголовок группы требований req/200.md:

# ТНУ

Требование req/200/100.md:

# Программный стек
Программа `hello` должна быть реализована как приложение на языке Си.

Это обусловлено [требованием минимизации усилий](../100/300.md)
[наличных трудовых ресурсов](../100/400.md) и наличия проверенного временем
[открытого аналога](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program).

Требование req/200/200.md:

#Консольное приложение
Программа `hello` должна запускаться из [командной строки](../100/200.md) и
[выводить фразу "hello, world"](../100/100.md) на стандартный вывод.

Исходник src/hello.c:

/*******************************************************************************
 * \file hello.c
 * \brief Вывести приветствие
 *
 * [Консольное приложение](../req/200/200.md) [на языке Си](../req/200/100.md),
 * [выводящее приветствие](../req/200/200.md).
 ******************************************************************************/
#include <stdio.h>

/**
 * \brief Выводит приветствие [на стандартный вывод](../req/200/200.md) и
 * заканчивает работу программы.
 */
int main( ) { 
    /// Выводит приветствие на стандартный вывод.
    printf("hello, world\n");
    /// Заканчивает работу с кодом возврата 0.
    return 0;
}

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

Комментарии (1)