Kitabı oku: «Обратные вызовы в C++», sayfa 12
6.2.8. Интерфейсный класс
Класс, объявляющий интерфейс для взаимодействия с приложением, представлен в Листинг 97.
Листинг 97. Интерфейсный класс (ControlInterface.h)
namespace sensor
{
class ISensorControl
{
public:
virtual ~ ISensorControl () = default;
virtual void initialize() = 0; // (1)
virtual void shutDown() = 0; // (2)
virtual void assignDriver(DriverPointer driver) = 0; // (3)
virtual DriverPointer getAssignedDriver() = 0; // (4)
virtual DriverPointer getSensorDriver(SensorNumber number) = 0; // (5)
virtual void addSensor(SensorType type, SensorNumber number) = 0; // (6)
virtual void deleteSensor(SensorNumber number) = 0; // (7)
virtual bool isSensorExist(SensorNumber number) = 0; // (8)
virtual bool isSensorOperable(SensorNumber number) = 0; // (9)
virtual SensorValue getSensorValue(SensorNumber number) = 0; // (10)
virtual void querySensorValue(SensorNumber number, SensorValueCallback callback) = 0; // (11)
virtual void readSensorValues(SensorValueCallback callback) = 0; // (12)
virtual SensorValue getMinValue(SensorNumber first, SensorNumber last) = 0; // (13)
virtual SensorValue getMaxValue(SensorNumber first, SensorNumber last) = 0; // (14)
virtual void setAlert(SensorNumber number, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс = 1) = 0; // (15)
virtual void resetAlert(SensorNumber number) = 0; // (16)
static ISensorControl* createControl(); // (17)
};
};
В строке 1 и 2 объявлены методы для запуска и останова. В строках 3 и 4 объявлены методы для назначения и получения драйвера. Этот драйвер должен быть создан и назначен в самом начале работы, поскольку он будет передаваться новым создаваемым датчикам. Узнать назначенный драйвер для соответствующего датчика можно в методе 5.
В строках 6 и 7 объявлены методы для добавления и удаления датчика. В методе 8 можно проверить, существует ли датчик с переданным номером, в методе 9 можно проверить, является ли датчик работоспособным.
В строке 10 объявлен метод для чтения текущего показания датчика. В методе 11 осуществляется асинхронный запрос показания датчика, значение будет возвращаться через передаваемый обратный вызов. В строке 12 осуществляется опрос показаний всех работоспособных датчиков, значения также возвращаются через обратный вызов. С помощью методов, объявленных в строках 13 и 14, можно получить минимальное и максимальное значение для набора датчиков с номерами из указанного диапазона.
В строке 15 назначается отслеживание пороговых значений, в строке 16 отслеживание выключается. С помощью метода, объявленного в строке 17, можно создать экземпляр соответствующего интерфейсного класса.
Класс, реализующий интерфейс, приведен в Листинг 98.
Листинг 98 Класс, реализующий интерфейс (SensorControl.h)
namespace sensor
{
class ISensor;
class IDriver;
class CommandQueue;
class AlertControl;
class SensorContainer;
class SensorControl: public ISensorControl
{
public:
SensorControl();
~SensorControl();
void initialize() override;
/* Other Interface methods – they are not displayed here*/
private:
SensorContainer* sensorContainer_; // (1)
CommandQueue* commandQueue_; // (2)
AlertControl* alertControl_; // (3)
bool isInitialized_; // (4)
DriverPointer driver_; // (5)
void checkInitialize(); // (6)
void checkDriver(); // (7)
};
}; //namespace sensor
В строке 1 объявлен контейнер для хранения датчиков, в строке 2 – класс для выполнения асинхронных запросов, в строке 3 – класс для отслеживания пороговых значений. Соответствующие указатели создаются в конструкторе и уничтожаются в деструкторе. Индикатор 4 указывает, была ли выполнена инициализация.
В строке 6 объявлен вспомогательный метод, который проверяет, была ли выполнена инициализация (если нет, выбрасывает исключение). В строке 7 аналогичный метод проверяет, был ли установлен драйвер.
Рассмотрим, как здесь используются обратные вызовы. Для начала самый простой случай – чтение показаний работоспособных датчиков (Листинг 99).
Листинг 99. Обратные вызовы в классе, реализующем интерфейс (SensorControl.cpp)
void SensorControl::readSensorValues(SensorValueCallback callback)
{
checkInitialize(); // (1)
sensorContainer_->forEachSensor([callback](SensorNumber number, SensorPointer sensor) // (2)
{
if (sensor->isOperable()) // (3)
{
callback(number, sensor->getValue()); // (4)
}
}
);
}
В строке 1 производится проверка, инициализирован ли класс. Если класс не проинициализирован, то функция выбросит исключение.
В строке 2 происходит перебор элементов контейнера, в качестве обратного вызова используется лямбда-выражение. Контейнер будет вызывать лямбда-выражение, в которое он будет передавать номер датчика и указатель на экземпляр класса. В теле выражения проверяется, является ли датчик работоспособным (строка 3), и если да, то выполняется соответствующий обратный вызов (строка 4).
Рассмотрим теперь поиск максимального и минимального значения для заданного диапазона номеров датчиков. Вначале разработаем вспомогательный класс, который будет последовательно принимать на вход показания датчиков и искать среди них максимальное и минимальное значение (Листинг 100).
Листинг 100. Класс для анализа минимального и максимального значения (SensorControl.cpp)
class FindMinMaxValue
{
public:
enum MinMaxSign { MIN_VALUE = 0, MAX_VALUE = 1 }; // (1)
FindMinMaxValue(SensorNumber first, SensorNumber last, MinMaxSign sign) : // (2)
sign_(sign), first_(first), last_(last), count_(0)
{
if (sign == MIN_VALUE)
{
result_ = std::numeric_limits<SensorValue>::max(); // (3)
}
else
{
result_ = std::numeric_limits<SensorValue>::min(); // (4)
}
arrayFunMinMax_[MIN_VALUE] = &FindMinMaxValue::CompareMin; // (5)
arrayFunMinMax_[MAX_VALUE] = &FindMinMaxValue::CompareMax; // (6)
}
void operator()(SensorNumber number, SensorPointer sensor) // (7)
{
if ( sensor->isOperable() && (number >= first_ && number <= last_) ) // (8)
{
(this->*arrayFunMinMax_[sign_])(sensor->getValue()); // (9)
count_++; // (10)
}
}
SensorValue result() { return result_; } // (11)
size_t count() { return count_; } // (12)
private:
SensorNumber first; // (13)
SensorNumber last; // (14)
MinMaxSign sign; // (15)
SensorValue result; // (16)
size_t count; // (17)
using FunMinMax = void (FindMinMaxValue::*)(SensorValue value); // (18)
void CompareMin(SensorValue value) // (19)
{
if (result_ > value)
{
result_ = value;
}
}
void CompareMax(SensorValue value) // (20)
{
if (result_ < value)
{
result_ = value;
}
}
FunMinMax arrayFunMinMax_[2]; // (21)
};
В строке 2 объявлен конструктор, который принимает на вход следующие параметры: минимальное значение диапазона номеров; максимальное значение диапазона номеров; параметр, указывающий, что необходим поиск минимального либо максимального значения. В конструкторе инициализируются переменные класса: минимальное значение диапазона (объявлено в строке 13); максимальное значение диапазона (объявлено в 14); параметр для поиска (объявлено в 15); итоговый результат (объявлено в 16); количество датчиков, которые участвовали в поиске (объявлено в 17). В зависимости от переданного параметра начальный результат инициализируется соответственно максимальным либо минимальным значением (строки 3 и 4). Кроме того, инициализируется массив указателей на функцию (строки 5 и 6, объявление в 21). Данные функции предназначены для сравнения и запоминания максимального либо минимального значений (объявлены в 19 и 20).
Анализ очередного значения происходит в перегруженном операторе 7. На вход подаются номер датчика и указатель на датчик. Если датчик работоспособный и его номер попадает в заданный диапазон номеров (строка 8), то в зависимости от параметра поиска через указатель вызывается соответствующая функция для анализа (строка 9), а также увеличивается счетчик просмотренных датчиков (строка 10). Функции 11 и 12 возвращают итоговые результаты.
Итак, класс для анализа готов. Теперь можно вызвать метод для итерации по элементам контейнера, и в качестве обратного вызова передать экземпляр соответствующего вспомогательного класса. Метод будет вызывать перегруженный оператор, и таким образом, мы узнаем минимальное либо максимальное значение (Листинг 101).
Листинг 101. Поиск минимального и максимального значений (SensorControl.cpp)
SensorValue SensorControl::getMinValue(SensorNumber first, SensorNumber last)
{
checkInitialize();
FindMinMaxValue fmv(first, last, FindMinMaxValue::MIN_VALUE);
sensorContainer_->forEachSensor(fmv);
return fmv.result();
}
SensorValue SensorControl::getMaxValue(SensorNumber first, SensorNumber last)
{
checkInitialize();
FindMinMaxValue fmv(first, last, FindMinMaxValue::MAX_VALUE);
sensorContainer_->forEachSensor(fmv);
return fmv.result();
}
6.3. Разработка системного API
6.3.1. API как оболочка
Уже после того, как классы модуля были разработаны, протестированы и начали использоваться в системе, появилось новое требование – ввести поддержку системного API. Как известно, в интерфейсах системных API можно использовать только внешние функции и простые структуры данных в стиле C; классы и другие специфические конструкции C++ использовать нельзя (см. п. 1.4.2). Так что же, все теперь придется переписывать? Можно предложить следующее решение: использовать интерфейс API как оболочку для вызова методов класса. Концептуальный пример приведен в Листинг 102.
Листинг 102. Концептуальный пример реализации API как оболочки
using ControlPointer = std::unique_ptr<sensor::ISensorControl>;
ControlPointer g_SensorControl(sensor::ISensorControl::createControl());
void initialize () // This function is declared in the header file as part of API interface
{
g_SensorControl->initialize();
}
Однако не все так просто, перед нами встают следующие проблемы.
1. В исходной реализации мы использовали специфические типы C++, такие, как std::function, smart pointers и т. п., что не допускается в интерфейсах системных API. Какие типы использовать взамен?
2. Для обработки ошибок в исходной реализации мы использовали исключения. Как сейчас обрабатывать ошибки, ведь в интерфейсах API исключения недопустимы?
3. В исходной реализации мы в каждом потоке могли объявить отдельный интерфейсный класс и работать с ним независимо от остальных потоков. Как теперь обеспечить многопоточную работу, ведь отдельные потоки вызывают одни и те же интерфейсные функции?
4. В исходной реализации драйвер настраивался путем создания нового класса и передаче его в интерфейсный класс. Как теперь настраивать драйвер, если в интерфейсах API нельзя использовать классы?
5. Как организовать обратные вызовы?
Рассмотрим, как эти проблемы можно решить.
6.3.2. Объявления типов
В исходной реализации общие типы объявлены в SensorDef.h, но мы не можем просто перенести их в интерфейс API из-за использования специфических конструкций С++. Поэтому нам придется повторить эти объявления в стиле C с использованием простых типов, которые можно будет использовать в интерфейсных функциях. Объявления представлены в Листинг 103.
Листинг 103. Объявления типов для интерфейса API (SensorLib.h)
#ifdef _WINDOWS // (1)
#ifdef LIB_EXPORTS
#define LIB_API __declspec(dllexport)
#else
#define LIB_API __declspec(dllimport)
#endif
#else
#define LIB_API
#endif
typedef uint32_t SensorNumber; // (2)
typedef double SensorValue; // (3)
typedef uint32_t CheckAlertTimeout; // (4)
typedef uint32_t SensorType; // (5)
typedef uint32_t DriverType; // (6)
typedef uint32_t AlertRule; // (7)
typedef void(*SensorValueCallback)(SensorNumber, SensorValue, void*); // (8)
typedef CheckAlertTimeout(*SensorAlertCallback)(SensorNumber, SensorValue, void*); // (9)
typedef SensorValue(*OnSimulateReadValue)(SensorNumber, int, void*); // (10)
typedef int (*OnSimulateOperable)(SensorNumber, void*); // (11)
enum eSensorType // (12)
{
SENSOR_SPOT = 0,
SENSOR_SMOOTH = 1,
SENSOR_DERIVATIVE = 2,
};
enum eDriverType // (13)
{
DRIVER_SIMULATION = 0,
DRIVER_USB = 1,
DRIVER_ETHERNET = 2
};
enum eAlertRule // (14)
{
ALERT_MORE = 0,
ALERT_LESS = 1
};
В строке 1 объявлены определения для экспортируемых функций. Эти объявления необходимы для компиляции динамической библиотеки в среде Windows, для других платформ они неактуальны.
В строках 2–4 объявлены типы, которые будут использоваться для входных параметров интерфейсных функций. Это те же объявления, которые использовались в исходной реализации (SensorDef.h, см. п. 6.2.2).
В строках 5–7 вместо перечислений C++ объявляются простые числовые типы. В экспортируемых функциях нежелательно использовать перечисления как типы входных параметров, потому что размер этих типов в C явно не определен. Вместо этого перечисления используются в качестве числовые констант, они объявлены соответственно в строках 12–14.
В строках 8–11 объявлены типы указателей на функцию для выполнения обратных вызовов. Как видим, в отличие от исходной реализации здесь присутствует дополнительный параметр для указания контекста вызова.
6.3.3. Интерфейс API и обработка ошибок
Исходя из концепции «API как оболочка», сигнатура интерфейсных функций API должна повторять сигнатуру методов интерфейсного класса. Однако здесь мы сталкиваемся с некоторыми проблемами, одна из которых – это обработка ошибок.
В исходной реализации мы обрабатывали ошибки с помощью исключений. Теперь исключения использовать нельзя, в системных API они недопустимы. Тем не менее, вызываемая функция должна как-то уведомить о возникновении ошибки, для чего могут использоваться следующие способы:
1) функция возвращает результат, для которого некоторое предопределенное значение говорит о том, что произошла ошибка. Код ошибки возвращается с помощью отдельного вызова;
2) код ошибки возвращается через дополнительный параметр функции;
3) все функции возвращают результат выполнения, который является кодом ошибки.
Ни один способов не является идеальным, каждый имеет свои достоинства и недостатки. Так, в первом способе возникают сложности, если результат, возвращаемый функцией, не имеет значений, которые недопустимы и могут сигнализировать о возникновении ошибки36. Во втором способе для всех вызовов придется использовать дополнительную переменную – код ошибки, даже если он нас не интересует. В третьем способе, если функция возвращает результат, то для него приходится использовать отдельный входной параметр, что не всегда удобно.
В нашем случае мы выберем третий способ, исходя из следующих соображений: объявления функций будут выглядеть единообразно; возникновение ошибки можно узнать непосредственно в момент вызова, (например, в операторе if); если функция не возвращает значений, то ей не нужно передавать никакие дополнительные параметры. Объявления интерфейсных функций с возвратом ошибок представлены в Листинг 104.
Листинг 104. Интерфейс системного API (SensorLib.h)
typedef unsigned int ErrorCode;
LIB_API ErrorCode initialize();
LIB_API ErrorCode shutDown();
LIB_API ErrorCode assignDriver(DriverType type);
LIB_API ErrorCode getAssignedDriver(DriverType* type);
LIB_API ErrorCode getSensorDriver(SensorNumber number, DriverType* type);
LIB_API ErrorCode addSensor(SensorType type, SensorNumber number);
LIB_API ErrorCode deleteSensor(SensorNumber number);
LIB_API ErrorCode isSensorExist(SensorNumber number, int* isExist);
LIB_API ErrorCode isSensorOperable(SensorNumber number, int* isOperable);
LIB_API ErrorCode getSensorValue(SensorNumber number, SensorValue* value);
LIB_API ErrorCode querySensorValue(SensorNumber number, SensorValueCallback callback, void* pContextData);
LIB_API ErrorCode readSensorValues(SensorValueCallback callback, void* pContextData);
LIB_API ErrorCode getMinValue(SensorNumber first, SensorNumber last, SensorValue* value);
LIB_API ErrorCode getMaxValue(SensorNumber first, SensorNumber last, SensorValue* value);
LIB_API ErrorCode setAlert(SensorNumber number, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс, void* pContextData);
LIB_API ErrorCode resetAlert(SensorNumber number);
LIB_API ErrorCode setSimulateReadCallback(OnSimulateReadValue callback, void* pContextData);
LIB_API ErrorCode setSimulateOperableCallback(OnSimulateOperable callback, void* pContextData);
В реализации этих функций мы будем возвращать код ошибки, получая его из перехваченного исключения. В качестве примера рассмотрим реализацию функции для получения значения датчика (Листинг 105).
Листинг 105. Функция для получения значения датчика
ErrorCode getSensorValue(SensorNumber number, SensorValue* value)
{
ErrorCode error = ERROR_NO; // (1)
try
{
*value = g_SensorControl->getSensorValue(number); // (2)
}
catch (sensor::sensor_exception& e) // (3)
{
error = e.code(); // (4)
}
return error; // (5)
}
В строке 1 объявляем переменную – код возврата. В строке 2 осуществляем вызов метода класса, который заключен в блок try. В строке 3 осуществляется перехват исключения, в строке 4 присваивается код ошибки, который возвращается в строке 5.
Итак, мы придумали, как в интерфейсных функциях осуществлять обработку ошибок. Теперь перед нами встает следующая проблема: как настраивать типы драйверов, ведь в исходной реализации для этого используются классы? Прежде чем перейти к решению этой задачи, остановимся на реализации многопоточной работы, поскольку используемые там конструкции нам понадобятся в дальнейшем.
6.3.4. Многопоточная работа
В исходной реализации в каждом потоке мы могли создать свой экземпляр класса ISensorControl и работать с ним независимо. В случае API это не работает, потому что экземпляр класса в реализации интерфейса объявляется глобальным, и все интерфейсные функции обращаются к одному и тому же экземпляру класса. Выходом здесь будет выделение отдельной области памяти для экземпляра класса в рамках одного потока, т. е. использование локальной памяти потока.
До появления стандарта C++ 11 использовать локальную память потока было непросто: для этого требовалось явное обращение к функциям операционной системы, что усложняло реализацию и делало код платформенно-зависимым. В C++ 11 появилось ключевое слово thread_local, и это сильно упростило жизнь: если в объявлении переменной добавить указанный спецификатор, то она становится локальной в рамках потока, т. е. каждый новый создаваемый поток будет иметь независимый экземпляр соответствующей переменной. Таким образом, достаточно экземпляр интерфейсного класса ISensorControl объявить как thread_local, и теперь для каждого потока будет существовать отдельный независимый экземпляр класса (Листинг 106).
Листинг 106. Объявление экземпляра класса как локального для текущего выполняемого потока (SensorLib.cpp)
using ControlPointer = std::unique_ptr<sensor::ISensorControl>;
thread_local ControlPointer g_SensorControl(sensor::ISensorControl::createControl());
6.3.5. Настройка драйвера
В исходной реализации в начале работы мы создавали необходимый класс драйвера, который затем передавали интерфейсному классу (Листинг 107). Но в интерфейсах системных API мы классы использовать не можем, как поступить в этом случае? Можно предложить следующее решение: класс драйвера создавать внутри API, а в функцию настройки передавать идентификатор, в соответствии с которым будет создан соответствующий драйвер (Листинг 108).
Листинг 107. Настройка драйвера в исходной реализации
ISensorControl sensorControl = ISensorControl::createControl;
DriverPointer driver = IDriver::createDriver(DRIVER_SIMULATION);
driver->initialize();
sensorControl->assignDriver(driver);
Листинг 108. Настройка драйвера в системном API (SensorLib.h)
thread_local sensor::DriverPointer g_DriverSimulation; // (1)
thread_local sensor::DriverPointer g_DriverUSB; // (2)
thread_local sensor::DriverPointer g_DriverEthernet; // (3)
void CreateDriver(sensor::DriverType driverType, sensor::DriverPointer& driverPointer) // (4)
{
if (!driverPointer)
{
driverPointer = sensor::IDriver::createDriver(driverType);
driverPointer->initialize();
}
g_SensorControl->assignDriver(driverPointer);
}
ErrorCode assignDriver(DriverType driverType) // (5)
{
ErrorCode error = ERROR_NO;
try
{
EnumConverter<sensor::DriverType> conv;
conv.convert (driverType,
{sensor::DriverType::Simulation, sensor::DriverType::Usb, sensor::DriverType::Ethernet}); // (6)
if (conv.error())
{
return ERROR_INVALID_ARGUMENT;
}
switch (conv.result()) // (7)
{
case sensor::DriverType::Simulation:
{
CreateDriver(sensor::DriverType::Simulation, g_DriverSimulation);
}
break;
case sensor::DriverType::Usb:
{
CreateDriver(sensor::DriverType::Usb, g_DriverUSB);
}
break;
case sensor::DriverType::Ethernet:
{
CreateDriver(sensor::DriverType::Ethernet, g_DriverEthernet);
}
break;
}
}
catch (sensor::sensor_exception& e)
{
error = static_cast<ErrorCode>(e.code());
}
return error;
}
В строках 1–3 объявляются указатели для хранения классов всех возможных типов драйверов. В строке 4 объявлена вспомогательная функция для создания драйвера. Эта функция проверяет, создан ли драйвер соответствующего типа, при необходимости создает, инициализирует и передает его в интерфейсный класс.
В строке 5 приведена реализация интерфейсной функции для настройки драйвера. В строке 6 конвертируется переданное числовое значение в перечисление C++ (будет рассмотрено ниже). В строке 7 объявлен оператор switch, в котором анализируется полученное значение перечисления, и вызывается вспомогательная функция с соответствующими параметрами.
В функции API для задания типа драйвера используются числовые значения, а в интерфейсном классе используются перечисления C++. Для того, чтобы сконвертировать числовое значение в перечисление, используется вспомогательный класс EnumConverter (Листинг 109)
Листинг 109. Конвертер числовых значений в перечисление (EnumConverter.h)
template <typename Enum> // (1)
class EnumConverter
{
public:
template<typename ConvValueType> // (2)
void convert(ConvValueType value, std::initializer_list<Enum> list) // (3)
{
isError_ = true;
for (Enum item : list) // (4)
{
if (static_cast<ConvValueType>(item) == value) // (5)
{
result_ = item; // (6)
isError_ = false;
break;
}
}
};
bool error() const { return isError_; }
Enum result() const { return result_; }
private:
bool isError_;
Enum result_;
};
В строке 1 объявлен шаблонный класс, параметром которого является тип перечисления. Конвертация происходит в функции 2, которая объявлена в виде шаблона, параметром шаблона является тип числового значения для конвертации. Функция принимает число, которое должно быть сконвертировано, а также список значений перечисления (строка 3). Реализация пробегает по всем элементам списка (строка 4) и, если какой-то из элементов списка перечисления равен переданному значению, запоминает это значение перечисления в качестве результата (строки 5,6).