ЧАСТЬ ТРЕТЬЯ
Примеры приложений
Чтобы построить теорию, необходимо как следует знать основные феномены изучаемой области. Что касается теории вычислений, то мы не знаем этого предмета настолько, чтобы изложить его на высоком уровне абстракции. Напротив, нам следует объяснять хорошо изученные конкретные примеры, на основе которых мы сможем догадаться об общих принципах и обосновать их.
Марвин Минский (Marvin Minsky)
Форма и содержание в компьютерной науке
(Form and Content in Computer Science)
Глава 8
Система сбора данных: метеорологическая станция
Теория теорией, но сточки зрения инженера-практика никакая, даже самая элегантная методология, предлагаемая учеными, не стоит и ломаного гроша, если она не помогает в построении реальных, работающих систем. Предыдущие семь глав были лишь прелюдией к данному разделу книги, где будут рассмотрены приложения объектно-ориентированного анализа к решению практических задач. В этой и в оставшихся четырех главах мы будем придерживаться следующей схемы: рассмотрев требования к той или иной системе, формализуем задачу, используя стандартные условные обозначения, и далее, в процессе объектно-ориентированной разработки, придем к некоторому решению. В качестве примеров был выбран ряд самых разнообразных областей, включая обработку данных, информационные системы, искусственный интеллект и управление. Каждой из них присущи свои особенности. Здесь вы не найдете подробного описания полученных решений, так как в этой книге мы обращаем основное внимание на анализ и проектирование, а не на программирование как таковое. Мы, однако, включили достаточно полное описание перехода от анализа к проектированию и, затем, к реализации проекта, а также обратили внимание на наиболее интересные аспекты, связанные с особенностями архитектуры рассматриваемых систем.
Требования к метеорологической станции
Система должна обеспечивать автоматический мониторинг следующих первичных погодных параметров:
-
скорость и направление ветра;
-
температура;
-
барометрическое давление;
-
влажность воздуха.
Система также должна вычислять некоторые производные параметры,
в число которых входят:
-
коэффициент резкости погоды;
-
точка росы;
-
относительное изменение температуры;
-
относительное изменение барометрического давления.
В системе должна быть предусмотрена возможность определения текущего времени и даты, которые будут использоваться при генерации сообщении о максимальных и минимальных значениях первичных параметров за последние 24 часа.
Система должна обеспечивать постоянный вывод на дисплей текущих значений всех восьми первичных и производных параметров, а также текущее время и дату. Пользователь должен иметь возможность увидеть максимальные и минимальные значения любого из первичных параметров за 24 часа, сопровождаемые информацией о времени произведения соответствующего замера.
Система должна позволять пользователю проводить калибровку датчиков по известным опорным значениям, а также устанавливать текущие время и дату. |
8.1. Анализ
Определение границ рассматриваемой задачи
Врезка ознакомила вас с требованиями к системе мониторинга погоды. Это довольно простая задача, решение которой позволяет обойтись всего несколькими классами. Инженер, не вполне искушенный во всех особенностях объектно-ориентированного анализа, может сделать поспешный вывод о том, что в данном случае наиболее простым и эффективным будет отказ от объектно-ориентированного подхода. Он обратится к рассмотрению потоков данных и входных/выходных значений. Тем не менее, как мы увидим в дальнейшем, даже для такой небольшой системы лучше ввести объектно-ориентированную архитектуру, которая прекрасно проиллюстрирует некоторые основные принципы, лежащие в основе объектно-ориентированной разработки.
Мы начнем наш анализ с рассмотрения аппаратной части системы. Это задача системного анализа. Она включает в себя такие вопросы, как технологичность и стоимость системы, которые выходят за рамки данной книги. Для того, чтобы сузить проблему, ограничившись анализом и проектированием только программных средств, сделаем следующие стратегические предположения об аппаратной части:
-
Используется компьютер с одним процессором i486 [Это решение может показаться избыточным, но сейчас платформы на базе 486-го процессора ненамного дороже компьютеров с процессорами предыдущих поколений. Закладывая в требования избыточную производительность компьютера, мы обеспечиваем больший срок жизни разрабатываемой системы].
-
Системные время и дата поддерживаются встроенными часами,
соответствующие значения отображаются в оперативную память.
-
Температура, барометрическое давление и влажность определяются встроенными контроллерами, которые соединены с соответствующими датчиками; показания контроллеров также отображены в оперативную память.
-
Направление ветра измеряется с помощью флюгера с точностью до одного из 16 направлений; скорость ветра определяется анемометром со счетчиком оборотов.
-
Ввод команд пользователем осуществляется с помощью клавишной панели (похожей на телефонную), сигналы которой обрабатываются с помощью встроенной платы. Эта же плата обеспечивает звуковой сигнал после каждого нажатия клавиши. Последняя команда пользователя сохраняется в памяти.
-
Экраном служит обычный жидкокристаллический дисплей. Встроенный
контроллер дисплея обеспечивает вывод на экран небольшого набора графических
примитивов: линий и дуг, закрашенных областей и текста.
-
Встроенный таймер посылает прерывания через каждую 1/60 долю
секунды.
На рис. 8-1 приведена диаграмма, иллюстрирующая состав аппаратной
части системы.
Рис. 8-1. Аппаратное обеспечение системы мониторинга
погоды.
Мы решили несколько упростить аппаратную модель для того,
чтобы уделить больше внимания программной части. Очевидно, можно было бы
вообще не определять типы устройств ввода команд и вывода информации на
экран, что почти не затронуло бы архитектуру системы. Одной из особенностей
объектно-ориентированного подхода является стремление говорить на языке
проблемной области, что облегчает проведение параллелей между программными
абстракциями и ключевыми понятиями исходной задачи. Изменения в аппаратной
части оказывают влияние лишь на некоторые нижние уровни системы.
Что касается особенностей организации ввода/вывода посредством отображения в память, то нам не хотелось бы подробно на них останавливаться, так как эти детали в большой степени зависят от способа реализации проекта. Мы можем легко изолировать наши программные абстракции от этих "неинтересных" подробностей, скрыв их в реализациях соответствующих классов. Например, имеет смысл создать простой класс для определения текущего времени и даты: для этого надо провести небольшой анализ, в процессе которого придется рассмотреть роли и обязанности данной абстракции [На самом деле, прежде чем создавать класс, полезно порыться в доступных вам библиотеках и постараться найти там что-нибудь похожее. Класс времени и даты - хороший кандидат на повторное использование, и скорее всего кто-нибудь уже разработал и отладил подобную абстракцию. Однако, для целей нашего изложения лучше предположить, что такого класса в готовом виде не нашлось]. Мы, в частности, могли бы прийти к решению, что данный класс ответственен за отслеживание информации о текущем времени в часах, минутах и секундах, а также о дате (текущий месяц, день и год). В результате анализа мы также могли бы сделать вывод о том, что среди обязанностей класса необходимо выделить две услуги: предоставление информации о текущем времени (currentTime) и о текущей дате (currentDate). Операция currentTime возвращает текстовую строку следующего формата:
13:56:42
показывающую текущие час, минуту и секунду. Операция currentDate возвращает строку следующего формата:
3-20-98
показывающую текущие месяц, день и год.
Дальнейший анализ подсказывает, что могут понадобиться более полные абстракции, позволяющие клиенту выбирать между 12- и 24-часовым форматом времени. Одно из возможных решений - введение дополнительного модификатора setFormat, меняющего формат представления текущего времени.
Определив поведение данной абстракции с точки зрения клиента, мы предлагаем затем четко разделить интерфейс класса и его реализацию. Основная идея состоит в том, чтобы сначала определить внешний интерфейс каждого класса, не задумываясь при этом об особенностях его внутреннего строения. Реализация интерфейса класса через его внутреннее устройство происходит на этапе разработки. Реализация класса осуществляет связь между внешним представлением об абстракции и ее воплощением в конкретной аппаратной платформе, которую, как правило, инженер-программист изменить не в силах. Но при этом, конечно, надо следить, чтобы разрыв между программной абстракцией и внутренним устройством не был слишком большим и не требовал от программиста громадных усилий по "склеиванию" совершенно разнородных понятий.
Рис. 8-2. Жизненный цикл класса TimeDate.
Предположим, что наша аппаратная модель обеспечивает доступ ко времени и дате как к 16-битовому целому числу, которое показывает, сколько секунд прошло со времени включения компьютера [В простейшем случае может использоваться аппаратно реализованный счетчик, увеличивающий свое значение на единицу каждую секунду. Более изощренная реализация может использовать микросхему времени/даты с питанием от батарейки. В любом случае, внешний вид этого класса (контракт с клиентами) должен быть одним и тем же. Наша реализация класса отвечает за поддержание этого контракта на данной аппаратной платформе]. Тогда наш класс времени и даты должен обеспечивать пересчет этой сырой информации в полезные значения, что, в свою очередь, диктует необходимость добавления новых операций setHour, setSecond, setDay, setMonth и setYear, определяющих на основе первичных данных текущие час, секунду, день, месяц и год.
Теперь подведем итоги. Абстракция класса времени и даты выглядит так:
Имя:
TimeDate
Ответственность:
Поддержание информации о текущем времени и о
текущей дате.
Операции:
CurrentTime - текущее время
currentDate - текущая дата
setFormat - установка формата
setHour - установка часа
setMinute - установка минут
setSecond - установка секунд
setMonth - установка месяца
setDay - установка дня
setYear - установка года
Атрибуты:
time - время
date - дата
Экземпляры этого класса имеют динамический жизненный цикл, который отражен в диаграмме состояний и переходов на рис. 8-2. Мы видим, что при инициализации экземпляра класса происходит обнуление значений атрибутов и безусловный переход в рабочее состояние в режиме 24-часового формата. В рабочем состоянии можно переключать режимы с помощью операции setFormat. Операция переустановки времени и даты нормализует его атрибуты вне зависимости от текущего вложенного состояния объекта. Запрос относительно текущего времени или даты приводит к выполнению необходимых вычислений и генерации текстовой строки.
Мы достаточно детально определили поведение этой абстракции и теперь можем использовать ту же схему при изучении других сценариев, обнаруженных при анализе. Но прежде рассмотрим поведение других объектов нашей системы.
Класс TemperatureSensor (температурный датчик) служит аналогом аппаратного температурного датчика нашей системы. Изолированный анализ поведения этого класса дает в первом приближении следующий результат:
Имя:
TemperatureSensor
Ответственность:
Поддержание информации о текущей температуре.
Операции:
currentTemperature - текущая температура
setLowTemperature - установка минимальной температуры
setHighTemperature - установка максимальной температуры
Атрибуты:
temperature - температура
Название операции currentTemperature (текущая температура) говорит само за себя. Назначение двух других операций (установка минимальной и максимальной температур) прямо определяется требованием к системе, а именно необходимостью проведения калибровки датчиков. Сигнал от каждого датчика - это число с фиксированной точкой из некоторого рабочего диапазона, граничные значения которого должны быть заданы. Промежуточные значения температуры вычисляются простой линейной интерполяцией между этими двумя точками, как показано на рис. 8-3.
Внимательный читатель может задать закономерный вопрос: зачем мы создаем специальный класс для данной абстракции, когда в требованиях к системе ясно сказано, что температурный датчик может быть только один? Это верно, но в целях обеспечения возможности повторного использования абстракции мы все же выделяем ее в отдельный класс. На самом деле количество температурных датчиков не должно влиять на архитектуру нашей системы, и, выделяя отдельный класс TemperatureSensor, мы открываем возможность его использования в других программах подобного типа.
Абстракция для датчика барометрического давления может
выглядеть следующим образом:
Имя:
PressureSensor
Отвественность:
Поддержание информации о текущем барометрическом
давлении.
Операции:
currentPressure - текущее давление
setLowPressure - установка минимального давления
setHighPressure - установка максимального давления
Атрибуты:
pressure - давление
Рис. 8-3. Калибровка класса-датчика TemperatureSensor.
Однако, при более подробном рассмотрении требований к системе выясняется, что мы упустили одну важную характеристику поведения данных классов. А именно, требования к системе предусматривают определение тенденций изменения температуры и барометрического давления (относительное изменение, тренд). В настоящий момент (на этапе анализа) мы обратим основное внимание на природу этого поведения и, самое важное, выясним, какая абстракция должна отвечать за него.
И для температурного датчика, и для датчика давления тренд может быть определен как вещественное число, изменяющееся в диапазоне от -1 до 1 и представляющее собой наклон графика изменений температуры и давления на некотором интервале времени [Значение 0 показывает, что температура или давление стабильно. Значение 0.1 указывает на небольшой рост, значение -0.3 соответствует резкому уменьшению. Значения, близкие к -1 и 1 намекают на природный катаклизм, выходящий за рамки тех сценариев, в которых наша система должна исправно работать]. Таким образом, к описанию двух вышеупомянутых классов можно добавить еще одну ответственность и соответствующую ей операцию:
Ответственности:
Определение тренда давления или температуры как
наклона графика (в линейном приближении) изменения
их значений за данный интервал времени.
Операции:
trend - тренд
Отметив сходство поведения обоих классов, разумно было бы создать общий суперкласс, ответственный за определение тренда. Назовем его TrendSensor.
Вообще говоря, подобная схема не является единственно возможной. Мы решили передать ответственность за определение тренда датчикам. Можно было бы создать внешний по отношению к датчикам класс, который бы периодически их опрашивал и вычислял тренд, но мы отвергли такой вариант, так как он неоправданно усложнил бы систему. Первоначальное описание классов температурного датчика и датчика давления уже подразумевало возможность вычисления тренда "своими силами", а выявив общность (организовав суперкласс TrendSensor), мы получили в результате простую и связную систему абстракций.
Абстракцию, соответствующую датчику влажности, можно определить
следующим образом:
Имя:
HumiditySensor
Ответственность:
Поддержание информации о текущей влажности,
выраженной в процентах от 0% до 100%.
Операции:
currentHumidity - текущая влажность
setLowHumidity - установка минимальной влажности
setHighHumidity - установка максимальной влажности
Атрибуты:
humidity - влажность
Нам не ставится задача определения тренда влажности, поэтому
класс HumiditySensor, в отличие от классов TemperatureSensor
и PressureSensor, не является потомком класса TrendSensor.
Однако требования к системе подразумевают наличие общего поведения для всех трех вышеперечисленных классов. В частности, мы должны обеспечить показ максимального и минимального значений каждого параметра за 24-часа. Эту обязанность можно отразить в следующем описании, общем для всех трех классов:
Отвественность:
Генерация сообщений о максимальных и минимальных
значениях параметров за 24-часа.
Операции:
highValue - максимальное значение
lowValue - минимальное значение
timeOf HighValue - время, соответствующее максимальному
значению
timeOfLowValue - время, соответствующее минимальному
значению
Пока отложим решение вопроса о том, как реализовать эту ответственность;
мы вернемся к нему на этапе проектирования. Однако, учитывая
то, что данное поведение является общим для всех трех датчиков, представляется
целесообразным организация еще одного суперкласса, который мы назовем HistoricalSensor.
Класс HumiditySensor является прямым потомком класса HistoricalSensor,
так же как и TrendSensor. Последний служит промежуточным абстрактным
классом, переходным между абстрактным HistoricalSensor и конкретными
TemperatureSensor и PressureSensor.
Абстракция для датчика скорости ветра может выглядеть
следующим образом:
Имя:
WindSpeedSensor
Ответственность:
Поддержание информации о текущей скорости ветра.
Операции:
currentSpeed - текущая скорость
setLowSpeed - установка минимальной скорости
setHighSpeed - установка максимальной скорости
Атрибуты:
speed - скорость
Требования к системе не предполагают возможности получения скорости непосредственно от датчика; текущая скорость ветра должна определяться как отношение числа оборотов на счетчике к величине интервала времени, за которое производились измерения. Полученное число затем надо умножить на калибровочный коэффициент, значение которого определяется конструкцией измерительного устройства. Этот алгоритм должен быть, естественно, реализован внутри класса. Клиенты не должны заботиться о том, каким образом посчитана текущая скорость ветра.
Краткий анализ последних четырех классов системы (TemperatureSensor, PressureSensor, HumiditySensor и WindSpeedSensor) показывает, что у них имеется еще одна общая черта: калибровка измеренных значений посредством линейной интерполяции. Вместо того, чтобы реализовывать эту способность по отдельности в каждом из классов, можно выделить особый суперкласс CalibratingSensor, ответственный за выполнение калибровки.
Ответственность:
Обеспечение линейной интерполяции значений, лежащих
в известном интервале.
Операции:
currentValue - текущее значение
setLowValue - установка минимального значения
setHighValue - установка максимального значения
CalibratingSensor является непосредственным суперклассом
для класса HistoricalSensor.
Последний рассматриваемый датчик - датчик определения
направления ветра - несколько отличается от всех остальных, так как он
не нуждается ни в калибровке, ни в вычислении минимальных и максимальных
значений. Мы можем определить данную абстракцию следующим образом:
Имя:
WindDirectionSensor
Ответственность:
Поддержание информации о текущем направлении
ветра, указываемом как точка на розе ветров.
Операции:
currentDirection - текущее направление
Атрибуты:
direction - направление
Чтобы объединить все классы, относящиеся к датчикам, в одну иерархию, имеет смысл создать еще один абстрактный базовый класс Sensor, который является непосредственным суперклассом для WindDirectionSensor и CalibratingSensor. Рис. 8-4 иллюстрирует полную иерархию классов датчиков.
Рис. 8-4. Иерархия классов датчиков.
Абстракция ввода информации с клавиатуры имеет следующий простой вид:
Имя:
Keypad
Ответственность:
Поддержание информации о коде последней клавиши,
нажатой на клавиатуре.
Операции:
lastKeyPress - последняя нажатая клавиша
Атрибуты:
key - клавиша
Заметим, что этот класс не знает о назначении той или иной
клавиши: экземпляр данного класса несет информацию лишь о том, какая клавиша
была нажата. Ответственность за интерпретацию клавиш несет другой класс,
который будет определен позднее.
Следующая абстракция - класс LCDDevice, предназначенный
для того, чтобы обеспечить определенную независимость нашей программной
системы от аппаратной части, на которой она будет работать. Для рабочих
станций и персональных компьютеров существует целый ряд стандартов (хотя
зачастую и конфликтующих между собой) графического интерфейса, таких, например,
как Motif или Microsoft Windows. К сожалению, для встроенных контроллеров
нет общепризнанных стандартов, поэтому анализ задачи приводит нас к мысли
о том, что для ее решения необходимо создать прототипы и затем определить
основные требования к интерфейсу пользователя.
Рис. 8-5. Дисплей метеорологической станции.
На рис. 8-5 приведен один из подобных прототипов. Здесь не показаны изображения, характеризующие коэффициент резкости погоды и точку росы, требуемые в задании, а также такие детали, как верхние и нижние границы измерений за 24 часа. Однако, все основные графические элементы присутствуют. Итак, нам необходимо выводить на экран текст (двух различных размеров и начертаний), окружности и линии различной толщины. Также следует заметить, что некоторые элементы изображения являются статическими (такие, как заголовок ТЕМПЕРАТУРА), а некоторые - динамическими (направление ветра). И статические, и динамические элементы изображения генерируются программно. В итоге упрощается аппаратная часть (не надо заказывать специальные жидкокристаллические дисплеи со встроенными статическими элементами), но несколько усложняется программное обеспечение.
Требования к графике можно выразить через следующую абстракцию:
Имя:
LCDDevice
Ответственность:
Управление выводом на экран графических элементов.
Операции:
drawText - рисовать текст
drawLine - рисовать линию
drawCircle - рисовать окружность
setTextSize - установка размера текста
setTextStyle - установка начертания текста
setPenSize - установка ширины линии
Аналогично классу Keypad, класс LCDDevice не понимает, зачем он выводит тот или иной элемент на экран. Это дает возможность свободно оперировать нашими абстракциями, однако требует наличия некоего внешнего агента, выполняющего функции посредника между датчиками и дисплеем. Мы отложим рассмотрение соответствующей абстракции до того, как изучим некоторые сценарии работы системы.
Последним классом, на который следует обратить внимание, является таймер. Сделаем упрощающее предположение о том, что таймер будет один на всю систему, и что системные прерывания будут осуществляться с периодичностью 60 раз в секунду. Лучше если детали реализации подобного таймера будут скрыты от остальных абстракций. Для этого можно организовать еще один класс, использующий функцию обратного вызова (техника callback объяснена в разделе 2.2) и экспортирующий только статические элементы класса (тем самым мы наложим ограничение на систему, запрещающее создание более чем одного таймера).
Рис. 8-6. Диаграмма взаимодействия с таймером.
На рис. 8-6 приведена диаграмма взаимодействий, иллюстрирующая применение данной абстракции. На ней видно, как клиент взаимодействует с таймером:
сначала клиент передает таймеру функцию обратного вызова, а затем с периодичностью в 0.1 секунды таймер вызывает эту функцию. Тем самым мы освобождаем клиента от заботы о том, как осуществляются прерывания, а таймер - от необходимости знать, что при этом прерывании делать. Единственным требованием к клиенту должно быть ограничение на продолжительность выполнения функции обратного вызова - оно не должно превышать 0.1 секунды, в противном случае таймер может пропустить событие.
Класс Timer, осуществляющий прерывания, является активной абстракцией, он инициирует цепочку управляющих команд. Его можно формализовать с помощью следующего описания:
Имя:
Timer
Ответственность:
Осуществление прерываний и диспетчеризация
функций обратного вызова.
Операции:
setCallback() - установка функции обратного вызова
Сценарии
Определив в рамках нашей системы основные абстракции,
продолжим анализ задачи и рассмотрим некоторые сценарии работы системы.
Начнем с составления списка ситуаций. С точки зрения пользователя список
будет выглядеть примерно следующим образом:
-
Мониторинг первичных измеряемых параметров: скорости и направления
ветра, температуры, барометрического давления и влажности.
-
Мониторинг производных параметров: коэффициента жесткости
погоды, точки образования росы, трендов температуры и барометрического
давления.
-
Показ максимальных и минимальных значений выбранных параметров.
-
Установка времени и даты.
-
Калибровка выбранных датчиков.
-
Включение системы.
Добавим еще две дополнительные ситуации:
-
Отказ питания.
-
Отказ датчика.
Исследуем вышеприведенные сценарии для того, чтобы понять
поведение (именно поведение, а не внутреннюю структуру) системы.
Главной задачей системы является мониторинг основных измеряемых
параметров. Одним из ограничений является невозможность обрабатывать информацию
с частотой, превышающей 60 измерений в секунду. К счастью, наиболее интересные
для нас погодные параметры меняются с гораздо меньшей скоростью. Дополнительный
анализ показывает, что для своевременной регистрации изменений различных
погодных параметров достаточно обеспечить следующие частоты снятия информации:
-
каждые 0.1 секунды направление ветра
-
каждые 0.5 секунды скорость ветра
-
каждые 5 минут температура, барометрическое давление и влажность
Ранее мы приняли решение о том, что классы датчиков не должны
отвечать за организацию периодических измерений. Эта работа лежит в сфере
ответственности внешнего агента, взаимодействующего с датчиками. Отложим
пока описание поведения данного агента (оно определяется в большей степени
особенностями реализации системы и будет рассмотрено на этапе проектирования).
Диаграмма взаимодействий, приведенная на рис. 8-7, иллюстрирует в некоторой
степени сценарий его работы. Мы видим, что когда агент начинает обработку
измерений, он последовательно опрашивает датчики, однако при этом может
пропускать те из них, для которых интервал опроса больше 0.1 секунды. Такая
схема, в отличие от той, где каждый датчик самостоятельно отвечает за измерение,
обеспечивает более предсказуемое поведение системы, потому что контроль
за процессом считывания параметров сосредоточен в одном месте, а именно,
в экземпляре класса-агента. Назовем этот класс Sampler.
Рис. 8-7. Сценарий измерений.
Продолжим рассмотрение данного сценария. Теперь нам предстоит
решить, какие из объектов, приведенных на диаграмме, должны отвечать за
вывод информации на экран дисплея, то есть, фактически, за передачу данных
экземпляру класса LCDDevice. Здесь возможны два варианта: можно
передать ответственность за эти действия самим классам датчиков (подобная
схема реализована в архитектурах, подобных MVC), либо создать отдельный
класс для связи между датчиками и дисплеем. В данном случае мы выбираем
второй вариант, так как он позволяет нам изолировать в рамках одного класса
все проектные решения, касающиеся механизмов реализации вывода параметров
на экран. В итоге к результатам нашего анализа добавляется описание еще
одного класса:
Имя:
DisplayManager
Ответственность:
Организация отображения параметров на экране
дисплея.
Операции:
drawStaticItems - рисование статических элементов
displayTime - вывод времени
displayDate - вывод даты
displayTemperature - вывод температуры
displayHumidity - вывод влажности
displayPressure - вывод давления
displayWindChill - вывод коэффициента жесткости погоды
displayDewPoint - вывод точки росы
displayWindSpeed - вывод скорости ветра
displayWindDirection - вывод направления ветра
displayHighLow - вывод максимальных и минимальных значений
Операция drawStaticItems рисует на экране ту часть изображения,
которая не изменяется в процессе работы системы, например, розу ветров
для индикации направления ветра. Мы также предполагаем, что операции displayTemperature
и displayPressure ответственны за вывод на экран трендов соответствующих
параметров (следовательно, когда мы перейдем к реализации проекта, надо
будет выработать подходящие сигнатуры этих операций).
На рис. 8-8 приведена диаграмма классов, иллюстрирующая
связи между абстракциями, ответственными за вывод информации на экран,
и роль каждой из них в обеспечении заданного сценария.
Отметим еще одно важное преимущество нашего решения о выделении отдельного класса DisplayManager. Задача локализации системы для различных стран предполагает изменение языка, на котором информация выводится на дисплей. Наличие отдельного класса, ответственного за вывод сообщений на экран, существенно облегчает процесс локализации, так как имена всех сообщений (например, ТЕМПЕРАТУРА, или скорость) находятся, в этом случае, в ведении единственного класса; они не разбросаны по множеству различных абстракций.
Рис. 8-8. Классы, ответственные за вывод данных.
Рассмотрение задачи локализации ставит перед разработчиком ряд дополнительных вопросов, не выраженных явным образом в требованиях к системе. Как следует показывать температуру, по Цельсию или по Фаренгейту? В чем отображать скорость ветра, в километрах в час или в милях в час? Ясно, что наше программное обеспечение не должно нас жестко ограничивать. Для обеспечения гибкости в использовании системы конечным пользователем необходимо добавить к описаниям классов TemperatureSensor и WindSpeedSensor еще одну операцию, setMode, устанавливающую нужную систему измерений. Также следует добавить в описание этих классов новую обязанность, предусматривающую возможность установки вновь создаваемых объектов в известное состояние. И, наконец, мы должны изменить описание операции DisplayManager::drawStaticItems таким образом, чтобы при изменении единиц измерений соответствующим образом менялась панель дисплея.
В результате нам придется добавить к списку режимов работы системы еще один сценарий:
-
Установка единиц измерения температуры и скорости ветра.
Мы отложим рассмотрение данного режима до того, как изучим другие сценарии. Мониторинг вторичных параметров, в частности трендов температуры и давления, можно обеспечить на основе протоколов уже приведенных ранее классов TemperatureSensor и PressureSensor. Однако, чтобы полностью определить сценарий мониторинга, придется добавить еще два класса (назовем их WindChill и DewPoint), предназначенных для определения коэффициента жесткости погоды и точки образования росы. Эти абстракции не отождествляются с датчиками и вообще с чем-либо осязаемым. Их задача - вычисление значений параметров. Они выступают в роли агентов, сотрудничающих с другими классами. Именно класс WindChill использует для вычислений информацию, содержащуюся в TemperatureSensor и WindSpeedSensor, а класс DewPoint сотрудничает с классами TemperatureSensor и HimiditySensor. Классы Windchill и DewPoint сотрудничают и с классом Sampler, так как они используют аналогичный механизм опроса датчиков. Рис. 8-9 иллюстрирует набор классов и связи между ними, необходимые для реализации рассмотренного сценария. Он почти не отличается от диаграммы классов, приведенной ранее на рис. 8-8.
Рис. 8-9. Вторичные параметры.
Почему мы решили определить WindChill и DewPoint в качестве классов, вместо того, чтобы реализовать вычисления соответствующих параметров с помощью отдельных функций? Потому что каждый из них удовлетворяет условиям, позволяющим выделить их в отдельные абстракции. Экземпляры этих классов обладают характерным поведением (вычисление определенных величин по определенному алгоритму), имеют в каждый момент времени определенное состояние (зависящее от состояния связанных с ними датчиков) и уникальны (любая ассоциация между экземплярами датчиков скорости ветра и температуры требует собственного экземпляра WindChill). "Объективация" этих алгоритмических абстракций повышает вероятность их повторного использования в архитектурах систем: классы WindChill и DewPoint легко можно будет перенести из нашего приложения в другие программные системы, потому что каждый из них обладает понятным внешним интерфейсом и четко выделяется как отдельная абстракция.
Далее рассмотрим различные сценарии взаимодействия пользователя и системы. Предоставление пользователю оптимальной последовательности действий для выполнения его задач является так же, как и проектирование графического интерфейса, в большой степени искусством. Изучение этого вопроса выходит за рамки данной книги, но основную мысль можно вкратце выразить следующим образом: используйте прототипирование, оно существенно уменьшает риск при разработке интерфейса пользователя. Кроме того, если архитектура системы является объектно-ориентированной, то снижаются затраты, связанные с изменением организации интерфейса пользователя.
Рассмотрим некоторые из возможных сценариев взаимодействия пользователя с системой:
Вывод на экран максимальных и минимальных значений выбранного параметра.
1. Пользователь нажимает клавишу SELECT.
2. Система выводит на экран сообщение SELECTING.
3. Пользователь нажимает одну из следующих клавиш: WIND
SPEED, TEMPERATURE, PRESSURE или HUMIDITY; нажатие всех остальных клавиш
(кроме клавиши RUN) игнорируется.
4. Название выбранного параметра начинает мигать на экране.
5. Пользователь нажимает одну из клавиш UP или DOWN,
выбирая тем самым, какое значение - максимальное или минимальное - будет
выведено на экран; нажатие всех остальных клавиш (кроме клавиши Run) игнорируется.
6. Система выводит на экран выбранное значение, а также
время его замера.
7. Переход управления к пункту 3 или 5.
Замечание: для прекращения работы в данном режиме пользователь
нажимает клавишу RUN, при этом экран дисплея возвращается в первоначальное
состояние.
После рассмотрения этого сценария мы приходим к выводу
о необходимости расширить описание класса DisplayManager, добавив
к нему операции flashLabel (переключает вывод названия параметра в режим
мигания и обратно, в зависимости от аргумента) и displayMode (выводит на
дисплей текстовое сообщение).
Установка времени и даты подчиняется аналогичному сценарию:
Установка времени и даты.
1. Пользователь нажимает клавишу SELECT.
2. Система выводит на экран сообщение SELECTING.
3. Пользователь нажимает одну из следующих клавиш: TIME
или DATE; нажатия всех остальных клавиш (кроме клавиши
RUN и клавиш, перечисленных в пункте 3 предыдущего сценария) игнорируются.
4. Название выбранного параметра, а также первое поле
его значения (для времени - это час, для даты - месяц) начинают мигать
на экране.
5. Пользователь нажимает одну из клавиш LEFT или RIGHT
для перехода на другое поле; пользователь нажимает одну из клавиш UР или
DOWN для увеличения или уменьшения значения выделенной величины.
6. Переход управления к пункту 3 или 5.
Замечание: для прекращения работы в данном режиме пользователь
нажимает клавишу RON, при этом экран дисплея возвращается в первоначальное
состояние, и происходит переустановка времени и даты.
Сценарий калибровки датчика следует той же схеме:
Калибровка датчика.
1. Пользователь нажимает клавишу CALIBRATE.
2. Система выводит на экран сообщение CALIBRATING.
3. Пользователь нажимает одну из следующих клавиш: WIND
SPEED, TEMPERATURE, PRESSURE или HUMIDITY; нажатия всех остальных клавиш
(кроме клавиши RUM) игнорируются.
4. Название выбранного параметра начинает мигать на экране.
5. Пользователь нажимает одну из клавиш Up или DOWN,
задавая тем самым, какое калибровочное значение, максимальное или минимальное,
будет переопределяться.
6. Соответствующее калибровочное значение начинает мигать
на экране.
7. Пользователь нажимает клавиши ПР или DOWN для изменения
значения выделенной величины.
8. Переход управления к пункту 3 или 5.
Рис. 8-10. Клавиатура метеорологической станции.
Замечание: для прекращения работы в данном режиме пользователь нажимает клавишу RUN, при этом экран дисплея возвращается в первоначальное состояние, и происходит перерасчет калибровочной функции.
На время калибровки все экземпляры класса Sampler должны прекратить считывание параметров, в противном случае будут показаны ошибочные данные. Таким образом, мы должны добавить в описание класса sampler еще две операции:
inhibitSample и resumeSample, приостанавливающие и возобновляющие
процесс.
Последний сценарий касается установки единиц измерений:
Установка единиц измерений температуры и скорости ветра.
1. Пользователь нажимает клавишу MODE.
2. Система выводит на экран сообщение MODE .
3. Пользователь нажимает одну из клавиш WIND SPEED или
TEMPERATURE; нажатия всех остальных клавиш (кроме
клавиши RUN) игнорируются.
4. Название выбранного параметра начинает мигать на экране.
5. Пользователь нажимает одну из клавиш UР или DOWN,
изменяя при этом единицу измерения параметра.
6. Система изменяет единицу измерения выбранного параметра.
7. Переход управления к пункту 3 или 5.
Замечание: для прекращения работы в данном режиме пользователь нажимает клавишу RUN, при этом экран дисплея возвращается в первоначальное состояние, и происходит переустановка единиц измерений параметров.
После изучения сценариев работы можно определить состав и расположение клавиш на клавиатуре (системное решение). На рис. 8-10 представлен один из вариантов такого решения.
Приведенные выше сценарии можно наглядно отобразить с помощью диаграмм состояний. Так как все сценарии тесно связаны, разумно будет выделить отдельный класс InputManager, определяемый следующим образом:
Имя:
InputManager
Ответственность:
Диспетчеризация команд пользователя.
Операции:
processKeyPress обработка сигналов с клавиатуры
Единственная операция processKeyPress приводит в действие конечный автомат, "живущий" в экземпляре данного класса.
Как видно из рис. 8-11, на котором представлена диаграмма состояний класса InputManager, есть четыре состояния: Running, Calibrating, Selecting, и Mode (работа, калибровка, выбор и режим). Эти состояния соответствуют вышеприведенным сценариям. Переход в новое состояние определяется первой клавишей, нажатой в состоянии Running. Мы возвращаемся в состояние Running после нажатия клавиши Run, при этом происходит очистка дисплея.
Рис. 8-11. Диаграмма состояний для InputManager.
Мы более детально расписали поведение системы в состоянии Mode (правая часть диаграммы), чтобы показать, как можно формализовать динамику сценария. При переходе в это состояние на экране появляется соответствующее сообщение. Затем система входит в состояние waiting (ожидание) до тех пор, пока пользователь не нажмет одну из клавиш Temperature или WindSpeed, которые переводят систему во вложенное состояние Processing. Если пользователь нажимает клавишу Run, система возвращается в основное эксплуатационное состояние. Каждый раз при переходе в состояние Processing соответствующий параметр начинает мигать. При последующих входах мы сразу попадаем в то подсостояние (Temp или wind), из которого вышли в прошлый раз.
Находясь в состояниях Temp или wind, система может реагировать на нажатие
пяти клавиш: up или Down (переход между режимами), Temp или wind (переход
к другому вложенному состоянию) и Run (выход из состояния Mode).
Состояния selecting и calibrating можно расписать подобным же образом.
Мы не приводим их здесь, потому что они мало добавляют к пониманию метода
[Естественно, при создании реального продукта детальный анализ
должен завершиться составлением диаграммы переходов. Мы можем опустить
здесь эту часть работы, потому что она достаточно скучна и не добавляет
ничего нового к нашим знаниям о системе].
Последний основной сценарий относится к включению системы. От нас при
этом требуется обеспечить создание всех ее объектов в нужной последовательности
и приведение их в стабильное начальное состояние:
Включение системы.
1. Включение питания.
2. Создание датчиков; датчики с историей очищают "исторические"
данные; датчики с трендом инициализируют алгоритм вычисления тренда.
3. Инициализация буфера клавиатуры, очистка его от случайной
информации, вызванной помехами при включении питания.
4. Прорисовка статических элементов экрана.
5. Инициализация процесса опроса датчиков.
Постусловия:
-
Последние минимаксные значения параметров устанавливаются
равными их первому замеру.
-
Время достижения микимакса считается равным времени первого
замера.
-
Тренды температуры и давления равны нулю.
-
InputManager находится в состоянии Running.
Отметим, что задание постусловий определяет ожидаемое состояние
системы после завершения сценария. Как мы увидим, выполнение этого сценария
обеспечивается совместной работой целой группы объектов, каждый из которых
самостоятельно приводит себя в стабильное начальное состояние.
На этом мы завершим изучение основных сценариев работы
метеорологической станции. Конечно, для полноты картины было бы полезно
пройтись и по некоторым дополнительным сценариям. Однако мы считаем, что
основные функциональные свойства системы уже в достаточной степени освещены,
и что теперь пора перейти к проектированию ее архитектуры и оправдать наши
стратегические решения.
8.2. Проектирование
Архитектурный каркас
Каждая программная система должна иметь простую и в то
же время всеобъемлющую организационную философию. Система мониторинга погоды
не является в этом смысле исключением. На следующем этапе нашей работы
мы должны четко определить архитектуру проекта. Это даст нам стабильный
фундамент, на основе которого мы будем строить отдельные функциональные
части системы.
Существует целый ряд архитектурных моделей для решения
задач сбора и обработки данных и управления, но наиболее часто встречаются
синхронизация автономных исполнителей и схема покадровой обработки.
В первом случае архитектура системы скомпонована из ряда относительно независимых объектов, каждый из которых выполняется как поток управления. Можно было бы, например, создать несколько новых объектов-датчиков, построенных с помощью более примитивных абстракций, каждый из которых отвечал бы за считывание информации с определенного датчика и за передачу ее центральному агенту, обрабатывающему всю информацию. Подобная архитектура имеет свои преимущества и является, пожалуй, единственной приемлемой моделью в случае проектирования распределенной системы, которая должна производить обработку большого числа параметров, поступающих с удаленных датчиков. Эта модель также позволяет эффективнее оптимизировать процесс сбора данных (каждый объект-датчик может содержать в себе информацию о том, как надо приспосабливаться к изменению окружающих условий - увеличивать или уменьшать частоту опроса, например).
Однако подобные архитектуры оказываются не всегда приемлемыми при создании жестких систем реального времени, где требуется обеспечить предсказуемость процесса обработки. Метеорологическую станцию нельзя отнести к таким системам, но для нее, тем не менее, требуется определенная степень предсказуемости и надежности. По этой причине мы выбираем для нашей системы модель покадровой обработки.
Рис. 8-12. Покадровая обработка.
Как показано на рис. 8-12, процесс мониторинга осуществляется в данном случае как последовательность считывания, обработки и вывода на экран значений параметров через определенные промежутки времени. Каждый элемент такой последовательности называется кадром, его, в свою очередь, можно разбить на ряд подкадров, соответствующих определенному функциональному поведению. Различные кадры могут нести информацию о различных параметрах. Направление ветра, например, необходимо измерять через каждые 10 кадров, а скорость ветра - через 30 кадров [Например, если кадры считываются через каждую 1/60 секунды, то 30 кадров занимают 0.5 секунды]. Основное преимущество такой модели состоит в том, что мы можем более жестко контролировать последовательность действий системы по сбору и обработке информации.
На рис. 8-13 приведена диаграмма классов, отражающая особенности
архитектуры системы. Здесь присутствуют, в основном, те же самые классы,
которые были определены на этапе анализа. Главное отличие от предыдущих
диаграмм состоит в том, что теперь мы видим, каким образом ключевые абстракции
нашего программного приложения взаимодействуют друг с другом. Мы, естественно,
не можем отразить на одной диаграмме все существующие
классы и связи между ними. Здесь, например, не воспроизведена иерархия
классов-датчиков.
Рис. 8-13. Архитектура системы мониторинга погоды.
Кроме того, мы ввели один новый класс Sensors, который служит для объединения в коллекцию всех объектов-датчиков. Поскольку по крайней мере два агента (Sampler и InputManager) в нашей системе должны ассоциироваться с целой коллекцией датчиков, помещение их в один контейнерный класс позволяет рассматривать все датчики единым образом.
Механизм покадровой обработки
Поведение нашей системы в основном определяется взаимодействием классов Sampler и Timer, поэтому, чтобы оправдать нашу модель, следует быть особенно внимательным при их описании.
Начнем с разработки внешнего интерфейса для класса Timer, осуществляющего диспетчеризацию функции обратного вызова (все решения будут в дальнейшем реализовываться на языке C++). Во-первых, с помощью ключевого слова typedef определим новый тип переменной, Tick, соответствующий словарю нашей проблемной области.
// Временной промежуток, измеряемый в 1/60 долях секунды
typedef unsigned int Tick
Затем определим класс Timer:
class Timer {
public:
static setCallback(void (*)(Tick));
static startTiming();
static Tick numberOfTicks();
private:
...
};
Это - необычный класс хотя бы потому, что он содержит не совсем обычную информацию. Функция-член setCallback используется для передачи таймеру функции обратного вызова. Таймер запускается вызовом функции startTiming, после чего единственный экземпляр класса Timer начинает вызывать функцию обратного вызова каждую 1/60 секунды. Отметим, что функция запуска введена в явном виде, поскольку нельзя полагаться на то, как в частной реализации определяется порядок обработки объявлений.
Прежде чем перейти к классу Sampler, желательно ввести перечислимый тип всех датчиков, присутствующих в нашей системе,
следующим образом:
// Перечисление названий датчиков
enum SensorName {Direction, Speed, WindChill,
Temperature, DewPoint, Humidity, Pressure};
Теперь можно определить интерфейс класса Sampler:
class Sampler {
public:
Sampler();
~Sampler();
void setSamplingRate(SensorName, Tick);
void sample(Tick);
Tick samplingRate() const;
protected:
...
};
Для того, чтобы клиент мог динамически изменять поведение сэмплера, мы определили модификатор setSamplingRate и селектор samplingRate.
Чтобы обеспечить связь между классами Timer и Sampler, придется еще приложить небольшие усилия. В следующем фрагменте кода создается объект класса Sampler и определяется "неклассовая" функция acquire:
Sampler sampler;
void acquire(Tick t)
{
sampler.sample(t);
}
После этого можно написать функцию main, где просто происходит
присоединение к таймеру функции обратного вызова и запускается процесс
опроса датчиков:
main() {
Timer::setCallback(acquire);
Timer::startTiming();
while(1);
return 0;
}
Это довольно типичная для объектно-ориентированной системы главная функция: она короткая (потому что основная работа делегирована объектам) и включает в себя цикл диспетчеризации (в нашем случае пустой, так как отсутствуют какие-либо фоновые процессы).
Продолжим рассмотрение нашей задачи. Определим теперь
внешний интерфейс класса Sensors (датчики). Мы предполагаем, что
существуют различные конкретные классы датчиков:
class Sensors : protected Collection {
public:
Sensors();
virtual ~Sensors();
void addSensor(const Sensor& SensorName, unsigned
int id = 0);
unsigned int numberOfSensors() const;
unsigned int numberOfSensors(SensorName);
Sensor& sensor(SensorName, unsigned int id
= 0);
protected:
};
Это, в основном, класс-коллекция и поэтому он объявляется подклассом фундаментального класса Collection. Класс Collection указан как защищенный суперкласс; это сделано для того, чтобы скрыть детали его строения от клиентов класса Sensor. Обратите внимание на то, что набор операций, который мы определили для класса Sensors, крайне скуден - это вызвано ограниченностью задач класса. Мы, например, знаем, что датчики могут добавляться в коллекцию, но не удаляться из нее.
Таким образом, мы изобрели класс-коллекцию для датчиков, который может содержать множество экземпляров датчиков одного и того же типа, причем каждый экземпляр своего класса имеет уникальный идентификационный номер, начиная с нуля.
Вернемся к спецификации класса Sampler. Нам надо
обеспечить его ассоциацию с классами Sensors и DisplayManager:
class Sampler {
public:
Sampler(Sensors&, DisplayManager&)
;
protected:
Sensors& repSensors;
DisplayManager& repDisplayManager;
};
Теперь следует изменить фрагмент кода, где происходит
создание экземпляра класса Sampler:
Sensors sensors;
DisplayManager display;
Sampler sampler(sensors, display);
При порождении объекта Sampler устанавливается связь между ним, коллекцией датчиков sensors, и экземпляром класса DisplayManager, который будет использоваться системой.
Теперь можно заняться описанием ключевой операции класса
Sampler, а именно, sample:
void Sampler::sample(Tick t)
{
for (SensorName name = Direction; name
<= Pressure; name++)
for (unsigned int id = 0; id
< repSensors.numberOfSensors(name); id++)
if (!(t % samplingRate(name)))
repDisplayManager.display(repSensors.sensor(name,
id).currentValue(), name, id);
}
Рис. 8-14. Механизм покадровой обработки.
Эта функция по очереди опрашивает каждый тип датчика и каждый датчик внутри типа. Она проверяет, пришло ли время считывать информацию с датчика, и если да, то определяет ссылку на датчик в коллекции, считывает его текущее значение и передает его менеджеру дисплея, ассоциированному с данным экземпляром класса Sampler.
Семантика этой операции основывается на полиморфном поведении определенного метода, а именно:
virtual float currentValue();
определенного для базового класса sensor. Эта операция, кроме того, основывается на функции display класса DisplayManager:
void display(float, SensorName, unsigned int id = 0);
Сейчас, после того как мы уточнили этот элемент нашей архитектуры, можно составить новую диаграмму классов, отражающую механизм покадровой обработки (рис. 8-14).
8.3. Эволюция
Планирование релизов
Рассмотрев несколько сценариев работы системы и убедившись в правильности стратегических решений, можно начинать планирование процесса разработки. Разобьем работу на ряд этапов, результат каждого из которых будет являться основой для последующего:
-
Разработка программы, обладающей минимальными функциональными свойствами и осуществляющей мониторинг только одного датчика.
-
Создание иерархии датчиков.
-
Создание классов, ответственных за управление изображением
на экране.
-
Создание классов, ответственных за работу пользовательского
интерфейса.
В принципе, можно было бы изменить порядок этапов, но мы выбрали именно такую последовательность, исходя из того, что наиболее сложная и рискованная часть работы должна выполняться в первую очередь. Разработка минимальной версии программы заставляет нас в первую очередь смоделировать архитектуру "по вертикали", реализовав в усеченном варианте практически все ключевые абстракции. Эта задача несет в себе основной риск, ведь в процессе ее решения фактически проверяется правильность выбора ключевых абстракций, их роль и функции. Успешное создание раннего прототипа играет очень большую роль в построении системы. Как уже отмечалось в главе 7, это дает нам ряд технических (и не только) преимуществ. В частности, мы сразу выявим несоответствия между аппаратной и программной частями. Кроме того, будущие пользователи получат возможность уже на ранних этапах проекта оценить внешний вид и работу системы.
Мы не будем подробно останавливаться на реализации данной версии, поскольку это в большей степени тактическая задача, а перейдем сразу к дальнейшим релизам. При этом мы откроем для себя некоторые интересные особенности процесса разработки.
Механизм датчиков
Мы уже видели, как при разработке архитектуры системы
постепенно наполнялись содержанием и приобретали устойчивые формы ее ключевые
абстракции, в том числе классы датчиков. Руководствуясь эволюционным подходом
к разработке, будем строить следующую версию на основе первой, минимальной.
На данном этапе разработки иерархия классов-датчиков, представленная на рис. 8-4, остается без изменений. Мы, однако, должны уточнить местонахождение некоторых полиморфных операций, чтобы добиться как можно более высокой степени общности классов в иерархии. Ранее, например, мы описали требования к операции currentValue, принадлежащей абстрактному базовому классу Sensor. Более полно конструкцию данного класса можно определить на C++ следующим образом:
class Sensor {
public:
Sensor(SensorName, unsigned int id = 0);
virtual ~Sensor();
virtual float currentValue = 0;
virtual float rawValue() = 0;
SensorName name() const;
unsigned int id() const;
protected:
...
};
Этот класс включает в себя чисто виртуальные функции-члены, и поэтому является абстрактным.
Отметим, что конструктор класса сообщает экземпляру его имя и номер. Это сделано для обеспечения возможности динамического определения типа датчика, а также для того, чтобы удовлетворить одно из требований к системе, согласно которому каждый из датчиков имеет постоянный адрес доступа в оперативной памяти. Эти детали реализации системы можно скрыть, вычисляя адрес в памяти через тип датчика и его идентификационный номер.
После того, как мы добавили новые свойства к классу датчиков, можно вернуться немного назад и упростить объявление функции DisplayManager::display, которая теперь может иметь только один аргумент, а именно ссылку на объект класса Sensor. От остальных аргументов можно отказаться, так как объект класса, производного от sensor, сам выдаст информацию о своем типе и идентификационном номере.
Это казалось бы незначительное изменение крайне желательно, так как если не стремиться к упрощению внешнего интерфейса классов, то со временем наша система будет все больше и больше страдать от перегруженности протоколов взаимодействия между ними.
Объявление подкласса CalibratingSensor основывается
на базовом классе Sensor:
class CalibratingSensor : public Sensor {
public:
CalibratingSensor(SensorName, unsigned
int id = 0);
virtual ~CalibratingSensor();
void setHighValue(float, float);
void setLowValue(float, float);
virtual float currentValue();
virtual float rawValue() = 0;
protected:
...
};
Этот класс включает в себя две новые операции (setHighValue
и setbowValue), и реализует виртуальную функцию currentValue базового класса.
Теперь рассмотрим объявление подкласса HistoricalSensor,
базирующегося на классе CalibratingSensor:
class HistoricalSensor : public CalibratingSensor
{
public:
HistoricalSensor(SensorName, unsigned
int id = 0);
virtual ~HistoricalSensor();
float highValue() const;
float lowValue() const;
const char* timeOfHighValue() const;
const char* timeOfLowValue() const;
protected:
...
};
В этом классе определены четыре новые операции, реализация которых требует взаимодействия с классом TimeDate. Отметим также, что HistoricalSensor все еще является абстрактным классом, так как мы не определили в нем реализацию чисто виртуальной функции rawValue, которая будет определена в следующем подклассе.
Класс TrendSensor является производным от HistoricalSensor;
в нем добавлено одно новое свойство:
class TrendSensor : public HistoricalSensor {
public:
TrendSensor(SensorName, unsigned int id
= 0);
virtual ~TrendSensor();
float trend() const;
protected:
...
};
В этом классе определена одна новая функция trend. Как и некоторые другие операции, добавляемые в промежуточные классы, она не обозначена как виртуальная, так как мы не хотим, чтобы наследующие классы ее переопределяли.
И вот, наконец, мы переходим к конкретному классу TemperatureSensor:
class TemperatureSensor : public TrendSensor {
public:
TemperatureSensor(unsigned int id = 0);
virtual ~TemperatureSensor();
virtual float rawValue();
float currentTenperature();
protected:
...
};
Отметим, что сигнатура конструктора для этого класса определена по-новому. Здесь нам известен конкретный тип датчика, поэтому нет необходимости задавать его имя при создании объекта. Обратим также внимание на новую операцию currentTemperature. Ее присутствие логически вполне оправдано, однако, если мы вернемся к результатам нашего анализа, то обнаружим, что аналогичную операцию выполняет полиморфная функция currentValue. Тем не менее, мы включили в описание и ту, и другую функции, так как операция currentTemperature более безопасна с точки зрения типов.
После того, как мы успешно завершили реализацию всех классов данной иерархии и интегрировали их с предыдущим релизом, можно переходить к следующему уровню функциональности системы.
Механизм вывода информации на экран
Подготовка следующего релиза, где должны быть окончательно определены классы DisplayManager и LCDDevice, не требует от нас новых проектных решений. Осталось лишь несколько тактических шагов, связанных с сигнатурой и семантикой некоторых функций-членов. Соединяя решения, принятые в процессе анализа, и наш первый архитектурный прототип, где мы сделали некоторые важные предположения о протоколе отображения значений, можно определить на C++ следующий интерфейс:
class DisplayManager {
public:
DisplayManager();
~DisplayManager();
void clear();
void refresh();
void display(Sensor&);
void drawStaticItems(TemperatureScale, SpeedScale);
void displayTime(const char*);
void displayDate(const char*);
void displayTemperature(float, unsigned int id
= 0);
void displayHumidity(float, unsigned int id =
0);
void displayPressure(float, unsigned int id =
0);
void displayWindChill(float, unsigned int id =
0);
void displayDewPoint(float, unsigned int id =
0);
void displayWindSpeed(float, unsigned int id =
0);
void displayWindDirection(unsigned int, unsigned
int id = 0);
void displayHighLow(float, const char*, SensorName,
unsigned int id = 0);
void setTemperatureScale(TemperatureScale);
void setSpeedScale(SpeedScale);
protected:
// ...
};
Ни одна из приведенных операций не является виртуальной, так как создание иерархии классов вывода информации на экран не планируется, и у DisplayManager не будет потомков.
Отметим, что этот класс содержит несколько достаточно примитивных операций (таких, как DisplayTime и refresh), но в то же время обладает составной операцией display, присутствие которой во многом упрощает взаимодействие клиентов с экземпляром класса DisplayManager.
DisplayManager в конечном итоге использует ресурсы
класса LCDDevice, который, как мы уже определили, служит программной
оболочкой аппаратуры. DisplayManager поднимает абстракцию до уровня
понятий предметной области.
Механизм пользовательского интерфейса
Последним основным элементом нашей системы является механизм пользовательского интерфейса, который должен быть реализован с помощью классов Keypad и InputManager. Подобно LCDDevice, класс Keypad служит связующим звеном с аппаратной частью, освобождающим InputManager от необходимости каждый раз приспосабливаться к новому "железу". Разделение этих двух абстракций во многом облегчает процесс адаптации системы к другим аппаратным устройствам ввода информации и повышает степень устойчивости ее архитектуры.
Начнем с определения словаря проблемной области:
enum Key {kRun, kSelect, kCalibrate, kMode, kUp, kDown, kLeft, kRight, kTemperature, kPressure, kHumidity, kWind, kTime, kDate, kUnassigned};
Нам приходится использовать префикс k, чтобы не дублировать
наименований типов, уже определенных для SensorName.
Далее, определим класс Keypad следующим образом:
class Keypad {
public:
Keypad();
~Keypad();
int inputPending() const;
Key lastKeyPress() const;
protected:
...
};
Протокол для данного класса уже был в основном определен в процессе анализа. Мы добавили лишь операцию inputPending; это сделано для того, чтобы клиент мог узнать, есть ли новая, еще не обработанная команда пользователя.
Класс InputManager имеет во многом аналогичный
интерфейс:
class InputManager {
public:
InputManager(Keypad&);
~InputManager();
void processKeyPress();
protected:
Keypad& repKeypad;
};
Как мы увидим, поведение этого класса почти исчерпывающе описывается конечным автоматом.
Рис. 8-13 иллюстрирует взаимодействие классов Sampler, InputManager и Keypad по обработке пользовательских команд. Чтобы интегрировать их, надо несколько видоизменить интерфейс класса Sampler, включив в его описание новый объект repInputManager:
class Sampler {
public:
Sampler(Sensor&, DisplayManager&,
inputManager&);
...
protected:
Sensors& repSensors;
DisplayManager& repDisplayManager;
InputManager& replnputManager;
};
Теперь связь между экземплярами классов Sensors, DisplayManager и InputManager устанавливается в момент создания объекта класса Sampler. Использование ссылок гарантирует, что каждый экземпляр Sampler получит соответствующий набор датчиков, менеджера экрана и менеджера ввода. Другая схема, в которой вместо ссылок используются указатели, обеспечила бы довольно слабую связь, позволяя создавать объект Sampler, у которого отсутствовали бы некоторые важные компоненты.
Ключевую функцию Sampler::sample надо модифицировать
следующим образом:
void Sampler::sample(Tick t)
{
repInputManager.processKeyPress();
for (SensorName name = Direction; name <= Pressure;
name++)
for (unsigned int id = 0; id
< repSensors.numberOfSensors(name); id++)
if (!(t % samplingRate(name)))
repDisplayManager.display(repSensors.sensor (name,
id));
}
В начало каждого кадра мы добавили вызов метода processKeyPress. Операция processKeyPress является точкой входа в конечный автомат, управляющий работой экземпляров класса InputManager. Существуют два подхода к реализации любого конечного автомата: можно представить состояния системы объектами и положиться на их полиморфное поведение или просто ввести перечисление состояний, обозначив их литералами.
Для конечных автоматов с относительно небольшим числом состояний, к числу которых принадлежит и класс InputManager, достаточно использовать второй подход. Сначала определим имена объемлющих состояний класса:
enum InputState {Running, Selecting, Calibrating,
Mode);
Затем определим некоторые защищенные функции класса:
class InputManager {
public:
...
protected:
Keypads repKeypad;
InputState repState;
void enterSelecting();
void enterCalibrating();
void enterMode();
};
И, наконец, начнем реализовывать переходы между состояниями
(см. рис. 8-11):
void InputManager::process Keypress() {
if (repKeypad.inputPending()) {
Key key = repKeypad.lastKeyPress();
switch (repState) {
case Running:
if (key == kSelect)
enterSelecting();
else if (key == kCalibrate)
enterCalibrating();
else if (key == kMode)
enterMode();
break;
case Selecting: break;
case Mode: break;
}
}
}
Таким образом, реализация данной функции отражает содержание
диаграммы переходов межу состояниями на рис. 8-11.
8.4. Сопровождение
Полная реализация рассматриваемой системы является не слишком объемной - всего около 20 классов. Тем не менее, для любого работающего фрагмента кода этап последующей модернизации неизбежен. Рассмотрим, что придется сделать, чтобы реализовать еще два дополнительных требования к нашей системе.
Видно, что система позволяет измерять многие погодные параметры, однако не все. Может оказаться, что пользователи захотят измерять также количество осадков. Какие изменения при этом необходимо будет внести в программу?
К счастью, нам не придется радикально менять нашу архитектуру, надо будет лишь дополнить ее. Используя в качестве основы архитектурный макет, представленный на рис. 8-13, можно выделить следующие необходимые изменения:
-
Создание нового класса-датчика RainFallSensor (датчика
осадков); выявление его оптимального положения в иерархии датчиков (RainFallSensor
есть разновидность HistoricalSensor).
-
Обновление перечисления SensorName.
-
Модификация класса DisplayManager, обеспечивающая
вывод на экран параметров, снимаемых с датчика нового типа.
-
Модификация класса InputManager, обеспечивающая обработку
нажатия новой клавиши RainFall.
-
Правильное включение экземпляров класса RainFallSensor
в коллекцию Sensors.
Нам может встретиться еще ряд более мелких задач по интеграции нового класса в уже существующую архитектуру, но в любом случае ни сама архитектура, ни основные механизмы системы не претерпят серьезных изменений.
Рассмотрим теперь совершенно другое функциональное свойство: предположим, что мы хотим обеспечить возможность пересылки собранных за день данных на удаленный компьютер. Для реализации этой задачи необходимо:
-
Создание нового класса SerialPort, ответственного
за управление последовательным портом RS232.
-
Разработка нового класса ReportManager, ответственного
за подготовку информации к записи в определенном формате. Этот класс в
основном использует ресурсы класса-коллекции Sensors и ассоциированных
с ним конкретных датчиков.
-
Изменение реализации функции Sampler::sample, дополнительно
обеспечивающее периодическое обслуживание последовательного порта.
Признак хорошо продуманной объектно-ориентированной архитектуры
- изменения не разрушают ее, а расширяют, сохраняя существующие механизмы.
Дополнительная литература
Проблемы синхронизации процессов, тупиков, конфликтов
и т. п. подробно обсуждаются в работах Хансена (Hansen) [H 1977], Бен-Ари
(Ben-Ari) [H 1982] и Холта и др. (Holt et al.) [H 1978]. Мелличамп (Mellichamp)
[H 1983], Гласе (Glass) [H 1983] и Фостер (Foster) [H 1981] являются традиционными
ссылками по вопросам разработки приложении реального времени. Параллельность
с точки зрения взаимодействия аппаратуры и программы обсуждает Лорин (Lorin)
[H 1972].
|