Механизми в програмирането: пиши логиката веднъж, преизползвай я навсякъде
- Каква е основната идея зад механизмите
- Прост пример: механизъм за валидация
- Една структура, много имплементации
- Патерн като механизъм: фабрика за обекти
- Механизъм за обработка на последователни стъпки
- Какво още може да прави един зрял механизъм
- Pipeline: комбиниране на механизми в логически структури
- Заключение
Преизползваемият код е една от най-търсените цели в разработката на софтуер. Когато една и съща логика се копира от проект в проект и от файл във файл, поддръжката става скъпа, а грешките се размножават. В тази статия ще разгледаме един прост, но мощен подход — механизмите — който позволява да отделим „сценария" на изпълнение от конкретните детайли и да го пишем само веднъж.
Ще видим как с помощта на параметри и callback функции можем да капсулираме поведение, да изграждаме преизползваеми абстракции и дори да реализираме класически дизайн патерни без да дублираме структура. Накрая ще покажем как механизмите се комбинират в pipeline архитектура.
Каква е основната идея зад механизмите
Представете си, че описвате общия поток на дадена операция по абстрактен начин, така че да може да се прилага в безброй ситуации. Кодът, който върши самите стъпки, живее на едно място и не знае нищо за конкретиката. А конкретиката — какво точно се прави, върху какви данни и с какви правила — се подава отвън като набор от стойности и функции.
С две думи: механизмът е „двигателят", който знае как да изпълни даден сценарий. Имплементацията описва какво точно да се изпълни. Двигателят се пише веднъж и се преизползва; имплементацията се сменя според задачата.
Механизмът никога не се променя, когато се появи нова задача. Променя се само имплементацията, която той получава.
Прост пример: механизъм за валидация
Да започнем с нещо познато — проверка на входни данни. Вместо да пишем нова валидираща функция за всеки екран, ще създадем един „двигател", който приема структура от правила и сам обхожда полетата.
class ValidatorEngine {
// Структурата, която всяка имплементация попълва
blueprint = {
rules: {}, // правила за всяко поле
messages: {}, // съобщения при провалено правило
prepare: (value) => value, // обработка на стойността преди проверка
};
input = {}; // данните, които валидираме
result = {
valid: true,
errors: {},
};
constructor(blueprint, input) {
this.blueprint = blueprint;
this.input = input;
}
run() {
for (const field in this.blueprint.rules) {
const value = this.blueprint.prepare(this.input[field]);
const checks = this.blueprint.rules[field];
for (const checkName in checks) {
const passed = checks[checkName](value, this.input);
if (!passed) {
this.result.valid = false;
this.result.errors[field] =
this.blueprint.messages[field]?.[checkName] ?? 'Невалидна стойност';
break;
}
}
}
return this.result;
}
}
Сега описваме конкретиката — правилата за регистрационна форма. Забележете, че тук няма никаква логика за обхождане, само декларации.
const registrationBlueprint = {
prepare: (value) => (typeof value === 'string' ? value.trim() : value),
rules: {
email: {
required: (v) => !!v,
isEmail: (v) => /.+@.+..+/.test(v),
},
password: {
required: (v) => !!v,
minLength: (v) => v.length >= 8,
},
username: {
required: (v) => !!v,
noSpaces: (v) => !/s/.test(v),
},
},
messages: {
email: {
required: 'Имейлът е задължителен',
isEmail: 'Невалиден имейл адрес',
},
password: {
required: 'Паролата е задължителна',
minLength: 'Паролата трябва да е поне 8 символа',
},
username: {
required: 'Потребителското име е задължително',
noSpaces: 'Без интервали в потребителското име',
},
},
};
А ето и употребата — кратка и предвидима:
const validator = new ValidatorEngine(registrationBlueprint, {
email: 'ivan@example.com',
password: '12345',
username: 'ivan petrov',
});
const result = validator.run();
console.log(result.valid); // false
console.log(result.errors); // { password: '...', username: '...' }
Една структура, много имплементации
Същинската сила се вижда, когато ни потрябва втора валидация. Не пипаме нито ред от двигателя — просто описваме нова имплементация със същата структура.
const productBlueprint = {
prepare: (value) => value,
rules: {
title: {
required: (v) => !!v,
maxLength: (v) => v.length v !== undefined && v !== null,
positive: (v) => v > 0,
},
},
messages: {
title: {
required: 'Заглавието е задължително',
maxLength: 'Заглавието е твърде дълго',
},
price: {
required: 'Цената е задължителна',
positive: 'Цената трябва да е положителна',
},
},
};
Една и съща структура от ключове и един и същ тип callback функции. Различни са само имената на полетата и съдържанието на функциите. Това е сърцевината на идеята — предсказуема форма, сменяемо съдържание.
Патерн като механизъм: фабрика за обекти
Почти всеки дизайн патерн може да се опише като механизъм. Така отпада нуждата всяка нова реализация да следва определен набор от файлове и класове — двигателят се пише еднократно. Ето как изглежда една фабрика.
class FactoryEngine {
blueprint = {
creators: {}, // тип -> функция, която създава обекта
fallback: () => null, // какво да правим при непознат тип
};
constructor(blueprint) {
this.blueprint = blueprint;
}
make(type, params = {}) {
const creator = this.blueprint.creators[type] ?? this.blueprint.fallback;
return creator(params);
}
}
Имплементацията решава кои типове съществуват и как се конструира всеки от тях:
const notificationFactory = {
creators: {
email: (params) => new EmailNotification(params.to, params.subject),
sms: (params) => new SmsNotification(params.phone),
push: (params) => new PushNotification(params.deviceId),
},
fallback: () => {
throw new Error('Непознат тип нотификация');
},
};
const factory = new FactoryEngine(notificationFactory);
const mail = factory.make('email', { to: 'user@webbuild.bg', subject: 'Здравей' });
const sms = factory.make('sms', { phone: '+359893557642' });
Механизъм за обработка на последователни стъпки
Сега нещо по-интересно — двигател, който изпълнява поредица от стъпки върху общ контекст. Идеален е за сценарии с много действия, които трябва да текат в строг ред.
class PipelineEngine {
blueprint = {
context: () => ({}), // начално състояние, споделено между стъпките
steps: [], // масив от стъпки
};
input = {};
output = {};
constructor(blueprint, input) {
this.blueprint = blueprint;
this.input = input;
}
execute() {
const ctx = this.blueprint.context();
for (const step of this.blueprint.steps) {
step(ctx, this.input, this.output);
}
return this.output;
}
}
Имплементация за публикуване на статия — всяка стъпка чете и записва нещо в контекста:
const publishArticle = {
context: () => ({
draft: null,
author: null,
html: '',
}),
steps: [
(ctx, input) => {
ctx.draft = DraftRepository.find(input.draftId);
},
(ctx) => {
ctx.author = AuthorRepository.find(ctx.draft.authorId);
},
(ctx) => {
ctx.html = Markdown.toHtml(ctx.draft.body);
},
(ctx) => {
ArticleRepository.publish({
title: ctx.draft.title,
html: ctx.html,
authorId: ctx.author.id,
});
},
(ctx) => {
Newsletter.notifySubscribers(ctx.author, ctx.draft.title);
},
(ctx, input, output) => {
output.published = true;
output.slug = ctx.draft.slug;
},
],
};
const pipeline = new PipelineEngine(publishArticle, { draftId: 42 });
const output = pipeline.execute();
Стъпките остават семантично разделени, нови могат да се вмъкват без да чупят съседните, а целият сценарий е лесен за четене отгоре надолу.
Какво още може да прави един зрял механизъм
Това е само опростена версия. Около един такъв двигател може да се изгради сериозна инфраструктура, която след това работи автоматично за всяка имплементация:
- дефиниране и типизиране на контекста;
- валидиране на входа и изхода преди и след изпълнение;
- декларация какво всяка стъпка има право да чете и записва;
- проверка дали стъпката наистина е попълнила очакваните данни;
- метод
executeи метод за компенсация (rollback) — ако дадена стъпка се провали, механизмът връща изпълнените назад в обратен ред; - автоматичен лог за всяка стъпка с входни и изходни стойности;
- дебъгер и визуализатор — понеже двигателят познава всички стъпки, той може да показва състоянието им наживо.
Хубавото е, че всяка от тези функционалности се пише веднъж върху механизма и веднага започва да работи за всички бъдещи имплементации — без допълнителни усилия.
Pipeline: комбиниране на механизми в логически структури
Когато имаме няколко вида механизми, можем да ги навържем в по-голяма архитектура. Достатъчни са три основни градивни блока:
- Sequence — поредица от стъпки в общ контекст (нашият PipelineEngine);
- Branch — разклонение, което избира кой блок да се изпълни според условие;
- Bridge — преходник, който взима изхода на един блок и го превръща във вход за следващия.
[ Sequence ] --> [ Bridge ] --> [ Branch ] --> [ Sequence A ]
--> [ Sequence B ]
Всеки блок е самостоятелна имплементация на съответния механизъм. Затова блоковете могат да се разкачат, разместват и заменят независимо един от друг — без да докосваме кода на останалите.
Заключение
Механизмите са лесен начин да отделим повтарящия се поток от изпълнение от конкретните детайли на всяка задача. Веднъж написан, двигателят се преизползва безброй пъти, а имплементацията остава с ясна, предсказуема структура. Този подход прави кода по-четим, по-лесен за тестване и значително по-устойчив на промени — а в комбинация с pipeline архитектура дава гъвкавост, която трудно се постига с класическо дублиране на логика.
Коментари за „Механизми в програмирането: пиши логиката веднъж, преизползвай я навсякъде“
Все още няма коментари. Бъдете първият, който ще коментира тази статия.