Navrhování bariér při softwarovém vývoji

Problém, který bariéry řeší

Jakmile systém roste a sdílený kód začínají používat různí klienti, každý chce trochu jiné chování. Naivní řešení je přidávat if (client === 'A') přímo do sdíleného kódu. To funguje chvíli, pak se to ale stane natolik komplexní, že se utestujete k smrti.

Kombinatorická exploze testů je symptom. Příčina je, že sdílený kód ví příliš mnoho o svých konzumentech.

Klientovi vadilo, že každá featura trvá strašně dlouho, a je drahá a náročná (nebo náročná a drahá?).

Zásada: Cílem je, aby přidání nového klienta nevyžadovalo žádnou změnu sdíleného kódu — jen novou implementaci rozhraní nebo novou konfiguraci.

Princip bariéry

Části a vrstvy systému poskytují API, ale nesmí na sobě jinak záviset. Existují dvě složky:

Informační bariéra — vrstva neví, co je za bariérou. Zná jen kontrakt, ne implementaci. Přímo navazuje na Parnasův princip Information Hiding (1972).

Implementační bariéra — implementace se může libovolně změnit, aniž to kdokoli na druhé straně pocítí. Závisí se na kontraktu, ne na konkrétním kódu.

Klient A Klient B │ │ Adapter A Adapter B │ │ └────────┬────────┘ │ ══ BARIÉRA ══ │ Core / Shared │ ══ BARIÉRA ══ │ Infrastruktura

Core o klientech vůbec neví. Klienti o sobě navzájem neví. Každý závisí jen na kontraktu vrstvy pod sebou.

Výhody

Nevýhody

Konkrétní implementační přístupy

Strategy Pattern

Místo ifů předáš chování zvenku. Core definuje interface, každý klient dodá vlastní implementaci.

// Core — žádné ify, žádná zmínka o klientech public class OrderProcessor { private final PricingStrategy pricing; public OrderProcessor(PricingStrategy pricing) { this.pricing = pricing; } public Order process(Order order) { BigDecimal price = pricing.calculate(order); // ... return order; } } // Každý klient za bariérou OrderProcessor processorA = new OrderProcessor(new DiscountPricingStrategy()); OrderProcessor processorB = new OrderProcessor(new PremiumPricingStrategy());

Výhody

Nevýhody

Composition

Sdílený kód rozbiješ na malé testovatelné kousky. Klienti si složí to, co potřebují.

// Sdílené stavební bloky public interface OrderDecorator { Order apply(Order order); } public class DiscountDecorator implements OrderDecorator { ... } public class EuropeVatDecorator implements OrderDecorator { ... } public class SubscriptionDecorator implements OrderDecorator { ... } // Klient A — vlastní kombinace OrderService serviceA = new ComposedOrderService( baseOrderService, new DiscountDecorator(), new EuropeVatDecorator() ); // Klient B — jiná kombinace, žádná změna sdíleného kódu OrderService serviceB = new ComposedOrderService( baseOrderService, new SubscriptionDecorator() );

Výhody

Nevýhody

Feature Flags jako konfigurace

Místo if (clientId.equals("X")) pracuješ s feature profilem načteným z konfigurace. Profil klienta je data, ne kód. Testovat pak stačí kombinace featur, ne kombinace klientů.

// Špatně — core ví o konkrétním klientovi if (client.equals("enterprise")) { applyAdvancedReporting(); } // Dobře — core ví jen o featurách if (features.isEnabled("advancedReporting")) { applyAdvancedReporting(); }

Výhody

Nevýhody

Datově řízená customizace — expression engine

Nejsilnější varianta. Veškerá customizace je data v DB — podmínky, výrazy, pravidla. Core je interpret. Klientova logika je program.

Klient A Klient B │ │ Adapter A Adapter B │ │ └────────┬────────┘ │ ══ BARIÉRA ══ │ Core Engine ┌────┴──────────┐ Expression Client Evaluator Routines │ ══ BARIÉRA ══ │ DB: pravidla, výrazy

Pravidla v DB

{ "clientId": "A", "rules": [ { "condition": "order.volume > 1000", "expression": "price * 0.85", "priority": 1 }, { "condition": "order.country == 'DE'", "expression": "applyVatRoutine(price, 'DE')", "priority": 2 } ] }

Vazba na speciální rutiny

Expression language sám nestačí (stačil by, ale není to dobrý nápad) na komplexní logiku. Proto registr rutin — kód registrovaný pro konkrétního klienta, který engine zavolá přes jméno. Engine stále neví, co uvnitř rutiny je.

public class RuleEngine { private final RoutineRegistry registry; private final ExpressionEvaluator evaluator; public BigDecimal calculate(Order order, List<Rule> rules) { BigDecimal price = order.getBasePrice(); for (Rule rule : rules) { if (evaluator.test(rule.getCondition(), order)) { price = evaluator.apply(rule.getExpression(), price, order, registry); } } return price; } } // Rutiny pro klienta A — za bariérou, engine je nezná registry.register("clientA", "applyVatRoutine", (price, country) -> vatService.calculate(price, country)); registry.register("clientA", "loyaltyDiscount", (price, customer) -> loyaltyService.apply(price, customer));

Kde co žije

Vrstva Co obsahuje
DB Podmínky, výrazy, priority, odkazy na rutiny
Rutiny Komplexní logika, kterou expression language nemá, nebo nedokáže řešit.
core Engine Vyhodnocení, orchestrace — žádná klientská logika
Adapter Načtení správných pravidel pro daného klienta

Pozor Expression language se může stát neudržitelným skriptem — disciplína v tom, co patří do výrazů a co do rutin, je nutná. Debuggování chyby v DB výrazu je těžší než chyby v kódu. Evaluace výrazů z DB musí být sandboxovaná kvůli bezpečnosti (což je ale další výhoda). Výrazy by neměly být příliš rozsáhlé.

Výhody

Nevýhody

Klíčová otázka při návrhu

Patří tato logika do Core, nebo je to klientova zodpovědnost za bariérou?

Jakmile si tuhle otázku pokládáš důsledně, if (client === 'X') přirozeně mizí. Odpověď je totiž vždy: klientova zodpovědnost.

Sdílený kód popisuje co se děje. Klienti definují jak. Bariéra je místo, kde se tyto dvě věci oddělují.

Core engine je interpret. Klientova logika je program. Bariéra odděluje interpret od programu — a program může být jak data, tak kód.

Také píšou