Buch lesen: «Обратные вызовы в C++», Seite 12

Schriftart:

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

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