Механизм покадровой обработки
Поведение нашей системы в основном определяется взаимодействием классов 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. Эволюция