8.1. Анализ

  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, предназначенный для того, чтобы обеспечить определенную независимость нашей программной системы от аппаратной части, на которой она будет работать. Для рабочих станций и персональных компьютеров существует целый ряд стандартов (хотя зачастую и конфликтующих между собой) графического интерфейса, таких, например, нет общепризнанных стандартов, поэтому анализ задачи приводит нас к мысли о том, что для ее решения необходимо создать прототипы и затем определить основные требования к интерфейсу пользователя.  

Рис. 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.

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

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