Цветные функции: ищем плохие архитектурные паттерны

Когда у языка нет цветовой дифференциации функций… то у языка нет цели?

Я уже много лет занимаюсь компиляторами и языками в целом. Хочу поделиться интересной мыслью, которая когда-то пришла мне в голову. Почему-то такого я нигде не видел.

Если немного расширить понятие функции (ввести атрибут »цвет»), можно описывать паттерны вида «вызывать логгер из performance-critical мест — это плохо» или «ходить в базу при рендеринге шаблонов запрещено».

Идея абсолютно не зависит от языка и применима к любому: хоть JS, хоть Go. Разберу её подробно в статье, это будет интересно больше с теоретической точки зрения. Хотя мы даже сделали практическую реализацию для PHP, чтобы использовать у себя. Ссылки на GitHub и видео приложу в конце, а пока обо всём по порядку.

Красные, зелёные и прозрачные

Представим, что есть атрибут @color над декларацией. Пусть будут красные функции и зелёные. А те, что без цвета — прозрачные.

Когда f1() вызывает f2(), их цвета смешиваются (добавляются в цепочку):

/** @color green */
function f1() { f2(); }

// без тега @color, прозрачная (transparent)
function f2() { f3(); }

/** @color red */
function f3() { /* ... */ }

Мы определили цвета, а теперь опишем палитру. Палитра — это паттерны смешивания цветов. К примеру, запретим вызывать красные из зелёных:

green red: "calling red from green is prohibited"

Цепочка "green transparent red" попадает под правило "green red", и поэтому "f1 → f2 → f3" триггерит ошибку. А просто "f1 → f2" — это цепочка "green transparent", уже нет. Это как паттерн-матчинг: "green yellow blue red" тоже попадает под "green red".

Думаем в терминах цветов: находим медленные места

Допустим, у нас есть медленные функции (работающие с базой данных или диском) и есть критичные к производительности (или быстрые) функции, задача которых — иметь минимальный оверхед.

Для наглядности пусть медленные будут красными, а быстрые зелёными:

Что такое потенциально медленное место в этом случае? Это если красная функция вызывается из зелёной. Как-то похоже на то, что выше, да? :)

На реальном примере:

// class ApiRequestHandler, зелёная
function handleRequest(DemoRequest $req) {
    $logger = createLogger();
    $logger->debug('Processing ' . $req->id);
}

// class Logger
function debug(string $msg) {
    DBLayer::instance()->addToLogTable([
        'time'    => time(),
        'message' => $msg,
    ]);
}

// class DBLayer, красная
function addToLogTable(array $kvMap) {
    // ...
} 

Будет такой граф вызовов (call graph):

И это снова попадает под правило "green red", только логически обозначает потенциальное замедление.

Не red и green, а вообще любое

На самом деле мы использовали красные и зелёные только для визуализации «красные медленные, зелёные быстрые». Но можно использовать непосредственно slow и fast:

/** @color slow */
function appendLogToTable() {}

/** @color fast */
function handleRequest() {}

И правило в палитре:

fast slow: "potential performance leak"

Любое свойство — это цвет:

  • @color slow, @color fast, @color performance-critical;

  • @color server-side-rendering, или просто @color ssr;

  • @color view и @color controller;

  • @color db-access;

  • …и вообще что угодно.

У функции может быть одновременно несколько цветов. Например, api-entrypoint и slow.

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

controller model: "controllers may not depend on models"

Когда проаннотируем в коде все соответствующие классы и функции как @color controller и @color model, у нас будут проверки на это правило — на любом уровне вложенности.

Другие примеры:

ssr db: "don't fetch data from server-side rendering"

api-entrypoint curl-request: "performing curl requests on production is bad practice"

green yellow red: "a strange semaphore in your code"

Окей, запретить запретили, а как точечно разрешать?

Что, если хочется сказать »да, ходить в базу из SSR нельзя, но вот здесь, пожалуйста-пожалуйста, можно»? Или »это точно не скажется на производительности, потому что скрыто за if (debug)»?

Или вернёмся к коллграфу выше: как разрешить Logger::debug(), несмотря на правило "fast slow"?

Цвета и палитра — примерно как HTML и CSS. Не на 100% корректная аналогия, но позволяет понять, как описывать исключения.

Представьте, что каждая функция — это div’ник. Вызовы по коллграфу — это вложенность. А цвета — классы.

d02b1d205fa2a7ba83ecc4ce456e09a1.png

Если бы мы описывали правило «performance leak» в CSS, то писали бы так:

.fast .slow {
    error-text: "potential performance leak";
}

Конечно, CSS-правила могут быть любой вложенности, типа ".red .green .blue". Главное, что есть более специфичные селекторы. На этом и основана вся вёрстка:

.green .red {
    error-text: "don't call red from green";
}

.green .yellow .red {
    error-text: "a strange semaphore in your code";
}

