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

We use cookies. Read the Privacy and Cookie Policy

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).