Три неочевидных примера использования шаблонизаторов в backend-е

в 14:06, , рубрики: mustache, python, python 3, Анализ и проектирование систем, Программирование, Проектирование и рефакторинг, шаблонизация

С одной стороны, предмет действительно был квадратным. C другой стороны он был круглым. Но с третьей стороны, с которой должен быть треугольник, предмет вышел кривой и косой.

— Алешенька идет на совещанку? — в дверь просунулась Леночкина заинтересованная физиономия.
— Алешенька на совещанку не идет. Алешенька пишет статью.
— О кубиках?
— Каких еще кубиках? — я опустил глаза, в руках и правда был злосчастный кубик. То есть шарик. То есть ромбик.
— Не о кубиках! И не о шариках. О шаблонах.
— Я им так и скажу! Шаблон, ах. — Леночка уже бежала дальше по коридору.

"О шаблонах. Даже о трех разных шаблонах". Точнее, о трех причинах использовать шаблоны в серверном коде. И ни одна из этих причин не будет про HTML.

В примерах я использовал синтаксис Mustache, в силу лаконичного синтаксиса и наличия реализаций для всего, что движется. Mustache практически не позволяет себе вольностей в отличии от, например .Net Razor, который позволяет кодировать внутри шаблона, подавая тем самым плохой пример некрепким духом разработчикам.

Примеры кода будут на python. Реализация Mustache под пайтон называется pystache.

Итак, три причины впустить шаблоны в свою жизнь свой код.

Текстовые артефакты

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

Артефактом может быть, например, JSON или plain text файл, вложение, HTTP-респонс. Главное — артефакт по сути своей результат применения функции от некоторой относительно компактной части данных вашей системы. И артефакт имеет свой собственный синтаксис.

Артефактом может быть выписка из банковского счета в текстовом формате для загрузки в legacy систему. Артефактом может быть выгрузка электронного чека в виде .json файла, который будет отправлен как вложение клиенту по почте.

Во всех этих случаях вы себе сильно упростите жизнь, используя шаблоны для создания артефактов.

Что такое шаблонизатор? Это библиотека, которая примет объектную модель (контекст), возьмет шаблон, применит одно к другому — и выдаст результат. Объектную модель и шаблон готовит программист. Итоговый результат готовит шаблонизатор.

Пример: попробуем создать текстовое сообщение о заказе.

Подготовим сначала объектную модель:

def add_comma(list):
    for a in list[:-1]:
        a["comma"] = True

def get_model():
    res = {
        "documentId": 3323223,
        "checkDate": "01.02.2019 22:20",
        "posId": 34399,
        "posAddr": "Urupinsk, 1 Maya 1",
        "lines": [
            {
                "no": 1,
                "itemtext": "Hello Kitty",
                "amount": 3,
                "sumRub": "55.20"
            },
            {
                "no": 2,
                "itemtext": "Paper pokemons",
                "amount": 1,
                "sumRub": "1230.00"
            },
            {
                "no": 2,
                "itemtext": "Book of Mustache",
                "amount": 1,
                "sumRub": "1000.00"
            }
        ],
        "total": {
            "amount": "3285.20"
        }
    }
    add_comma(res["lines"])
    res["posInUrupinsk"] = res["posId"] > 34000
    return res

Код сугубо заглушечный. В реальном коде может быть запрос базы данных, какая-то логика расчета значений (например, значение total.amount вычисляем на основании позиций заказа).

