Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования / Книги / ob''ektno-orientirovannyj_analiz_i_proektirovanie

.pdf
Скачиваний:
43
Добавлен:
11.05.2015
Размер:
9.27 Mб
Скачать

Объектно-ориентированный анализ и проектирование

271

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

знаниям о системе].

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

Включение системы.

272

Объектно-ориентированный анализ и проектирование

1.Включение питания.

2.Создание датчиков; датчики с историей очищают "исторические" данные; датчики с трендом инициализируют алгоритм вычисления тренда.

3.Инициализация буфера клавиатуры, очистка его от случайной информации, вызванной помехами при включении питания.

4.Прорисовка статических элементов экрана.

5.Инициализация процесса опроса датчиков.

Постусловия:

Последние минимаксные значения параметров устанавливаются равными их первому замеру.

Время достижения микимакса считается равным времени первого замера.

Тренды температуры и давления равны нулю.

InputManager находится в состоянии Running.

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

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

8.2. Проектирование

Архитектурный каркас

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

Существует целый ряд архитектурных моделей для решения задач сбора и обработки данных и управления, но наиболее часто встречаются синхронизация автономных исполнителей и схема покадровой обработки.

В первом случае архитектура системы скомпонована из ряда относительно независимых объектов, каждый из которых выполняется как поток управления. Можно было бы, например, создать несколько новых объектов-датчиков, построенных с помощью более примитивных абстракций, каждый из которых отвечал бы за считывание информации с определенного датчика и за передачу ее центральному агенту, обрабатывающему всю информацию. Подобная архитектура имеет свои преимущества и является, пожалуй, единственной приемлемой моделью в случае проектирования распределенной системы, которая должна производить обработку большого числа параметров, поступающих с удаленных датчиков. Эта модель также позволяет эффективнее оптимизировать процесс сбора данных (каждый объект-датчик может содержать в себе информацию о том, как

Объектно-ориентированный анализ и проектирование

273

надо приспосабливаться к изменению окружающих условий - увеличивать или уменьшать частоту опроса, например).

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

Рис. 8-12. Покадровая обработка.

Как показано на рис. 8-12, процесс мониторинга осуществляется в данном случае как последовательность считывания, обработки и вывода на экран значений параметров через определенные промежутки времени. Каждый элемент такой последовательности называется кадром, его, в свою очередь, можно разбить на ряд подкадров, соответствующих определенному функциональному поведению. Различные кадры могут нести информацию о различных параметрах. Направление ветра, например, необходимо измерять через каждые 10 кадров, а скорость ветра - через 30 кадров [Например, если кадры считываются через каждую 1/60 секунды, то 30 кадров занимают 0.5 секунды]. Основное преимущество такой модели состоит в том, что мы можем более жестко контролировать последовательность действий системы по сбору и обработке информации.

На рис. 8-13 приведена диаграмма классов, отражающая особенности архитектуры системы. Здесь присутствуют, в основном, те же самые классы, которые были определены на этапе анализа. Главное отличие от предыдущих диаграмм состоит в том, что теперь мы видим, каким образом ключевые абстракции нашего программного приложения взаимодействуют друг с другом. Мы, естественно, не можем отразить на одной диаграмме все существующие классы и связи между ними. Здесь, например, не воспроизведена иерархия классов-датчиков.

274

Объектно-ориентированный анализ и проектирование

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

...

};

Объектно-ориентированный анализ и проектирование

275

Это - необычный класс хотя бы потому, что он содержит не совсем обычную информацию. Функция-член 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;

}

276

Объектно-ориентированный анализ и проектирование

Это довольно типичная для объектно-ориентированной системы главная функция: она короткая (потому что основная работа делегирована объектам) и включает в себя цикл диспетчеризации (в нашем случае пустой, так как отсутствуют какие-либо фоновые процессы).

Продолжим рассмотрение нашей задачи. Определим теперь внешний интерфейс класса 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, который будет использоваться системой.

Объектно-ориентированный анализ и проектирование

277

Теперь можно заняться описанием ключевой операции класса 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. Эволюция

Планирование релизов

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

278

Объектно-ориентированный анализ и проектирование

работу на ряд этапов, результат каждого из которых будет являться основой для последующего:

Разработка программы, обладающей минимальными функциональными свойствами и осуществляющей мониторинг только одного датчика.

Создание иерархии датчиков.

Создание классов, ответственных за управление изображением на экране.

Создание классов, ответственных за работу пользовательского интерфейса.

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

Разработка минимальной версии программы заставляет нас в первую очередь смоделировать архитектуру "по вертикали", реализовав в усеченном варианте практически все ключевые абстракции. Эта задача несет в себе основной риск, ведь в процессе ее решения фактически проверяется правильность выбора ключевых абстракций, их роль и функции. Успешное создание раннего прототипа играет очень большую роль в построении системы. Как уже отмечалось в главе 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:

...

};

Объектно-ориентированный анализ и проектирование

279

Этот класс включает в себя чисто виртуальные функции-члены, и поэтому является абстрактным.

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

После того, как мы добавили новые свойства к классу датчиков, можно вернуться немного назад и упростить объявление функции 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:

...

};

280

Объектно-ориентированный анализ и проектирование

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

Соседние файлы в папке Книги