angle-left

Вариант реализации шаблона проектирования Строитель в C++

Доброе утро!

На днях некоторые коллеги, привычные к синтаксису Java, были удивлены выразительностью «экзотической» реализации чего-то близкого к шаблону проектирования Строитель (Builder), что и подтолкнуло меня поделиться ею.

Для начала взглянем на предмет вопроса со стороны «пользовательского API». Разработчик, имеющий опыт в C++ вряд ли увидит что-то новое для себя, но первый взгляд на подобное действительно заставляет обратить внимание на столь странную, и в то же время красивую синтаксическую конструкцию:

  using rt = table::entry::type_t;
  int x = 3;
  table t;
  t.add_row()
    (rt::STRING, "the first entry")
    (rt::NUMBER_LIST, std::vector<int>{1, 3, 3, 7})
    ;
  if (5 > x) {
    t.last_row()
      (rt::NUMBER, x)
      ;
  }
  t.add_row()
    (rt::STRING, "the first entry of the second row")
    ;

Красиво, не правда ли?

Здесь и далее я буду приводить примеры на абстрактной таблице, содержащей в себе произвольное число строк, каждая из которых содержит произвольное число записей, включающих себя тип данных внутри некоторой предметной области и std::any (кто не хочет C++17 — есть boost::any), хранящий запись. Предполагается, что разбором того, что же это такое занимается некий внешний код, получающий такую таблицу.

Теперь попробуем реализовать её. Объявим класс table, содержащий в себе классы строки (row) и элемента строки (entry):

class table {
public:
  class entry {
  public:
    enum class type_t {
      STRING,
      NUMBER_LIST,
      NUMBER
    };
    entry(type_t type, std::any value);
    type_t type() const;
    const std::any & value() const;
    template<typename T>
    T value() const;

  private:
    type_t type_;
    std::any value_;
  };

  class row {
    std::vector<entry> row_;

  public:
    row & operator()(entry::type_t type,
                     const std::any &value);
    std::vector<entry>::iterator begin();
    std::vector<entry>::iterator end();
    std::vector<entry>::const_iterator begin() const;
    std::vector<entry>::const_iterator end() const;
    std::vector<entry>::const_iterator cbegin() const;
    std::vector<entry>::const_iterator cend() const;
  };

  using entry = entry;
  using row = row;
  using content = std::vector<row>;

  table();
  row & add_row();
  row & last_row();
  content::iterator begin();
  content::iterator end();
  content::const_iterator begin() const;
  content::const_iterator end() const;
  content::const_iterator cbegin() const;
  content::const_iterator cend() const;

private:
  content content_;
};

Методы, возвращающие итераторы не особо интересны, так как они нужны лишь для того, чтобы пройтись по строкам и столбцам таблицы в пользовательском коде. Реализация их примитивна — они проксируют соответствующие методы у объектов, хранящих данные.

Определим сразу методы, реализующие нашего строителя. Первый из следующей пары методов создаёт очередную строку в таблице, после чего, как и второй, возвращает неконстантную ссылку на неё, позволяя таким образом эту строку редактировать:

table::row & table::add_row() {
  content_.push_back(row{});
  return content_.back();
}

table::row & table::last_row() {
  return content_.back();
}

А добавление элементов в эту строку реализуется следующим образом:

table::row & table::row::operator()(entry::type_t type,
                                    const std::any &value) {
  row_.push_back(entry(type, value));
  return *this;
}

Здесь мы видим, что после вызова add_row() была создана строка, ссылку на которую мы получили. А далее мы просто вызываем operator() у этой строки, который добавляет в неё элемент и возвращает ссылку на неё же. Даже почти не запутано.

Всё, что остаётся пользователю — создать и заполнить объект таблицы по примеру из начала статьи, после чего передать куда-то, где по ней смогут проитерировать, пользуясь геттерами следующего вида:

table::entry::type_t table::entry::type() const { return type_; }
const std::any & table::entry::value() const { return value_; }
    // Шаблонный метод, реализуется в заголовочном файле
    template<typename T>
    T value() const { return std::any_cast<T>(value_); }

Таким образом, получив информацию о типе объекта (или зная его заранее), пользовательский код сможет воспользоваться шаблонным методом с правильным параметром, даже не делая any_cast<>() к данным.

На этом всё, мы рассмотрели, как средствами C++ можно красиво реализовать шаблон проектирования Строитель.