Бертран Мейер - Основы объектно-ориентированного программирования
Обнаружение вершины
Нисходящий метод проектирования предполагает, что каждая система характеризуется на самом абстрактом уровне своей главной функцией. Хотя многие учебные примеры алгоритмических проблем - "Ханойские башни", "Задача о 8 ферзях" и т. п. - действительно легко задать с помощью их "верхних" функций, более полезно описывать практические системы в терминах предоставляемых ими услуг.
Рассмотрим какую-либо операционную систему. Наиболее разумно представлять ее как систему, предоставляющую такие услуги, как распределение времени процессора, управление памятью, обращение с устройствами ввода-вывода, декодирование и исполнение команд пользователя. Модули хорошо структурированной ОС стремятся сгруппироваться вокруг этих групп функций. Но это не та структура, которую можно получить при нисходящей функциональной декомпозиции. Этот метод заставляет проектировщика отвечать на искусственный вопрос: "что является "верхней" функцией?", а затем использовать последовательные уточнения полученного ответа в качестве основы для структуры системы. При определенных усилиях можно придти к следующему ответу на исходный вопрос
"Обработать все запросы пользователя",
который далее можно уточнять примерно так:
from
начальная загрузка системы
until
остановка или аварийный отказ
loop
"Прочесть запрос пользователя и поместить во входную очередь"
"Взять запрос r из входной очереди"
"Обработать r"
"Поместить результат в выходную очередь"
"Взять результат q из выходной очереди"
"Выдать результат q получателю"
end
Уточнения могут продолжаться. Однако маловероятно, что после такого начала кому-либо удастся спроектировать разумно структурированную операционную систему.
Вернемся к примеру с компилятором. Оставив в нем самую суть или представив точку зрения старых учебников, можно сказать, что компилятор - это реализация функции типа вход-выход, трансформирующей текст исходной программы на некотором языке программирования в машинный код некоторой платформы. Но для современных компиляторов этот взгляд недостаточен. Среди многих услуг, предоставляемых компилятором, обнаружение ошибок, форматирование программы, возможность управления конфигурацией системы, вход в систему, генерация отчетов.
По-видимому, очевидная отправная точка проектирования сверху вниз - взгляд, согласно которому для каждой новой разработки требуется запросить некоторую специальную функцию - является весьма сомнительной:
У реальной системы нет "вершины"!
Функции и эволюция
Главная функция часто не только не является наилучшим критерием для начального определения системы, но она может также в процессе эволюции системы почти сразу оказаться среди изменяемых свойств.
Рассмотрим в качестве примера программу, имеющую две версии: одну "пакетную", которая выполняет во время сессии одно большое непрерывное вычисление, и другую - интерактивную, которая в каждой сессии реализует последовательность транзакций с разбиением взаимодействия пользователя с системой на более мелкие шаги. Большие научные программы очень часто имеют две версии: одну, которая "пусть работает всю ночь, выполняя большую порцию вычислений", и другую, которая "позволяет мне сначала проверить некоторые вещи, посмотреть на результаты, а затем вычислить еще что-нибудь".
Уточнение сверху вниз пакетной версии могло начаться следующим образом.
[B0] - Абстракция верхнего уровня
"Решить полный экземпляр проблемы"
[B1] - Первое уточнение
"Прочесть входные данные"
"Вычислить результаты"
"Вывести результаты"
и т. д. Проектирование интерактивной версии сверху вниз может происходить в следующем стиле.
[I1]
"Обработать одну транзакцию"
[I2]
if "Пользователь предоставил новую информацию" then
"Ввести информацию"
"Запомнить ее"
elseif "Запрошена ранее данная информация" then
"Извлечь запрошенную информацию"
"Вывести ее"
elseif "Запрошен результат" then
if "Необходимая информация доступна" then
"Получить запрошенный результат"
"Вывести его"
else
"Запросить подтверждение запроса"
if Да then
"Получить требуемую информацию"
"Вычислить запрошенный результат"
"Вывести результат"
end
end
end
(и т. д.)
Начавшаяся таким образом разработка приведет к совершенно неверному результату. Подход сверху вниз не способен учесть то обстоятельство, что результирующие программы должны быть ничем иным как двумя версиями одной и той же программной системы, независимо от того, как они проектируются - одновременно или одна выводится из другой.
Этот пример высвечивает два самых неприятных последствия подхода сверху вниз: во-первых, он сосредотачивается на внешнем интерфейсе (здесь это проявилось в раннем выборе между пакетной и интерактивной версиями), во-вторых, он преждевременно устанавливает временные отношения (т.е. порядок выполнения действий).
Интерфейсы и проектирование ПО
Архитектура системы должна основываться на содержании, а не на форме. Но проектирование сверху вниз стремится использовать в качестве основы для структуры самый поверхностный аспект системы - ее внешний интерфейс.
Такой упор на внешний интерфейс неизбежен для метода, ключевой вопрос которого: "Что система будет делать для конечного пользователя?" Ответ на него обязательно будет акцентироваться на самых внешних аспектах.
Интерфейс пользователя, как правило, оказывается одним из наиболее изменчивых компонентов, поскольку трудно получить правильный интерфейс с первой попытки. Довольно часто удается построить интерфейс отдельно от других компонент системы, используя один из множества доступных сегодня инструментов реализации элегантных и дружественных интерфейсов, основанных на ОО-методах. В таких случаях интерфейс пользователя почти не оказывает влияния на проектирование всей системы.
Преждевременное упорядочение
Предыдущие примеры иллюстрируют также и другой недостаток функциональной декомпозиции сверху вниз: преждевременную фиксацию временных ограничений. Каждое уточнение развертывает часть абстрактной структуры в более подробную архитектуру управления, задающую порядок выполнения различных функций (различных частей соответствующего действия). Такие уточнения и ограничения порядка становятся существенными свойствами архитектуры системы, но они также подвержены изменениям.
Напомним две альтернативных структуры для первого уточнения компилятора.
[C1]
"Прочесть программу и породить последовательность лексем"
"Разобрать последовательность лексем и построить абстрактное синтаксическое дерево"
"Снабдить дерево семантической информацией"
"Сгенерировать по полученному дереву код"
[C'1]
from
"Инициализировать структуры данных"
until
"Определения всех функций обработаны"
loop
"Прочесть определение следующей функции"
"Сгенерировать частичный код"
end
"Заполнить перекрестные ссылки"
Как и в предыдущем примере, мы начинаем с двух совершенно разных архитектур. Каждая из них задается некоторой структурой управления (последовательностью команд в первом случае и циклом, за которым идет команда - во втором), накладывающей строгие ограничения на порядок элементов в этой структуре. Но было бы неразумно зафиксировать такие отношения порядка на самых ранних стадиях проектирования. Такие вопросы как число проходов компилятора, установление последовательности различных этапов (лексического анализа, синтаксического разбора, семантической обработки, оптимизации) имеют много различных решений, к которым должны придти разработчики, учтя соотношения между памятью и временем и другие критерии, которыми они, возможно, не руководствовались в начале проекта. Они могут успешно выполнять работу по проектированию и реализации отдельных компонентов задолго до фиксации временного порядка между ними, и захотят подольше сохранять свободу в выборе этого порядка. Функциональное проектирование сверху вниз не обеспечивает такой гибкости: требуется определять порядок выполнения операций до появления глубокого понимания того, что эти операции будут делать.