Сообщений 49    Оценка 0 [+0/-2]         Оценить  
Система Orphus

Автоматическая генерация интерфейсов классов

Автор: Чигринец Владислав Александрович
Опубликовано: 20.02.2012
Исправлено: 10.12.2016
Версия текста: 1.1
Кодирование в стиле Си с классами
Использование библиотеки автоматической генерации интерфейсов
Реализация библиотеки шаблонов сообщений
Заключение
Список литературы

Время от времени разработчики сталкиваются с ситуацией, когда классы содержат большое число полей. Объектно-ориентированный подход накладывает требование инкапсуляции полей класса, что приводит к разрастанию интерфейсов доступа к ним. Ситуация усугубляется, когда подобных классов становится много. В данной статье показан способ автоматической генерации интерфейсов доступа к полям класса, рассмотрен вопрос локализации в одном файле типов полей класса.

Кодирование в стиле Си с классами

В качестве примера взята одна из реализаций служебных сообщений стандарта IEEE 802.16 Broadband Wireless MANs (WiMAX) [1]. Служебные сообщения стандарта IEEE 802.16 представляют собой пакет данных (PDU – protocol data unit), состоящий из одного байта заголовка сообщения и нескольких полей – параметров сообщения. На сегодняшний момент стандарт IEEE 802.16 описывает 70 служебных сообщений, ряд из которых содержат десятки параметров. Обязательные параметры располагаются в начале сообщения, их размер и порядок следования фиксированы, строго прописаны в стандарте и не меняются ни при каких обстоятельствах. За обязательными параметрами следуют параметры, которые могут присутствовать либо только при определенных условиях, либо появиться при новых релизах стандарта (обеспечение гибкости стандарта). Каждый «необязательный» параметр упаковывается дополнительно в кортеж «тип, длина, значение» (TLV — type, length, value), что позволяет однозначно идентифицировать его в пределах сообщения.

Для конкретности рассмотрим сообщение «Описатель нисходящего канала» «DCD (DL channel descriptor) message». Полное описание DCD-сообщения содержит 50 параметров, но для описания принципа работы нам достаточно и четырех из них: двух обязательных параметров и двух необязательных TLV-параметров, таблица 1.

Таблица 1 – DCD-сообщение [1]

Название параметра

Размер (байт)

Описание

Management Message Type = 1

1

Заголовок сообщения, размер 1 байт, значение всегда «1»

Count

1

Счетчик изменений DCD

Далее следуют необязательные параметры, которые упаковываются в TLV кортежи:

...

BS EIRP

тип= 2, длина=2

Знаковое, требуемый уровень принимаемого сигнала, дБм. В TLV кортеже его тип всегда «2»

Frequency

тип= 12, длина=4

Центральная рабочая частота базовой станции, кГц. В TLV кортеже его тип всегда «12»

...

Если бы DCD сообщение действительно состояло только из четырех вышеперечисленных полей, то его бинарный вид в шестнадцатеричном представлении, рассылаемый базовой станцией в эфире, мог бы выглядеть так, как показано в последней строке таблицы 2.

Таблица 2 – Пример DCD сообщения

Type

Count

BS EIRP

Frequency

header

T

L

V= -30 dBm

T

L

V=2000000 kHz

01

05

02

02

FF

E2

0C

04

00

1E

84

80

Первый параметр Type со значением «1» означает, что это именно DCD-сообщение, второй параметр, Count=5, свидетельствует о том, что значения последующих полей DCD-сообщения менялись пять раз с момента начала работы базовой станции. Поскольку обязательные параметры DCD-сообщения закончились, то последующие параметры дополнительно упаковываются в TLV-кортежи. Поэтому третий байт сообщения является полем «тип» со значением 0x02 параметра «BS EIRP». Его длина по стандарту равна 2 байта, что отражено в следующем байте сообщения. Соответственно, пятый и шестой байт DCD-сообщения содержат значение параметра «BS EIRP», равное минус 30, или, в шестнадцатеричном представлении, 0xFFE2. В стандарте принят формат big-endian, старшие байты идут первыми в потоке. Подобным образом пристыковывается к сообщению параметр «Frequency». По стандарту его тип = 12 (0x0C), длина = 4 байта (0x04), значение, выбранное мною для примера = 2000000 (0x001E8480).

