Доброго понедельника, хабровчане! Только что возился с одним универсальным, а потому неприлично мощным интерфейсом доступа к данным на Python. Неприличная мощь выражается в виде разнообразных параметров на все случаи жизни, зачастую крайне экстравагантных и необходимых лишь в 5% случаев.
В результате приходится дублировать весь стек параметров и деталей даже в простых запросах, что вызывает пессимизм и желание заняться чем-то другим.
И тут я вспомнил похожую историю из своего далекого прошлого, которой делюсь.
Проблема
Это было давно и неправда, настолько давно, что это можно смело отнести к мемуарам.
Нам поручили помочь отделу аутсорсинга.
Проект уже находился на стадии активного кодирования и обсуждать и что-либо менять было уже поздно (а может, и вообще невозможно).
Необходимо было создать интерфейс бизнес-уровня к данным, хранящимся на 20 планшетах.
В С++.
Для Windows и Linux. 20 пластин в Postgres или Oracle. «Интерфейс бизнес-уровня» — это эвфемизм для очень глупого набора операций над сущностями из этих таблиц, а именно выбора по ключу или значению других полей, создания, изменения или выбора связанных сущностей.
К тому времени я уже начинал догадываться, что наша работа порой не очень интересна, но не то чтобы интересна! Эта задача в своем запустении побила все рекорды.
Я вдруг почувствовал себя на месте тех людей, которым неинтересно свое дело, и это меня почему-то совершенно не вдохновило.
На этом фоне у моих коллег все было не так уж и плохо - они просто перебрали кучу C++(C) или библиотек, упрощающих доступ к базе данных, и убедились, что ни одна из них, вопреки утверждениям, не работает нормально одновременно как Oracle и Postgres. Идея Убежденность в том, что эту дурацкую работу должен делать кто угодно, только не я, постепенно подтолкнула меня к мысли, что я должен винить в этом кого-то.
Лишь компьютер из моего круга согласился смириться с такой участью, поэтому в итоге ему выпала честь сгенерировать необходимый код. Единственное, что меня смущало во всей этой затее, это то, что я имел на тот момент весьма смутное представление о предмете как о чем-то очень сложном и нетривиальном.
Идея генерации была вынесена из инкубационного периода случайным воспоминанием об отладке веб-приложения с JSP, бинами и прочими ужасами, летающими на крыльях ночи.
Отладчик показал мне код, сгенерированный на странице JSP (страницы сервера Java).
Например, с этой страницы:
rhino codegen.js template:В результате получается примерно следующий код:
rhino codegen.js template:То есть алгоритм генерации кода из JSP, который, в свою очередь, генерирует HTML, очень прост, примерно: всё внутри <% %> становится кодом, а все, что находится снаружи, оборачивается out.print(" "), ну и немного синтаксического сахара.
В нашем случае нам просто нужно заменить HTML на C++, а Java на что-то другое и после простейших преобразований мы получим код, который сгенерирует для нас нужный код C++.
Я выбрал «что-то еще» по принципу наименьшего сопротивления — хост Windows-скриптов уже был на нашей сборочной машине, поэтому мы используем java-скрипт (или ECMAScript, как бы он ни назывался).
Опытный образец В этой статье мы рассмотрим решение, максимально «близкое к тексту».
К сожалению, рассмотреть оригинальное решение не получится — слишком много важных деталей уже стерты из памяти.
Будем считать, что у нас есть только база данных Postgres, поэтому, чтобы не покидать уютную среду Linux, мы также не будем использовать хост сценариев Windows; мы будем использовать некоторую простую стандартную схему с данными о сотруднике, отделе и т. д. В качестве самостоятельной реализации JS возьмем первую, которая пришла на ум — Rhino (почему-то v8 была второй).
Итак, устанавливаем Rhino, создаем файл codegen.js, пишем print(2*2); rhino codegen.js: 4, вуаля! codegen.js
rhino codegen.js cpp.template:— самая тупая и прямолинейная реализация описанного чуть выше алгоритма — 50 строк — весь генератор кода.
Тестирование: файл шаблона:
rhino codegen.js sql.template:
<ul>
<% for (int i = 0; i < 10; i++) { %>
<li> <%= i %>
<% } %>
</ul>
out.println("<ul>");
for (int i = 0; i < 10; i++) {
out.print("<li>");
out.print(i);
out.println();
}
out.println("</ul>");
В <% %> у нас есть любой js-код, <%= expr %> заменяется результатом вычисления выраж.
В принципе, это всё, что нам нужно, достаточно, чтобы сгенерировать вообще что угодно.
К сожалению, стоит отметить, что у этой простоты есть и обратная сторона — код в шаблоне очень плотный и его трудно читать без подходящей подсветки синтаксиса.
Сам JS играет в этом не последнюю роль — он не отличается лаконичностью и выразительностью.
Теперь пришло время попытаться сгенерировать что-то более полезное.
файл шаблона: if (arguments.length < 1)
{
print("Usage: codegen.js <template>");
quit();
}
function produce_text(text)
{
return "__codegen_output += '" + text.replace("'", "\\'", 'g').
replace('\n', '\\n', 'g').
replace('\r', '\\r', 'g').
replace('\t', '\\t', 'g') + "';\n";
}
function produce_code(code)
{
if (code[0] == '=')
{
return '__codegen_output += ' + code.substr(1) + ';\n';
}
return code + '\n';
}
remainder = readFile(arguments[0]);
var code = 'var __codegen_output = ""; ';
while (remainder.length)
{
var begin = remainder.indexOf('<%');
if (begin >= 0)
{
code += produce_text(remainder.substr(0, begin));
remainder = remainder.substr(begin + 2);
var end = remainder.indexOf('%>');
if (end >= 0)
{
code += produce_code(remainder.substr(0, end));
remainder = remainder.substr(end + 2);
}
else
{
code += produce_code(remainder);
remainder = '';
}
}
else
{
code += produce_text(remainder);
remainder = '';
}
}
code += 'print(__codegen_output);'
eval(code);
<%
var className = 'MyClass';
var fields = ['Name', 'Description', 'AnotherOne', 'LastOne'];
%>
class <%= className %>
{
private:
<% for(var i = 0; i < fields.length; i++) { %>
int <%= fields[i] %>;
<% } %>
};
class MyClass
{
private:
int Name;
int Description;
int AnotherOne;
int LastOne;
};
Содержимое переменной модели, по сути, является так называемым DSL (предметно-ориентированный язык).
В нашем случае это язык описания сущностей предметной области.
Текущая его версия слишком примитивна, чтобы быть хоть как-то полезной, поэтому мы дополним ее всем необходимым.
<%
var model = [
{
name: 'Employee',
fields: {
Id: { type: 'int' },
Name: { type: 'string' }
}
}
];
var cppTypeMap = {
'int': 'int',
'string': 'std::string'
};
%>
<% for (var i = 0; i < model.length; i++)
{
var entity = model[i];%>
struct <%= entity.name %>
{
<% for (var field in entity.fields)
{ %>
<%= cppTypeMap[entity.fields[field].
type] %> <%= field %>;
<% } %>
};
<% } %>
Решение
Теперь мы можем генерировать код для извлечения сущностей из базы данных по ключу, по связанным сущностям и т.д. Также можно сгенерировать sql-скрипт для создания базы данных.
Чтобы генерировать разные исходники из одной модели, немного разбросаем код: модель struct Employee
{
int Id;
std::string Name;
};
будет содержать только наш DSL, cpp.шаблон var model = [
{
name: 'Department',
fields: {
Id: { type: 'int' },
Name: { type: 'string'}
},
primaryKey: 'Id'
},
{
name: 'Employee',
fields: {
Id: { type: 'int' },
Name: { type: 'string' },
DepartmentId: { type: 'int', references: 'Department' }
},
primaryKey: 'Id'
}
];
— шаблон для генерации плюскода и sql.шаблон var model = [
{
name: 'Department',
fields: {
Id: { type: 'int' },
Name: { type: 'string'}
},
primaryKey: 'Id'
},
{
name: 'Employee',
fields: {
Id: { type: 'int' },
Name: { type: 'string' },
DepartmentId: { type: 'int', references: 'Department' }
},
primaryKey: 'Id'
}
];
— шаблон для генерации скрипта создания базы данных.
<%
load('model');
var cppTypeMap = {
'int': 'int',
'string': 'std::string'
};
function fieldType(entity, field)
{
return cppTypeMap[entity.fields[field].
type]; } %> <% for (var i = 0; i < model.length; i++) { var entity = model[i];%> struct <%= entity.name %> { <% for (var field in entity.fields) { %> <%= fieldType(entity, field) %> <%= field %>; <% } %> <% var fieldList = []; for (var field in entity.fields) fieldList.push(field); %> static <%= entity.name %> ByKey(<%= fieldType(entity, entity.primaryKey) %> key, pqxx::work& tr) { if (!tr.prepared("<%= entity.name %>ByKey").
exists()) { tr.conn().
prepare("<%= entity.name %>ByKey", "select <%= fieldList.join() %> from <%= entity.name %> where <%= entity.primaryKey %> = $1"); } pqxx::result rows = tr.prepared("<%= entity.name %>ByKey")(key).
exec(query); <%= entity.name %> result; <% for (var j = 0; j < fieldList.length; j++) { %> result.<%= fieldList[j] %> = rows[0][<%= j %>].
as<<%= fieldType(entity, fieldList[j]) %>>(); <% } %> return result; } <% for (var field in entity.fields) if (entity.fields[field].
references) { var ref = entity.fields[field].
references; %> <%= ref %> Get<%= ref %>() { return <%= ref %>::ByKey(<%= field %>); } static std::vector<<%= entity.name %>> By<%= ref %>(<%= fieldType(entity, field) %> key) { if (!tr.prepared("<%= entity.name %>By<%= ref %>").
exists()) { tr.conn().
prepare("<%= entity.name %>By<%= ref %>", "select <%= fieldList.join() %> from <%= entity.name %> where <%= field %> = $1"); } pqxx::result rows = tr.prepared("<%= entity.name %>By<%= ref %>")(key).
exec(query); std::vector<<%= entity.name %>> result; for (pqxx::result::size_type i = 0; i < rows.size(); i++) { <%= entity.name %> row; <% for (var j = 0; j < fieldList.length; j++) { %> row.<%= fieldList[j] %> = rows[i][<%= j %>].
as<<%= fieldType(entity, fieldList[j]) %>>();
<% } %>
result.push_back(row);
}
return result;
}
<% } %>
};
<% } %>
<%
load('model');
var sqlTypeMap = {
'int': 'integer',
'string': 'text'
};
function fieldType(entity, field)
{
return sqlTypeMap[entity.fields[field].
type]; } %> <% for (var i = 0; i < model.length; i++) { var entity = model[i];%> CREATE TABLE <%= entity.name %> ( <% for (var field in entity.fields) { var ref = entity.fields[field].
references; %>
<%= field %> <%= fieldType(entity, field) %><% if (ref) { %> REFERENCES <%= ref %><% } %>,
<% } %>
PRIMARY KEY (<%= entity.primaryKey %>)
);
<% } %>
Признаюсь, что я не запускал сгенерированный код и даже не компилировал его, но обещаю, что он очень близок к реальному коду, работающему с Postgres. :) struct Department
{
int Id;
std::string Name;
static Department ByKey(int key, pqxx::work& tr)
{
if (!tr.prepared("DepartmentByKey").
exists()) { tr.conn().
prepare("DepartmentByKey", "select Id,Name from Department where Id = $1"); } pqxx::result rows = tr.prepared("DepartmentByKey")(key).
exec(query); Department result; result.Id = rows[0][0].
as<int>(); result.Name = rows[0][1].
as<std::string>(); return result; } }; struct Employee { int Id; std::string Name; int DepartmentId; static Employee ByKey(int key, pqxx::work& tr) { if (!tr.prepared("EmployeeByKey").
exists()) { tr.conn().
prepare("EmployeeByKey", "select Id,Name,DepartmentId from Employee where Id = $1"); } pqxx::result rows = tr.prepared("EmployeeByKey")(key).
exec(query); Employee result; result.Id = rows[0][0].
as<int>(); result.Name = rows[0][1].
as<std::string>(); result.DepartmentId = rows[0][2].
as<int>(); return result; } Department GetDepartment() { return Department::ByKey(DepartmentId); } static std::vector<Employee> ByDepartment(int key) { if (!tr.prepared("EmployeeByDepartment").
exists()) { tr.conn().
prepare("EmployeeByDepartment", "select Id,Name,DepartmentId from Employee where DepartmentId = $1"); } pqxx::result rows = tr.prepared("EmployeeByDepartment")(key).
exec(query); std::vector<Employee> result; for (pqxx::result::size_type i = 0; i < rows.size(); i++) { Employee row; row.Id = rows[i][0].
as<int>(); row.Name = rows[i][1].
as<std::string>(); row.DepartmentId = rows[i][2].
as<int>();
result.push_back(row);
}
return result;
}
};
CREATE TABLE Department (
Id integer,
Name text,
PRIMARY KEY (Id)
);
CREATE TABLE Employee (
Id integer,
Name text,
DepartmentId integer REFERENCES Department,
PRIMARY KEY (Id)
);
Для многих, думаю, очевидно, что я не придумал ничего нового.
Многие ORM могут генерировать аналогичный код. Основной целью статьи было продемонстрировать, что создать свой язык, пусть даже просто DSL, дело непростое, а очень простое.
Это совсем не так страшно, как кажется, ведь на многих этапах можно сэкономить немало денег.
Например, в данном случае мы сэкономили на парсере — большую часть работы выполняет JS-движок, и на компиляторе — генерировать плюс-код гораздо проще, чем машинный код, и пусть плюс-компилятор несет всю нагрузку.
Теги: #JavaScript #C++ #dsl #генерация кода #генератор кода #генеративное программирование #orm #JavaScript #C++
-
Монитор Dell Ultrasharp 2709 Вт
19 Oct, 24 -
Рейтинг Mail.ru Научился Смотреть Smart Tv
19 Oct, 24 -
Когда Мы Были Молоды... Компьютерщики
19 Oct, 24 -
Фотография Плазменного Шара Во Времени
19 Oct, 24