Доброе утро!
На днях некоторые коллеги, привычные к синтаксису 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++ можно красиво реализовать шаблон проектирования Строитель.