Это очень привычно для всех, кто знаком с CSS: если есть жёлтая функция между зелёной и красной, срабатывает более специфичное правило. Или…

.green .yellow .red {
    error-text: none;
}

Якобы используя CSS, убрали ошибку для этого селектора. Ну вы же догадываетесь, да?

.fast .slow-ignore .slow {
    error-text: none;
}

Цвета работают точно так же. Можно описывать более специфичные смешивания в палитре, которые перегружают или отменяют ошибки.

- fast slow: "potential performance leak"
- fast slow-ignore slow: ""

Тогда как разрешить Logger::debug() из fast-функций? Имея то правило в палитре, просто пометить:

/** @color slow-ignore */
function debug() { /* ... */ } 

Другой пример. Из server-side rendering загружать данные из БД нельзя. Но если специальным цветом пометить функцию, то можно:

- ssr db: "don't fetch data from server-side rendering"
- ssr allow-db db: ""

Нужно понимать, что должны проверяться все достижимые пути. К примеру, если есть другой путь по коллграфу из f() в h():

Первый путь — окей, а второй — это ошибка "f → g2 → h => don't fetch data from server-side rendering".

Неймспейсы — это тоже цвета?

Цвет может быть и над классом. Это означает, будто этот цвет написан над каждым методом.

/**
 * @color low-level
 */
class ConnectionCache { /* ... */ }

И поскольку цвета — это произвольные строки, то неймспейсы — чем не цвета? Тут они в явном виде, но при желании могут быть и неявными:

/**
 * @color VKApi\Handlers
 */
class AuthHandler { /* ... */ }

/**
 * @color VKDesktop\Templates
 */
function tplPostTitle($post) { /* ... */ } 

Тогда можно писать любые правила на неймспейсах и исключения для них.

VKApi\Handlers VKDesktop\Templates: "using UI from api is strange"

И это тоже будет работать на любом уровне вложенности.

Можно даже эмулировать internal в языках, где его нет

Например, в PHP все классы публичные, поэтому из любой точки кода можно вызвать любой публичный метод. Даже если разработчик хочет упрятать внутри монолита какой-то класс глубоко в легаси-код, чтобы его вызывать только через обёртку, на уровне языка нет никаких ограничений на это.

namespace DBLayer\Internals;

// допустим, это какие-то внутренние утилиты
class LegacyIDMapper {
    static function mapUserId($user_id) { /* ... */ }
}
// и у вас есть высокоуровневая обёртка как публичный интерфейс
function transformLegacyUser(User $user) {
    $user->id = LegacyIDMapper::mapUserId($user->id);
    // ...
}

Даже если вы в Confluence напишете «Не используйте LegacyIDMapper, плиз!», его всё равно кто-то заиспользует, поскольку не запрещено языком.

А вот с цветами это решается действительно красиво. Сделаем два цвета:

/**
 * @color db-internals
 */
class LegacyIDMapper { /* ... */ }

/**
 * @color db-public
 */
function transformLegacyUser(User $user) { /* ... */ }  

И такие два правила в палитру:

- db-internals: "don't access db implementation layer directly"
- db-public db-internals: ""

И всё! Прямые вызовы из внешнего кода будут матчиться в первое правило и давать ошибку, а вызовы через обёртку — во второе. И если хочется из сторонней функции fff() всё-таки вызвать легаси, можно даже добавить правило:

- please-allow-legacy-here db-internals: "" 

Теперь можно проаннотировать fff() таким цветом — явно обозначая своё намерение и оставляя комментарий в коде. На код-ревью это тоже будет видно.

Использование на практике и выводы

Концепцию цветных функций я изначально придумал для KPHP, чтобы мы внутри монолита ВКонтакте могли изолировать участки кода и описывать правила вроде «лента не должна лезть в сообщения». Но идея и в целом получилась красивая, поэтому помимо решения для KPHP мы сделали отдельный инструмент и выложили его в open source под названием nocolor.

Как я говорил в начале, язык здесь не важен. Важен концепт. Даже имплементация не играет особой роли. К примеру, мы отслеживаем все коллграфы на этапе компиляции и без рантайм-оверхеда, но это может быть и рантайм-стек в виде хуков в начале и конце цветной функции.

Идею можно развивать. Например, помимо цвета ввести насыщенность (saturation). Мол, не просто медленная, а медленная в такой-то степени. Например, для медленности мерой насыщенности может служить 95-й перцентиль исполнения согласно профилированию. Тогда не только цвета смешиваются, но ещё и интенсивности складываются. А через палитру мы триггерим ошибку — если, условно, потенциальный путь исполнения дольше трёх секунд.

Кстати, на прошлом PHP Russia я выступал с докладом, где рассказывал про всё это и делился подробностями реализации. А ещё объяснял, почему это всё-таки называется «цвет» и никак иначе. Посмотреть доклад можно по ссылке ниже.

Полезные ресурсы

© Habrahabr.ru