Объектно-ориентированное проектирование с примерами

       

Механизм пользовательского интерфейса


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






      Содержание раздела