датчиков 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);
}
Эта функция по очереди опрашивает каждый тип датчика и каждый датчик внутри типа. Она проверяет, пришло ли время считывать информацию с датчика, и если да, то определяет ссылку на датчик в коллекции, считывает его текущее значение и передает его менеджеру дисплея, ассоциированному с данным экземпляром класса Sampler.
Семантика этой операции основывается на полиморфном поведении определенного метода, а именно:
virtual float currentValue();
определенного для базового класса sensor. Эта операция, кроме того, основывается на функции display класса DisplayManager:
void display(float, SensorName, unsigned int id = 0);
Сейчас, после того как мы уточнили этот элемент нашей архитектуры, можно составить новую диаграмму классов, отражающую механизм покадровой обработки (рис. 8-14).
8.3. Эволюция
Планирование релизов
Рассмотрев несколько сценариев работы системы и убедившись в правильности стратегических решений, можно начинать планирование процесса разработки. Разобьем работу на ряд этапов, результат каждого из которых будет являться основой для последующего:
• Разработка программы, обладающей минимальными функциональными свойствами и осуществляющей мониторинг только одного датчика.
• Создание иерархии датчиков.
• Создание классов, ответственных за управление изображением на экране.
• Создание классов, ответственных за работу пользовательского интерфейса.
В принципе, можно было бы изменить порядок этапов, но мы выбрали именно такую последовательность, исходя из того, что наиболее сложная и рискованная часть работы должна выполняться в первую очередь. Разработка минимальной версии программы заставляет нас в первую очередь смоделировать архитектуру 'по вертикали', реализовав в усеченном варианте практически все ключевые абстракции. Эта задача несет в себе основной риск, ведь в процессе ее решения фактически проверяется правильность выбора ключевых абстракций, их роль и функции. Успешное создание раннего прототипа играет очень большую роль в построении системы. Как уже отмечалось в главе 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: ... };
Этот класс включает в себя чисто виртуальные функции-члены, и поэтому является абстрактным.
Отметим, что конструктор класса сообщает экземпляру его имя и номер. Это сделано для обеспечения возможности динамического определения типа датчика, а также для того, чтобы удовлетворить одно из требований к системе, согласно которому каждый из датчиков имеет постоянный адрес доступа в оперативной памяти. Эти детали реализации системы можно скрыть, вычисляя адрес в памяти через тип датчика и его идентификационный номер.
После того, как мы добавили новые свойства к классу датчиков, можно вернуться немного назад и упростить объявление функции 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: ... };