С учетом вышесказанного класс DCD-сообщения может быть представлен следующим образом (листинг 1)

      //------------------- файл MessageDcd.h
      class MessageDcd
{
unsigned char _type;
unsigned char _count;
short _eirp;
unsigned int _frequency;

public:
  unsigned char GetType() const { return _type; }
  unsigned char GetCount() const { return _count; }
  short GetEirp() const { return _eirp; }
  unsigned int GetChannelFrequency() const { return _frequency; }
  void SetPdu(const Pdu& aPdu);
};

Пример использования такого класса в мобильных станциях, получающих DCD-сообщение от базовой станции через эфир, показан в листинге 2.

      const Pdu& aPdu = GetPduFromDriver();

MessageDcd dcd;

dcd.SetPdu(aPdu);

unsignedint frequency = dcd.GetChannelFrequency();

std::cout << "The BS works on channel frequency = " << frequency;

Из пяти функций класса MessageDcd четыре функции относятся к интерфейсу доступа на чтение переменных класса. К ним еще необходимо добавить четыре функции доступа на запись переменных, которые реализуются по аналогии. Итого восемь функций интерфейса доступа. Одна функция десериализации бинарного потока SetPdu(..) является интерфейсом, обеспечивающим функциональность класса. Эта функция принимает от драйвера массив байтов Pdu и заполняет ими значения полей объекта «сообщения DCD». Если реализовать полный интерфейс доступа к 50-ти параметрам класса, то единственная информативная функция SetPdu(..) потерялась бы в сотне функций доступа.

Другим недостатком явного объявления типа параметра является то, что изменение размера переменной или ее типа в исходном техническом задании влекут за собой соответствующие изменения в листингах 1 и 2, а именно: в объявлении переменной, в интерфейсах доступа к ней, и во всех местах кода, где она назначается.

Использование библиотеки автоматической генерации интерфейсов

Предлагаемая реализация основана на использовании в интерфейсе класса шаблонных функций доступа к полям класса. Алгоритм создания класса сообщения DCD, как и любого другого класса сообщения стандарта [1], предполагает три шага: 1) создание перечисления (enum) идентификаторов параметров, 2) наследование создаваемого класса DCD-сообщения от класса интерфейсов и 3) описание характеристик параметров DCD-сообщения. Продемонстрирую эти три шага на примере.

Первый шаг: перечисление идентификаторов параметров показан в листинге 3.

Листинг 3. Перечисление идентификаторов параметров DCD-сообщения

      //------------------- файл dcd_para_id.h
      enum DcdParameterIds
{
  DcdMsgType,
  DcdConfigChangeCount,
  //for TVL parameters:
  DcdEirp = 2,
  DcdFrequency = 12
};

Название каждого элемента перечисления DdcParameterIds будет играть роль «имени» в параметре шаблонной функции доступа к полям класса DCD-сообщения. Численное значение элементов перечисления, относящихся к TLV-параметрам, должно совпадать с соответствующим значением поля «тип» TLV-кортежа из таблицы 1, что необходимо для механизма десериализации PDU. Для идентификаторов обязательных параметров численное значение можно не указывать. Однако, чтобы в перечислении DdcParameterIds не было случайного совпадения значений идентификаторов обязательных параметров со значениями, указанными для идентификаторов TLV параметров, первому элементу перечисления можно задать отрицательное значение, например: DcdMsgType = -16.

Второй шаг: создание класса Message<DcdId> через наследование его от шаблонного класса интерфейсов, показан в листинге 4 и 5.

      //------------------- файл dcd.h
      #include
      "msg_interface.h"
      template<>
class Message<DcdId>
  : public MessageInterface<MessageSet<DcdId>, DcdParameterIds>
{
};

Прокомментирую, класс DCD-сообщения является специализацией шаблона Message<>, параметром которого является перечисление. Это перечисление (enum) определено в отдельном файле и единственное, что нам надо сейчас знать о нем, так это то, что в перечислении DcdId = 1 соответствует значению первого байта PDU, заголовка DCD-сообщения.

Шаблон MessageInterface<> будет описан ниже, в реализации библиотеки. Определение класса MessageSet<DcdId>, объединяющего в себе набор всех параметров сообщения, показано в листинге 5.

      //------------------- файл dcd_set.h
      template<>