Обратите внимание на несколько вещей:

  • Это НЕ объектная модель заказа, это нечто более простое, подготовленное для применения в шаблоне. Значения "sumRub" и "total.amount" в реальной бизнес-модели не должны быть текстовыми, значению "comma" у массива lines в объектной модели не место, оно нужно только для упрощения рендеринга (pystache не может сам понять, что элемент списка последний и после него не надо ставить запятую.
  • Тип поля "amount" — текст и этот текст отформатирован под вывод в шаблоне. Если ваш шаблонный движок поддерживает форматтеры ( что-то типа "… {someValue|asMoney}" ) то форматировать прямо в модели не надо.
  • Текст в шаблоне у нас должен выглядеть несколько по другому для клиентов из Урюпинска (менеджер в последний момент прибежал и попросил добавить — бизнес очень просил, у них нежданно-негаданно стартанула маркетинговая акция для города). Поэтому мы добавили в модель булевское значение "posInUrupinsk" и использовали его в шаблоне.
  • Повторно модель от шаблона лучше не использовать, разве что для рендеринга других шаблонов

Текст шаблона mustache выглядит так:

{{#posInUrupinsk}}
ДОРОГОЙ КЛИЕНТ ИЗ УРЮПИНСКА! ОТПРАВЬТЕ НОМЕР СВОЕГО ЧЕКА НА
КОРОТКИЙ НОМЕР 100 И ПОЛУЧИТЕ КОФЕВАРКУ.
{{/posInUrupinsk}}
{{^posInUrupinsk}}
ИНФОРМАЦИЯ О ВАШИХ ПОКУПКАХ:
{{/posInUrupinsk}}

{{#lines}}
#{{no}} ... {{itemtext}}: {{sumRub}} руб{{#comma}};{{/comma}}{{^comma}}.{{/comma}}
{{/lines}}

ИТОГО: {{total.amount}}
---------------------------
N документа: {{documentId}} от {{checkDate}}

Мы видим в шаблоне, что шапка документа для заказов в Урюпинске отличается от других городов. Мы также видим, что в конце последней строки с товарной позицией стоит точка, а во всех ранних позициях — точка с запятой. За это отвечает атрибут "comma" и метод "add_comma" в генераторе моделей.

Код, который применяет контекст к шаблону, тривиален:

model = get_model()

with open(os.path.join("templates", "check.txt.mustache"), 'r') as f:
    template = f.read()

check_text = pystache.render(template, model)
print(check_text)

Результат:

ДОРОГОЙ КЛИЕНТ ИЗ УРЮПИНСКА! ОТПРАВЬТЕ НОМЕР СВОЕГО ЧЕКА НА
КОРОТКИЙ НОМЕР 100 И ПОЛУЧИТЕ КОФЕВАРКУ.

#1 ... Hello Kitty: 55.20 руб;
#2 ... Paper pokemons: 1230.00 руб;
#2 ... Book of Mustache: 1000.00 руб.

ИТОГО: 3285.20
---------------------------
N документа: 3323223 от 01.02.2019 22:20

Еще совет: если задача позволяет, сохраняйте вместе с зарендеренным шаблоном саму модель (например, в JSON формата). Это поможет при отладке и траблшутинге.


Принтер трижды пискнул, выдавая новую модельку. Треугольная сторона была теперь идеальным треугольником. Две остальные стороны были квадратными. Нейронная сеть жила своей жизнью и отказывалась выдавать примитивную по всем понятиям 3д модельку.

"Подарю Леночке кубик." — подумал я. Пусть радуется.

Кодогенерация

Вам может потребоваться создавать JavaScript в рантайме изнутри backend-а. Зачем? Для создания отчетов на стороне браузера, например. Или получить скрипт на F# изнутри программы на Go. Или Kotlin-овский код изнутри ReactJS (не могу представить, зачем это может понадобится, но вдруг у вас такие специфические наклонности).

В случае с кодогенерацией, лучше сначала написать руками результирующий код (то что мы хотим сгенерировать) а только потом разбить его на шаблон и модель. Этот подход избавит нас от тоски излишней сложности модели. Модель усложнить никогда не поздно, но лучше начать с простого.

var report = CreateChart({ title: "За второе полугодие - сводный" }, type: "lineChart", sourceUrl: "/reports/data/0" );
report.addLine({ dataIndex:0, title: "Доходы", color: "green" });
report.addLine({ dataIndex:1, title: "Убытки", color: "red" });

report.render($("#reportPlaceholder1"));

var report = CreateChart({ title: "За второе полугодие - по продуктам" }, type: "lineChart", sourceUrl: "/reports/data/1");
report.addLine({ dataIndex:0, title: "Hello Kitty", color: "#000" });
report.addLine({ dataIndex:1, title: "PokemonGo", color: "#222" });
report.addLine({ dataIndex:2, title: "Mustache", color: "#333" });

report.render($("#reportPlaceholder2"));

Здесь мы видим, что у нас на странице есть от одного до N графиков lineChart, у каждого из которого свой источник данных, заголовок и список показателей. Моделька:

def get_model():
    return {
        "charts": [
            {
              "divId": "#reportPlaceholder1",
              "title": "За второе полугодие - сводный",
                "sourceUrk": "/reports/data/0",
                "series": [
                    {"dataIndex": 0, "title": "Доходы", "color": "green"},
                    {"dataIndex": 1, "title": "Расходы", "color": "red"},
                ]
            },
            {
                "divId": "#reportPlaceholder2",
                "title": "За второе полугодие - по продуктам",
                "sourceUrk": "/reports/data/1",
                "series": [
                    {"dataIndex": 0, "title": "Hello Kitty", "color": "#000"},
                    {"dataIndex": 1, "title": "PokemonGo", "color": "#111"},
                    {"dataIndex": 2, "title": "Mustache", "color": "#222"},
                ]
            }

        ]
    }

ну и шаблон:

{{#charts}}

var report = CreateChart({ title: "{{title}}" }, type: "lineChart", sourceUrl: "{{sourceUrl}}" );
{{#series}}
report.addLine({ dataIndex:{{dataIndex}}, title: "{{title}}", color: "{{color}}" });
{{/series}}
report.render($("{{divId}}"));
{{/charts}}

Обратите внимание: такой "в лоб" подход к шаблонизации требует отдельного усилия по экранированию значений в модельке. если в series[0].title у нас прокрадется запятая или кавычка — "Hello Kitty"" — синтаксиc результирующего файла с треском развалится. Поэтому пишите функции экранирования и используйте их при создании моделей. Используйте форматтеры, если шаблонизатор это умеет.


Третий кубик полетел в дверь и со стуком отскочил. Тоже никуда не годится. Интересно. а можно кинуть кубик так, чтобы он проскочил под дверью и долетел до конца коридора? Можно ли 3д-печатать резиной? Или лучше сделать его наполненным маленькими такими икосаэдрами, наполненными воздухом?...

SQL-запросы

Придирчивый читатель скажет, что это тоже кодогенерация, трансляция концепций из одного языка программирования в другой. На что мы ответим придирчивому читателю, что работа с SQL или с любым другим языком запросов к БД — это немного отдельная область программирования и не всем очевидно, что если шаблонами можно генерить js скрипты, то и SQL тоже можно. Поэтому вынесем запросы в отдельный кейс.

И чтобы придирчивый читатель не скучал, оставим в статье только примеры нескольких запросов. Можете сами прикинуть, какая модель и какой шаблон тут лучше подойдут. В примерах на гитхабе можете посмотреть, что у меня получилось.

SELECT * FROM hosts WHERE firmware_id=1 AND bmc_login='admin' ORDER BY ip DESC;
SELECT * FROM hosts  ORDER BY name LIMIT 3;
SELECT host_type_id, COUNT(*) FROM hosts GROUP BY host_type_id;

Шаблоны (в том числе и для SQL) и примеры кода можно найти на гитхабе.

Автор: Yenox

Источник

* - обязательные к заполнению поля