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
- Změna implementace za bariérou nevyžaduje změnu na druhé straně.
- Testování jednotlivých vrstev je izolované, přehledné a rozumné.
- Jasná odpovědnost — každá vrstva řeší svůj problém.
- Minimalizace chyb. Chyby prostě nemohou nastat, nebo jen izolovaně.
Nevýhody
- Vyžaduje disciplínu při návrhu kontraktů — špatně navržené rozhraní se pak těžko mění.
- Více vrstev = více indirekcí, které musí nový člen týmu pochopit.
- Riziko over-engineeringu u jednoduchých systémů.
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
- Čistá separace — Core je triviálně testovatelný s mock strategií.
- Nová varianta chování = nová třída, žádná změna existujícího kódu (OCP).
- Strategie lze kombinovat a znovu použít napříč kontexty.
Nevýhody
- Proliferace tříd — u mnoha variant chování vzniká mnoho malých tříd.
- Pokud se interface strategie musí změnit, zasahuje to všechny implementace.
- Nová featura přináší vydání nového balíčku.
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
- Maximální flexibilita — každý klient si poskládá přesně to, co potřebuje.
- Každý kousek testuju samostatně, kombinace jsou předvídatelné.
- Sdílený kód zůstane malý a stabilní.
Nevýhody
- Pořadí dekorátorů může záležet — a to se špatně dokumentuje i testuje.
- Složité kombinace se obtížně debugují ("který dekorátor to změnil?").
- Vytváření správné kompozice musí někdo orchestrovat — logika konfigurace se někam přesune, ne zmizí.
- Nakonec zjistíte, že malý sdílený kód není výhoda.
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
- Změna chování klienta = změna konfigurace, ne deployment.
- Ify v kódu jsou generické, netýkají se konkrétního klienta.
- Lze kombinovat s A/B testováním nebo postupným rolloutem.
Nevýhody
- Ify z kódu úplně nezmizí — jen jsou obecnější. Při mnoha flagách kód stejně zhoustne.
- Neaktivní kombinace featur se časem stávají mrtvým kódem, který nikdo nesmaže.
- Chybějící flag v konfiguraci může způsobit tiché selhání nebo neočekávané výchozí chování.
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
- Nový klient = nová data v DB + případně nové rutiny. Žádná změna Core, žádný deployment.
- Změna pravidel klienta = změna dat. Dá se dělat za běhu.
- Testování: Core testuju na mock pravidlech. Rutiny testuju izolovaně. Nic se neprolíná.
- Nejsilnější oddělení ze všech přístupů — klientova logika je doslova mimo kód.
- Dá se dokonce zjišťovat, které featury se již nepoužívají.
Nevýhody
- Expression language se může stát neudržitelným skriptem — nutná disciplína v tom, co patří do výrazů a co do rutin.
- Debuggování chyby v DB výrazu je výrazně těžší než v kódu — chybí stack trace, IDE podpora, typová kontrola.
- Evaluace výrazů z DB musí být sandboxovaná, jinak je to bezpečnostní díra.
- Vysoká vstupní investice — smysluplné až od určitého počtu klientů nebo složitosti pravidel.
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.