class MessageSet<DcdId>:
  public Parameter<DcdParameterIds, DcdMsgType>,
  public Parameter<DcdParameterIds, DcdConfigChangeCount>,
  public Parameter<DcdParameterIds, DcdEirp>,
  public Parameter<DcdParameterIds, DcdFrequency>
{
};

Определение шаблона Parameter<class T, T id> будет показано на третьем шаге, но, забегая вперед, продемонстрирую, как изменится программа, представленная в листинге 2, если использовать новый подход к доступу переменных класса.

      const Pdu& pdu = GePduFromDriver();

Message<DcdId> dcd;

dcd.SetPdu(pdu);

const Parameter<DcdParameterIds, DcdEirp>&
  eirp = dcd.GetParameter<DcdEirp>();

const Parameter<DcdParameterIds, DcdFrequency>& 
  frequency = dcd.GetParameter<DcdFrequency>();

std::cout << "BS equivalent isotropic power = " << eirp;
std::cout << "BS works on channel frequency = " << frequency;

Какую выгоду получает пользователь библиотеки?

Во-первых, пользователь оперирует в программе уже не конкретными типами, а их именами-идентификаторами. Ему нет необходимости задумываться над тем, какими характеристиками обладает параметр, это все скрыто в определении класса Parameter<>. Такой подход позволяет разработчику направить усилия на решение задачи, а не на поиск и подбор подходящих типов.

Во-вторых, интерфейс доступа ко всем полям сообщения становится однотипным, унифицированным и осуществляется через шаблонную функцию GetParameter<>, параметром которой является идентификатор поля сообщения. Следует отметить, что шаблонная функция GetParameter<> определена в классе MessageInterface<>, поэтому ее использование на данном этапе является частью соглашения, или правилом использования библиотеки.

Третий шаг: описание характеристик параметров, показан в листинге 7.

Листинг 7. Отображение рабочей частоты базовой станции

      //------------------- файл dcd_parameters.h
      template<>
class Parameter <DcdParameterIds, DcdMsgType>
: public ParameterFactory<Plain, DcdParameterIds, DcdMsgType, 1, Unsigned>
{};

template <>
class Parameter<DcdParameterIds, DcdConfigChangeCount>
: public ParameterFactory<Plain, DcdParameterIds, DcdConfigChangeCount, 1, Unsigned>
{};

//----- TLV parameterstemplate <>
class Parameter<DcdParameterIds, DcdEirp>
: public ParameterFactory<Tlv, DcdParameterIds, DcdEirp, 2, Signed>
{};

template <>
class Parameter<DcdParameterIds, DcdFrequency>
: public ParameterFactory<Tlv, DcdParameterIds, DcdFrequency, 4, Unsigned>
{};

Класс Parameter<> объявлен в отдельном заголовочном файле, но нигде не определен. Определены лишь полные его специализации, как в листинге 7. Это защищает разработчика от ошибок доступа к неописанному параметру, поскольку компилятор в этом случае не сможет скомпилировать программный код. Класс Parameter<> является оболочкой для класса фабрики параметров. В фабрику параметров ParameterFactory<> мы записываем такие характеристики, как тип поля сообщения (простой тип или TLV), идентификатор параметра, размер параметра в байтах и тип поля (знаковый или беззнаковый). Все эти характеристики мы берем из таблиц стандарта IEEE802.16 [1]. Таким образом, если в стандарте изменяется тип или размер параметра, эти изменения мы вносим только в одном файле. Для DCD-сообщения это файл из листинга 7. Первый и последний параметры шаблона ParameterFactory<> являются обычными перечислениями и показаны в листинге 8.

Листинг 8. Параметры шаблона

      enum FieldFormat
{
  Plain,
  Tlv
};

enum SignType
{
  Signed,
  Unsigned
};

Подведем краткий итог. Чтобы иметь доступ к параметрам сообщений, нет необходимости помнить название интерфейсных функций и тип возвращаемого объекта. Для этого необходимо иметь только список идентификаторов, которые содержатся в перечислении. Для сообщения Message<DcdId> таким перечислением является DcdParameterIds. Чтобы создать новое сообщение, необходимо выполнить три вышеописанные операции, аналогично приведенным в листингах 3, 4 и 5.

ПРИМЕЧАНИЕ

