Към съдържанието
wannacode

ADR: записани архитектурни решения

Накратко: ADR (architecture decision record) е кратък документ с контекста, вариантите, решението и последиците му; старото решение не се пренаписва, а се заменя с ново.

Проблем

Защо сме с опашка?

Нов колега отваря кода на плащането. Известията минават през опашка, имейлът понякога идва с минута по-късно, а потребителят му трябва да хвърля дубликати. „Това е сложно. Защо просто не викаме имейла директно?“

Никой не помни. Който е решил, вече не е във фирмата. Обсъждането е било в чат, който отдавна е изтрит. Остават два лоши избора: да пазиш нещо, което не разбираш, или да го махнеш и на следващия Black Friday да настъпиш същата грабла, заради която е сложено.

Идеята

ADR: решението и защо

ADR (architecture decision record, запис на архитектурно решение) е кратък документ за едно решение. Пише се, когато решението се взима, и стои в хранилището до кода (docs/adr/0007-notifications-queue.md). Има шест части:

  • заглавие и номер: какво е решено, с пореден номер;
  • статус: предложено, прието, заменено;
  • контекст: какво е вярно сега, кое боли и кое ограничава;
  • варианти: поне два, които наистина са обмислени;
  • решение: кой вариант и защо;
  • последици: какво става по-лесно и какво става по-трудно.

Ето ADR-ът, който колегата е търсел:

# ADR-0007: Известията минават през опашка
## Статус
Прието. Заменя ADR-0004.
## Контекст
ADR-0004: плащането вика имейла директно. На Black Friday доставчикът
на имейли отговаряше за 8 секунди и плащанията изтичаха по timeout.
## Варианти
1. Директно извикване с кратък timeout: плащането минава, имейлът се губи.
2. Опашка: плащането пуска събитие, известията го четат по-късно.
## Решение
Опашка. Загубен имейл за платена поръчка е по-лошо от закъснял.
## Последици
- Плащането не чака имейла.
- Имейлът може да дойде с минута по-късно.
- Доставката е „поне веднъж“, затова потребителят е идемпотентен.
- Още една част за наблюдение: опашката.

Последната половина е най-ценната: тя казва на колегата защо има дубликати и какво ще счупи, ако махне опашката. Опашката и идемпотентният потребител са от урока за събитията.

Идеята

Статусите и смяната на решение

ADR минава през няколко статуса:

  • предложено (proposed): написано е, обсъжда се, обикновено в pull request;
  • прието (accepted): от днес това е правилото;
  • заменено (superseded): по-ново ADR го смени, а старото сочи към него;
  • отменено (deprecated): вече не важи, без заместник.

Приетото ADR не се пренаписва. Когато решението вече не е вярно, пишеш ново ADR, което заменя старото, а на старото слагаш статус „заменено от ADR-0007“. Старото е било вярно в своя контекст: ако го изтриеш, губиш защо е било избрано и какво се е случило после.

Виж го

Решение, последици, ново решение

static readonly HashSet<string> Statuses =
["proposed", "accepted", "superseded", "deprecated"];
static List<string> Check(List<Adr> adrs)
{
var all = ByNumber(adrs);
var bad = new List<string>();
foreach (var a in adrs)
{
var n = a.Name;
if (!Statuses.Contains(a.Status))
bad.Add($"{n}: непознат статус {a.Status}");
if (a.Context == "") bad.Add($"{n}: няма контекст");
if (a.Options.Count < 2) bad.Add($"{n}: по-малко от два варианта");
if (a.Decision == "") bad.Add($"{n}: няма решение");
if (a.Consequences == "")
bad.Add($"{n}: няма последици");
if (a.Status == "superseded")
{
var next = a.SupersededBy is int k
? all.GetValueOrDefault(k) : null;
if (next is null)
bad.Add($"{n}: заменено, но не казва с кое");
else if (next.Status != "accepted")
bad.Add($"{n} → {next.Name}: заместникът не е приет");
}
}
return bad;
}
Стъпка по стъпка, с думи
  1. ADR-0004 е предложено: контекст, два варианта и решение. Плащането вика имейла директно.
  2. Екипът го приема. Записана е и цената: бавният имейл бави плащането.
  3. Black Friday: доставчикът на имейли отговаря за 8 секунди и плащанията изтичат. Записаната последица се сбъдва.
  4. Ново решение, ADR-0007: известията минават през опашка. Контекстът му сочи към ADR-0004.
  5. Вариантите и цената: плащането не чака, но имейлът може да дойде по-късно и два пъти.
  6. ADR-0007 е прието. Старото не се трие и не се пренаписва: статусът му става „заменено“ и сочи към новото.
  7. Проверката от урока: замененото ADR трябва да сочи към прието. ADR-0004 → ADR-0007 минава.
  8. Който пита „защо опашка?“, чете ADR-0007, а оттам и ADR-0004. Историята остава, с вариантите и цената.

