Contents
Основной задачей проектирования системы является определение ее основных компонентов, а также способа их взаимодействия друг с другом и окружением.
The purpose of the System Design is to supplement the system architecture providing information and data useful and necessary for implementation of the system elements. Design definition is the process of developing, expressing, documenting, and communicating the realization of the architecture of the system through a complete set of design characteristics described in a form suitable for implementation. -- Guide to the Systems Engineering Body of Knowledge (SEBoK), 2012
Цель разработки системы — дополнить архитектуру системы, предоставляя информацию и данные, полезные и необходимые для реализации элементов системы. Проектирование системы— это процесс разработки, описания, документирования и передачи информации о реализации архитектуры системы посредством полного набора конструктивных характеристик, описанных в форме, подходящей для реализации.
Под «проектированием ПО» понимают разработку или изобретение схемы преобразования спецификации приложения в готовое приложение. Проектирование — это тот процесс, который связывает выработку требований с кодированием и отладкой. Структура удачного высокоуровневого проекта приложения может успешно охватывать целый ряд более низкоуровневых проектов. Хорошее проектирование полезно при работе над небольшими приложениями и просто необходимо при работе над крупными. -- Стив Макконнелл, Совершенный код, 2010
Есть множество видов проектирования, по различным характеристикам применяемого метода или подхода. В логическом подходе системного проектирования, ориентированном на модули, можно выделить в отдельную группу нисходящее проектирование (сверху вниз или функциональное), восходящее (снизу вверх или объектно-ориентированое), *эволюционное* (метод расширения ядра), иерархическое проектирование модулей (метод Джексона). Как правило, в реальной жизни применяются различные сочетания этих методов.
В данной статье будет рассмотрен нисходящий метод проектирвания (сверху вниз).
Для проектирования сверху вниз необходимо понимать главную абстрактную функцию проектируемой системы. Такое проектирование происходит путем последовательных шагов уточнений. При каждом шаге уточнения уровень абстракции системы в целом должен уменьшаться. С каждым шагом операция разбивается на композицию более простых операций.
Рассмотрим для примера такую задачу: необходимо рассчитать зарплату для работника за месяц.
Можно представить простейший алгоритм решения, который базируется на формуле расчета.
- Получить данные о работнике.
- Получить количество часов.
- Рассчитать зарплату по формуле.
На каждом шаге проектировщик должен проверять оставшиеся элементы к уточнению и раскрывать их, используя тот же механизм. Уточнение заканчивается, когда на самом низком уровне абстракции допускается реализация.
Для нисходящего метода важно начинать проектирование с описания программы, и затем переходить к детализации конкретных элементов. На практике нужно определить, что программа будет делать на самом высоком уровне и уже после этого начинать детализацию, касающейся каждого конкретного действия. Нужно составить список действий, которые будет выполнять программа. Каждый элемент списка должен представлять только одну функциональность. Можно представлять каждый элемент как черный ящик на этом этапе. Затем, нужно более подробно описать каждый функциональный элемент как функциональный модуль. Это можно сделать например на уровне псевдокода или блок схемы, начиная описание от основного цикла.
Итерация полезна на многих этапах разработки ПО. При разработке первоначальной спецификации системы вы составляете с заказчиком несколько версий требований, пока не достигнете согласия. Это итеративный процесс. Гибкий процесс разработки, предусматривающий создание и поставку системы по частям, тоже итеративен. Прототипирование, имеющее целью быструю и дешевую разработку предварительных вариантов решений, — еще одна форма итерации. Итеративная выработка требований, наверное, не менее важна, чем любой другой аспект процесса разработки ПО. Проекты завершаются неудачей потому, что разработчики приступают к решению проблемы, не изучив альтернативных вариантов. Итерация позволяет лучше узнать систему перед ее созданием. -- Стив Макконнелл, Совершенный код, 2010
Для демонстрации, вернемся к задаче, описанной выше и проведем функциональную декомпозицию, беря в основу ее главную функцию и составляющие действия.
Расчет заработной платы:
- запросить имя работника
- получить данные работника
- рассчитать сумму по формуле
- напечатать результат
Теперь имея больше деталей, нужно продолжить разбивать полученные функции на более мелкие.
Запросить имя работника:
- показать форму запроса
- проверить введенные данные
- вернуть строку
Получить данные работника:
- прочитать имя работника
- выполнить запрос
- вернуть структуру данных
Рассчитать сумму по формуле:
- прочитать структуру данных пользователя
- подставить данные в формулу
- вернуть число
Напечатать результат:
- прочитать число
- отформатировать число
- напечатать сумму
'a В дальнейшем понадобится определить каждый из этих модулей в отдельности. Если при их описании будут создаваться новые функциональные элементы, то они должны быть также описаны.
Отформатировать число:
- прочитать число
- применить формат отображения денежной единицы
- вернуть строку
Особое внимание заслуживает не определенная структура данных. Схема сейчас описывает “что” должна делать система, но не “как” (каким образом). Структуру данных, конечно же, нужно определить до начала кодирования. Это определение и его реализация задаст еще один уровень ограничений будущей системы.
В высококачественном приложении должна быть очевидна связь между концептуальной целостностью архитектуры и ее низкоуровневой реализацией. Реализация должна соот ветствовать высокоуровневой архитектуре и обладать внутренней согласованностью. -- Стив Макконнелл, Совершенный код, 2010
Когда все функциональные элементы определены, можно начинать писать код.
Достоинства нисходящего метода проектирования:
- метод проектирования сверху вниз логичен, организует дисциплину мышления
- помогает систематизировать знания на раннем этапе проектирования системы
- позволяет поставить во главу процесса бизнес потребности и проблемы
Недостатки нисходящего метода проектирования:
- задача “описать всю систему целиком” является достаточно сомнительной
- метод не способен учесть эволюционную природу программных систем.
Как только мы разделяем уровни, мы невольно устраняем взаимосвязь и взаимодействие между ними, выводим явление или предмет из естественного контекста, в котором они существуют. В результате, чем «уже», конкретнее задача, тем больше требуется абстрагироваться от реальности, вводя новые допущения. -- Блашенкова В. Методы социологического исследования
Эволюция системы предполагает, что главная функция системы со временем может перестать быть главной. В примере выше функция системы расчета зарплаты предполагает вычисление размера зарплаты и вывод отчета. В требованиях описана функциональная часть системы.
Представим, что разработка на данном этапе завершена и программа выполняет свою задачу полностью. Скорее всего разработка на этом этапе не прекратится. Хорошие системы имеют раздражающую тенденцию для разработчиков перфекционистов — возбуждать в пользователях новые идеи о функциях, которую система может делать.
Функциональный метод проектирования сверху вниз предназначен для случаев, когда проблема строго очерчена и задание состоит в вычислении одной функции — вершины проектируемой системы. -- Бертран Мейер. Основы объектно-ориентированного программирования, 2016
Как разработчик вы получили задание сделать расчет и генерацию отчета. И вслед за этим появляются просьбы расширить набор функций.
- Заказчик: Мы могли бы выводить дополнительную статистику?
- Разработчик: Да. Понадобится только ввести дополнительную таблицу в базе данных, куда будем сохранять полученные вычисления.
- Заказчик: Давайте сделаем генерацию отчетов систематической, каждый месяц?
- Разработчик: Думаю мы могли бы делать это в конце каждого месяца.
- Заказчик: А для некоторых работников мы можем делать отчеты два раза в месяц?
Процесс изменений происходит непрерывно. Новая система продолжает все еще быть системой расчета заработной платы. Но ее главная функция, которая изначальна выглядела самой важной, становится просто одной из функций системы. А иногда совсем исчезает, становясь ненужной.
- Заказчик: Для администрации нужен суммарный ежемесячный отчет.
- Заказчик: Акционеры просят делать ежеквартальные отчеты. Мы можем добавить такую функцию?
- Разработчик: Хм… Конечно, мы могли бы…
- Разработчик: А вы не могли это сказать с самого начала? Система работает сейчас в рамках только одного месяца!
Итак, нужно добавить функциональность в систему. В результате в системе расчета зароботной платы появляется функция, которая косвенно связана с первоначальной задачей — сбор статистики и генерация отчетов. Структура системы может существенно изменить. Возможно понадобится выделить вспомогательный модуль в системе. Изменения наверняка затронут главный модуль системы, который теперь выполняет более широкий спектр задач. Возможно, что вам даже захочется изменить название системы.
Если при анализе и проектировании используется метод декомпозиции, основанный на функции, то структура системы будет вытекать из исходного понимания разработчиками главной функции системы. При этом добавление новой функции, даже если это выглядит простой задачей для заказчика, может разрушить всю структуру системы. Поэтому важно найти в качестве критерия декомпозиции свойства более стабильные, чем главная функция системы. -- Бертран Мейер. Основы объектно-ориентированного программирования, 2016
В таком примере проблема относится к расширяемости, и если более точно, то качество системы не удовлетворяет критерию непрерывного расширения. Обеспечение непрерывности крайне важно при составлении реального жизненного цикла программных систем. Учитывать нужно не только первоначальную версию, но и эволюцию системы на протяжении долгого времени. Модель разработки системы, которая этого не учитывает, весьма далека от реальной жизни.
Метод проектирования, удовлетворяет критерию непрерывности, если он приводит к устойчивой архитектуре и обеспечивает объем изменений соразмерный объему изменений в спецификациях. -- Бертран Мейер. Основы объектно-ориентированного программирования, 2016
Вполне вероятно, что диалог заказчика и разработчика может закончиться следующим
- Заказчик: А мы могли бы?..
- Разработчик: Нет. Иначе мне придется все переписать.
- Заказчик: Почему?
Ответ скорее всего будет содержать отсылку к неправильно спректированной архитектуре системы.
Чтобы оценить качество архитектуры или метода проектирования, который к ней привел, нужно понять не только факт, насколько просто было ее получить, но и насколько просто ее можно изменить.
One of the differences between building architecture and software architecture is that a lot of decisions about a building are hard to change. It is hard to go back and change your basement, though it is possible.
-- Martin Fowler, Who needs an Architect? (PDF)
Любой метод проектирования имеет свои недостатки. Важно понимать, как избежать тех или иных проблем, связанных с развитием и масштабированием системы.
В последнем примере останется, пожалуй, только определить самый главный уровень программы. Очень дорого может стоить применение преждевременного упорядочивания. Примером может служить преждевременная фиксация временных ограничений.
Уточняя каждый абстрактный уровень элементов, определяются зависимости элементов и порядок их вызова. В то же время, каждый элемент может быть подвержен изменениям. Представьте, что в основный цикл программы нужно внести коррективы:
- данные должны обновляться с периодичностью, для поддержания актуальности информации после вывода
- в любой момент работы программы нужно позволять изменить входное значение
- шаг ввода данных может быть пропущен, программа должна завершить работу
- вывод данных на экран заменить на сохранение в память
- программа должна обрабатывать ввод множественных данных.
К сожалению, функциональное проектирование сверху вниз не обеспечивает достаточной гибкости архитектуры для непрерывного расширения. Мы вынуждены менять порядок выполнения операций и их компоновку до тех пор, пока не поймем, “что” главная функция должна делать и “как”, в какой последовательности будут выполняться операции.
Стоит заметить, что объектно-ориентированный метод также подвержен проблеме преждевременного упорядочивания.
В качестве решения можно предложить метод не нумерованного списка и проектирование взаимосвязей по принципу контракта. Другими словами, каждый элемент помещается в общий список как доступная операция. Далее модули рассматриваются как отдельные модули. Если какой-то функциональный модуль требует значение, которое расчитывается в другом модуле, то ввод связи вида клиент-поставщик как ограничение будет обоснованным.
Использование простых структур данных поможет облегчить внесение изменений на ранних этапах. Иначе можно получить избыточную структуру или структуру, которая будет тяжело подвергаться изменениям или не отражать реальный предмет, на конкретном этапе эволюции системы
Важно следить за относительной прозрачностью функциональных модулей. Чтобы добиться соответствия системы критерию относительной прозрачности функций, нужно добиться независимости одних функционых модулей от других конкретных. Если можно заменить вызов функции на возвращаемое значение, и состояние при этом не изменится, то можно считать, что функция относительно прозрачна. Этот совет можно свести к рекомендации использовать чистые функции, чтобы избежать побочных эффектов в системе. Также можно свести совет к рекомендации соблюдения принципов SRP (Single Responsibility Principle) и KISS (Keep it simple), что приведет к легкой поддержке функциональных модулей. У каждого компонента должна быть одна зона ответственности и изменние логики должно быть оправдано поведением в рамках этой зоны.
Проект системы должен быть рассмотрен в разрезе возможности переиспользования модулей (компонентов) и следование принципу DRY (Don't repeat yourself). Если функция может выполнять отдельную часть работы, то не нужно делать похожую функцию, которая аггрегирует в себя ее функциональность. Стоит пересмотреть описание таких модулей (функций) на предмет отношенния клиент-поставщик. Возможно понадобится заменить выделенный на этапе функциональной декомпозиции функциональный модуль и заменить его двумя зависимыми, воспользовавшись методами рефакторинга из онлайн каталога Мартина Фаулера
Идеальная техническая система — это система, вес, объем и площадь которой стремятся к нулю, хотя её способность выполнять работу при этом не уменьшается. Иначе говоря, идеальная система — это когда системы нет, а функция её сохраняется и выполняется -- Альтшуллер Г.С., Творчество как точная наука, 1979
В какой-то степени это актуально и для программных систем в частности. В любом случае стоит стремиться к минимальным изменениям в системе, используя уже имеющиеся возможности окружения.
- Заказчик: Можно сделать так, чтобы программа генерировала отчеты самостоятельно и отправляла отчеты по почте?
- Заказчик: Мы можем ввести графический интерфейс в программу, чтобы можно было просматривать все сгенерированные отчеты за последний месяц?
Это уже больше похоже на систему отчетности, даже документооборота, в некотором смысле. И выглядит совсем не похоже на систему расчета зароботной платы.