Примечание: в листинге 7 нет необходимости наследовать шаблонный класс Parameter<> от класса ParameterFactory<>. Идеологически было бы более правильно сделать класс Parameter<> классом свойств, смотри рекомендацию под номером 32 из [2]. Это позволило бы избежать дополнительных накладных расходов на наследование и избавиться от возможных проблем, связанных с наследованием (перегруженные конструкторы, операторы присваивания). Реализация класса Parameter<> в виде класса свойств показана в листинге 9.

Листинг 9. Реализация параметра в виде класса свойств

      template<>
class Parameter <DcdParameterIds, DcdMsgType>
{
public:
  typedef ParameterFactory<Plain, DcdParameterIds, DcdMsgType, 1, Unsigned> Type;
};

Одним из неудобств применения класса свойств является небольшое усложнение синтаксиса. Если при наследовании (листинг 7) можно записать тип Parameter<>, то для класса свойств (листинг 9) необходимо будет добавлять Parameter<>::Type, и в шаблонах потребуется запись в виде typename Parameter<>::Type.

Реализация библиотеки шаблонов сообщений

В данном разделе будут представлены лишь ключевые классы из библиотеки шаблонов сообщений. Одним из таких классов является класс-шаблон MessageInterface<>, показанный в листинге 10.

Листинг 10. Класс-шаблон интерфейса

      //------------------- файл msg_interface.h
      template<typename Set, typename ParameterIds>
class MessageInterface : private Set
{
public:
  void SetPdu(const Pdu& aPdu){Set::SetPdu(aPdu);}

  template <ParameterIds ID>
  const Parameter <ParameterIds, ID>& GetParameter() const
  {
    return *this;
  }

  template <ParameterIds ID>
  consttypename Parameter <ParameterIds, ID>::
  ValueType& GetValue() const
  {
    return Parameter<ParameterIds, ID>::GetValue();
  }
};

Класс MessageInterface<> унаследован от класса – набора параметров, см. пример в листингах 4 и 5. Вторым параметром шаблона MessageInterface<> является перечисление, содержащее идентификаторы полей сообщения. Эти идентификаторы в свою очередь являются параметрами шаблонных функций доступа к переменным класса MessageInterface<>. Поэтому любое обращение в программе к интерфейсной функции GetParameter<>() даёт указание компилятору воплотить ее в объекте, что избавляет от необходимости вручную описывать каждую функцию доступа.

В классе MessageInterface<> представлена также функция запроса значения параметра GetValue<>(). Эта функция возвращает численное значение параметра, но его тип скрыт за именем Parameter<>::ValueType. ValueType – это имя конкретного типа, переопределенное в классе ParameterFactory<> через объявление typedef класса свойств.

Листинг 11. Реализация типов через свойства параметров

      template <unsigned Size, SignType SignedOrUnsigned>
struct ValueTraits;

template <> 
struct ValueTraits<1, Signed> 
{
typedef ParameterValue< char > PolicyType;
};

template <> 
struct ValueTraits<2, Signed> 
{
typedef ParameterValue< short > PolicyType;
};

template <> 
struct ValueTraits<4, Unsigned> 
{
typedef ParameterValue< unsignedlong > PolicyType;
};

Таким образом, зная размер параметра, в данном случае в байтах, а также какое число, знаковое или беззнаковое, он в себе хранит, можно передать в класс-шаблон ParameterValue<> конкретный тип параметра (char, short или unsigned long). В программе этот тип будет фигурировать как Parameter<>::ValueType или ParameterValue<>::ValueType, что одно и то же, т.к. шаблон Parameter<> в библиотеке унаследован от ParameterValue<> через класс свойств ValueTraits<>, см. листинги 7 и 12.

Листинг 12. Доступ к конкретному типу параметра сообщения

      template<typename Value>
class ParameterValue
{
protected:
  typedef Value ValueType;

public:
  const ValueType& GetValue() const {return _value;}

private:
  Value _value;
};

template<FieldFormat TlvOrPlain, typename ParameterIds,
    ParameterIds ID, int Size, SignType SignedOrUnsigned >
class ParameterFactory
: publictypename SetTraits<TlvOrPlain>::template Set< ParameterIds >::PolicySet,
  protectedtypename ValueTraits<Size, SignedOrUnsigned >::PolicyType
{
  ...
};