Виж го

Проверка на ADR-ите

В истински проект ADR-ите са markdown файлове, а проверката чете заглавията им. Тук всяко ADR е запис със същите полета, за да е програмата кратка:

record Adr(int Number, string Title, string Status,
string Context, List<string> Options, string Decision,
string Consequences, int? SupersededBy)
{
public string Name => $"ADR-{Number:D4}";
}
var adr4 = new Adr(4,
"Известията се пращат синхронно",
"superseded",
"Първата версия: имейлът е бърз",
["директно извикване", "опашка"],
"Плащането вика имейла директно",
"Просто е, но бавният имейл бави плащането",
7);
var adr7 = new Adr(7,
"Известията минават през опашка",
"accepted",
"ADR-0004: имейлът бави плащанията",
["директно с timeout", "опашка"],
"Плащането пуска събитие в опашката",
"Не чака; имейлът може да дойде два пъти",
null);

Проверката пази двете неща, които хората забравят: частите на ADR и веригата „заменено от“.

static Dictionary<int, Adr> ByNumber(List<Adr> adrs) =>
adrs.ToDictionary(a => a.Number);
static readonly HashSet<string> Statuses =
["proposed", "accepted", "superseded", "deprecated"];
static List<string> Check(List<Adr> adrs)
{
var all = ByNumber(adrs);
var bad = new List<string>();
foreach (var a in adrs)
{
var n = a.Name;
if (!Statuses.Contains(a.Status))
bad.Add($"{n}: непознат статус {a.Status}");
if (a.Context == "") bad.Add($"{n}: няма контекст");
if (a.Options.Count < 2) bad.Add($"{n}: по-малко от два варианта");
if (a.Decision == "") bad.Add($"{n}: няма решение");
if (a.Consequences == "")
bad.Add($"{n}: няма последици");
if (a.Status == "superseded")
{
var next = a.SupersededBy is int k
? all.GetValueOrDefault(k) : null;
if (next is null)
bad.Add($"{n}: заменено, но не казва с кое");
else if (next.Status != "accepted")
bad.Add($"{n} → {next.Name}: заместникът не е приет");
}
}
return bad;
}

ADR-0005 е отбелязано като заменено, но не казва с кое. ADR-0009 е с един вариант и без последици:

var adr5 = new Adr(5, "Отстъпката се смята в контролера", "superseded", "Един екран, едно правило",
["в контролера", "в домейна"], "Правилото е в контролера", "Бързо, но правилото се дублира", null);
var adr9 = new Adr(9, "Кеш на страниците на продуктите", "proposed", "Базата е на 100% в кампания",
["Redis"], "Cache-aside с TTL 5 минути", "", null);
var problems = Check([adr4, adr5, adr7, adr9]);
problems.ForEach(Console.WriteLine);
Console.WriteLine($"{problems.Count} проблема");
Изход
ADR-0005: заменено, но не казва с кое
ADR-0009: по-малко от два варианта
ADR-0009: няма последици
3 проблема

Такава проверка тръгва с тестовете при всеки pull request, и ADR без последици не стига до основния клон.

Пробвай

Предвиди изхода

Проверката от горе получава две ADR-а за отчетите. Какво ще отпечата?

var adr11 = new Adr(11, "Отчетите четат основната база", "superseded", "Отчетите са малко",
["основната", "реплика"], "Отчетите четат основната", "Просто, но товари базата", 12);
var adr12 = new Adr(12, "Отчетите четат реплика", "proposed", "Отчетите бавят плащанията",
["реплика"], "Отчетите четат реплика", "Данните са с минута по-стари", null);
Check([adr11, adr12]).ForEach(Console.WriteLine);
Отговор

Виж го

Защо сме с опашка: по веригата

Колегата тръгва от ADR-0004, което вижда в старите обсъждания, и стига до действащото решение:

static void History(Dictionary<int, Adr> all, int number)
{
var a = all[number];
Console.WriteLine($"{a.Name} {a.Title} [{a.Status}]");
while (a.SupersededBy is int next)
{
a = all[next];
Console.WriteLine($"→ {a.Name} {a.Title} [{a.Status}]");
}
}
// защо известията минават през опашка?
History(ByNumber([adr4, adr7]), 4);
Изход
ADR-0004 Известията се пращат синхронно [superseded]
→ ADR-0007 Известията минават през опашка [accepted]

Две минути четене вместо седмица гадаене. Ако контекстът на ADR-0007 вече не е вярен (имейлите отиват в друга система, която отговаря бързо), колегата пише ADR-0015, което го заменя, и веригата расте.

Пробвай

Намери грешката

Колега предлага ADR за кеша от урока за кеша. Кой ред прави ADR-а безполезен за следващия?

Избери реда с грешката.

Идеята

Кога ADR помага и кога е бюрокрация

  • Помага за решения, които струват скъпо, ако се сменят (опашка, база, граници на модулите, формат на API), и които имат повече от един разумен отговор.
  • Помага, когато се пише преди решението: вариантите и цената излизат наяве, докато още може да се избере друго.
  • Вреди, когато всяка дреболия иска ADR. Името на клас или библиотека, която сменяш за час, не е архитектурно решение.
  • Вреди, когато се пише след това, за да оправдае вече направеното: тогава вариантите са измислени, а последиците са само добри.
  • Безполезно е, когато никой не го намира. Сложи номера на ADR-а в коментара на мястото в кода и в pull request-а.
По-точно, за любопитните

Откъде идва. Форматът е на Майкъл Найгард (Michael Nygard, 2011). Популярни шаблони са неговият и MADR (Markdown Any Decision Records), който добавя „плюсове и минуси на всеки вариант“.

Инструменти. adr-tools прави нов файл с поредния номер и сменя статусите; log4brains прави сайт от ADR-ите. Проверка като тази в урока се пише за един следобед и се пуска с тестовете.

ADR, RFC и design doc. Design doc описва цяла система или голяма промяна, преди да се строи. RFC е предложение за обсъждане от много хора. ADR е едно решение, кратко, и остава като история. Често design doc-ът завършва с няколко ADR-а.

Кой ги пише. Не само архитектът. Който предлага промяната, пише ADR-а в pull request, а прегледът на кода става и преглед на решението.

Идеята

Кога коя форма

Модулът показа девет начина да подредиш система. Никой не е добър сам по себе си: всеки решава една болка и носи цена.

Форма Помага, когато Вреди, когато
Слоеве правилата трябва да се тестват без база и без екран приложението е само форми към таблици и слоевете само препращат
Hexagonal домейнът живее дълго, а плащането и базата се сменят всеки клас има интерфейс без втора реализация
Модулен монолит един или няколко екипа, един deploy границите не се пазят и модулите бъркат в чужди таблици
Услуги независими екипи, различен мащаб, отделен deploy един екип: мрежата, данните и наблюдението струват повече от ползата
Събития и опашки бавна последица не бива да бави главното действие отговорът трябва да е веднага; дубликатите и редът се пазят на ръка
Outbox събитие не бива да се губи при срив няма опашка: извикване в същия процес стига
Договор на API клиенти, които не можеш да обновиш (мобилното приложение) код в един процес, който се мени заедно
Кеш едно и също се чете много и може да е малко старо стойността трябва да е точна или не може да се инвалидира
Мащабиране един сървър не стига или не бива да е единствен един сървър стига, а тясното място е базата

Монолит първо. Нова система започва като модулен монолит с ясни граници. Услуга се отделя, когато има конкретна причина: отделен екип, различен мащаб, независим deploy. Причината се записва в ADR, заедно с цената. Обратното (услуги от първия ден) плаща цената на разпределената система, преди да е ясно къде са истинските граници.

Упражнение

Съдържанието на папката

Реши задачата в редактора си.

Папката docs/adr/ расте и никой не помни кое действа. Напиши Toc: печата всички ADR-и, подредени по номер, със статуса им, а при замененото и номера на заместника. Дай му ADR-0007, ADR-0001, ADR-0009 и ADR-0004 в този ред.

Изход
ADR-0001 Модулен монолит вместо услуги [accepted]
ADR-0004 Известията се пращат синхронно [superseded → ADR-0007]
ADR-0007 Известията минават през опашка [accepted]
ADR-0009 Кеш на страниците на продуктите [proposed]

Още задачи

  • Действащите. Отпечатай само приетите ADR-и: това е „архитектурата днес“.
  • Цикъл. Какво става с history, ако ADR-0004 е заменено от 0007, а 0007 от 0004? Добави проверка, която намира такъв цикъл.

Тест

Провери се

1. Какво НЕ е задължителна част от ADR?
2. Прието ADR вече не е вярно. Какво правиш?
3. В раздел „Последици“ пише само „всичко става по-бързо“. Какъв е проблемът?
4. Кога ADR е излишна бюрокрация?
5. Магазин с един екип от петима започва нова система. С какво да започне?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·