В итоге программист создает сообщение, указывая свойства параметров из стандарта IEEE802.16 [1] (или из технического задания), а компилятор, руководствуясь правилом из листинга 11, конструирует параметры, подставляя конкретные типы. У архитектора библиотеки теперь есть механизм для изменения, контроля или оптимизации типов в одном месте (листинг 11), позволяющий не затрагивать код, использующий эту библиотеку. При внесении изменений необходимо будет только перекомпилировать библиотеку и использующий ее программный код.

Класс свойств SetTraits<>, в зависимости от формата поля (простой или TLV), подставляет в класс ParameterFactory<> соответствующий обработчик PDU. Механизм подстановки аналогичен описанному алгоритму для шаблона ValueTraits<> в листинге 11 и в данной статье не приводится.

Фактически класс фабрики параметров ParameterFactory<> в листинге 12 наследует две стратегии: механизм обработки PDU и способ хранения данных.

Использование классов свойств и классов стратегий позволяет конструировать классы на этапе компиляции, поскольку уже на этом этапе известны свойства будущих объектов – статическая диспетчеризация [3, 4]. Наследование с виртуальными функциями-обработчиками необходимы для этапа исполнения программы, когда тип требуемой операции определяется из поступающих данных – динамическая диспетчеризация [3]. На принципе динамической диспетчеризации основан обработчик полей PDU, описание которого выходит за рамки данной статьи, однако идея реализации обработчика, в которой выбор вызываемой функции определяется по поступающему коду операции, описана в статье [5].

Заключение

Целью данной статьи являлось показать возможность упрощения интерфейса доступа к внутренним переменным классов. Множественное наследование не является обязательным условием работоспособности описанного принципа доступа, и его можно заменить механизмом включения. Более того, обобщенное программирование, описание параметров в виде шаблонов совместно с частичной или полной специализацией шаблонов, позволяет использовать рекурсивные методы создания классов параметров, подобно тому, как это описано в статье [5]. Таким образом, имея относительно сложный механизм реализации классов сообщений Message<>, мы получаем унификацию методов доступа к внутренним переменным, избавив разработчика от ненужной работы описывать интерфейс, и простой механизм использования классов (листинг 6). Внешняя простота является одним из залогов безопасного использования библиотеки классов сообщений. Как писал Гради Буч в одном из выводов к первой главе книги [6]: "Задача разработчиков программных систем – создать у пользователя разрабатываемой системы иллюзию простоты".

Хотелось бы процитировать выдержку из Web-ресурса «Inside C++» [7] об ответственности: "...Задача ответственного программиста заключается в том, чтобы написать такой класс (функцию, модуль, библиотеку и так далее), чтобы ни у кого не было ни одной возможности его в чем-либо обвинить. Программист должен защитить пользователя со всех сторон. Что бы пользователь ни вытворял с классом — это не должно доставить ему никаких неприятностей". Применительно к описываемому в статье коду, заботой о пользователе библиотеки является предоставленная ему возможность описывать сообщения и их параметры в категориях близких к стандарту IEEE802.16 [1]. Задача реализации классов, подстановки и проверки типов, генерации классов исключений возложена на компилятор и классы шаблонов описанной библиотеки. Необходимо как можно больше проверок переносить из этапа времени выполнения в этап времени компиляции. Надеюсь, что представленные идеи и их реализация будут интересны и полезны сообществу разработчиков.

Список литературы

  1. IEEE 802.16-2009 – http://standards.ieee.org/getieee802/download/802.16-2009.pdf
  2. Саттер Г., Александреску А. Стандарты программирования на С++.: Пер. с англ. – М.: Издательский дом «Вильямс», 2005. – 224 с.: ил. – Парал. тит. англ.
  3. Александреску А. Серия С++ In-Depth, т.3. Современное проектирование на С++, испр. изд. : Пер. с англ. – М.: Издательский дом «Вильямс», 2004. – 336 с.: ил. – Парал. тит. англ.
  4. Кузнецов п. Симуляция частичной специализации шаблонов в C++. – http://dtf.ru/articles/read.php?id=11
  5. Нетривиальное использование шаблонов. Часть первая. – http://www.source-code.ru/magazine/0105/templates.html
  6. Гради Буч Объектно-ориентированный анализ и проектирование с примерами приложений на С++, Издательства: Бином, Невский Диалект, 1998 г., – 560 с.
  7. Inside C++. Об ответственности – http://www.insidecpp.ru/art/25/


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 49    Оценка 0 [+0/-2]         Оценить