Low dependency přístup při vývoji knihoven a aplikací

Jan Barášek
Oct 13 · 5 min read

Při vývoji knihoven i celých aplikací se vždy snažím používat co nejméně závislostí i za cenu duplikace některých metod. Tento článek popisuje způsoby, jak o softwarovém vývoji přemýšlet trochu na vyšší úrovni a aktivně bojovat s fenoménem Dependency hell.

Na čem všem může být software závislý

Pokud již delší dobu vyvíjíte software, určitě si všímáte, že se většina úkolů stále dokola opakuje. Typicky je potřeba získat nějakým způsobem data od uživatele, zpracovat, uložit a příště zobrazit. Způsobů, jak toto udělat, existují stovky. Pro ty nejčastější úlohy existují hotová řešení (databáze, knihovna programovacího jazyka, případně volitelné rozšíření), když se těchto knihoven nahromadí celá řada a řeší určitý problém jako celek, jedná se o framework.

V internetových diskusích se čas od času vyskytne námitka, že jsou frameworky příliš obrovské kusy balastu a je lepší vyvíjet bez nich, protože vývojář ušetří někdy i desítky MB zdrojového kódu. S tímto přístupem jsem určitý čas také bezvýhradně souhlasil, nicméně nyní se situace změnila a rád bych předvedl pár myšlenek, které kombinují výhody obou řešení.

Low dependency přístup — návrhový vzor, jak snížit počet závislostí a přitom zachovat kód elegantní.

Každý software totiž vždy na něčem závislý. Typicky to je programovací jazyk (a jeho verze), operační systém, nainstalovaná rozšíření, ostatní knihovny a balíky. Pokud si nainstalujete jakýkoli balík, vždy by se měl správce závislostí (v PHP to je například Composer) podívat, jaké další knihovny jsou pro instalaci potřeba a ty rekurzivně doinstalovat. Někdy se může stát, že pro instalaci drobné fičury je potřeba stáhnout desítky balíků, z kterých se používá třeba jen jedna funkce. Tomu se říká Dependency hell. To pravé peklo je znát hlavně ve chvíli, kdy některá ze závislostí nefunguje, nebo není jednoduše k dispozici. Aplikace pak nefunguje, nebo něfunguje jen někdy (horší scénář).

Jak navrhnout low dependency knihovnu / balík

Při vývoji webů ve firmách jsem často řešil otázku, jak si ušetřit práci a neopakovat se. Typicky programátor potřebuje naprogramovat určité části webu jednou (například administraci) a pak ji poskytnout všem klientům. Samozřejmostí je, že se všem starším instalacím pravidelně aktualizuje jádro a díky tomu je web stabilní, proaktivně se řeší bugy a klienti dostávají pravidelné vylepšení, což dost výrazně zlepšuje vztahy.

Dejme tomu, že jsme si veškerý napsaný kód rozdělili do samostatných balíků, které se mezi sebou provázaly prostřednictvím závislostí. Při instalaci libovolného balíku se proto rekurzivně stáhnou i jeho přímé závislosti, proto bude vždy fungovat. Tento přístup je velmi jednoduchý na pochopení, nicméně vzniká několik velmi nepříjemých situací, které jsem musel vyřešit (a někdy to bylo opravdu velmi náročné):

  • Co když balík vyžaduje databázi a potřebuje založit tabulku s relacemi?
  • Jak řešit konfiguraci?
  • Co když potřebuji nějaký statický helper, typicky pro drobnou úpravu řetězce, zjištění IP adresy, …?
  • Jak dynamicky přidávat properties a další vlasnosti?

Protože je každá instalace projektu jiná, vůbec nepřipadá v úvahu cokoli nastavovat ručně. Skvěle se pro toto hodí vlastnosti Nette framework v kombinaci s Doctrine.

Databázové tabulky lze popsat pomocí entit a používat objektovou databázi. Hlavní přínos je v tom, že se při instalaci nového balíku automaticky aktualizuje databázové schéma, díky čemuž se vygenerují neexistující tabulky, sloupce a relace. Proces aktualizace databáze není ve všech případech 100% spolehlivý, nicméně po pár týdnech cviku a robustním návrhu architektury se na problémy v praxi téměř nenaráží. I tak doporučuji zálohovat.

Konfiguraci řeší sám Nette framework prostřednictvím Neon souborů, které mají skvělé vlastnosti a umí se automaticky rekurzivně mergovat. Instanci konkrétní třídy je pak potřeba získávat z DI kontejneru, protože se může na jednotlivých instalacích lišit. Jedná se tedy o mnohem vyšší míru abstrakce, protože nás nezajímá konkrétní implementace, ale spíše obecné rozhraní a jeho vlastnosti.

Helpery je dobré používat přímo z Nette Utils, pokud však jde o něco speciálního, obvykle je lepší implementovat statickou metodu přímo v konkrétním balíku. Čas od času se stane, že je nějaká metoda duplicitní s jiným balíkem, ale to zas tolik nevadí, protože je pořád důležitější zachovat co nejméně závislostí. Navíc funkčnost metody je důležité testovat v kontextu konkrétního balíku a případné bugy odhalí automatické testy, které byly psány v daném kontextu.

Myšlenkové cvičení: Návrh architektury balíku popisující e-shopový produkt

Zkuste si schválně následující myšlenkové cvičení se mnou: V posledním roce jsem řešil co nejlepší implementaci balíku pro e-shopový produkt. Po roce programování jsem konečně přišel na dostatečně kompromisní řešení (stále není ideální), díky kterému bylo potřeba začít o návrhu softwaru přemýšlet trochu jinak a snižovat závislosti.

Dejte tomu, že chcete do databáze ukládat entitu produktu. Je potřeba myslet obzvlášť mazaně a připravit si prostředí i hodně do budoucnosti, protože balík chceme zachovat obecný, aby šel použít napříč všemy typy e-shopů a zároveň byl kompatibilní i s dalšími balíky, které řeší například košík, vyhledávání nebo filtraci v kategorii.

Požadavků na produkt je celá řada a nemohu všechny zveřejnit, zkusme proto jen některé základní:

  • Název, tagy, popisek (u popisku chceme zajistit, aby nešel moc extrémně formátovat a z e-shopu nebyly omalovánky)
  • Cena (často je potřeba řešit DPH, hodně e-shopů řeší více měn a je potřeba respektovat různé ceny pro různé měny ne nutně podle kurzu, některým skupinám zákazníků navíc chcete dát speciální jinou cenu)
  • Varianty svázané relací (prodáváte trička, existují různé barvy a velikosti. Ne vždy existují všechny kombinace a je potřeba to zohlednit. Varianta může mít různý vliv na cenu)
  • Parametry (kapacita uložiště v telefonu, velikost operační paměti, váha a fyzické rozměry, barva, …)

Jako jedno z možných řešení je implementovat Produkt jako Doctrine entitu a z ní dědit podle typu produktu a jednotlivé properties přidat dynamicky. Tento přístup se zdá z pohledu návrhu softwaru nejlepší, nicméně vzniká mnoho problémů:

  • Do databázové tabulky se vygeneruje příliš mnoho sloupců, které se ne vždy použijí, což má extrémní dopad na výkon
  • Ostatní balíky znají jen obecné rozhraní produktu, protože je žádný projektový produkt nezajímá a například šablona neumí speciální atributy vykreslit
  • Vyhledávání prakticky nelze implementovat, protože máte ohromnou kopu netypových dat
  • Pokud potřebujete z jednoho typu produktu udělat jiný, musí se použít typicky discriminator, který se přepíše. V takovém případě je však potřeba doplnit výchozí hodnoty pro povinné atributy, jinak vznikne nevalidní entita. Navíc výchozí hodnoty pro povinné properties často nejsou k dispozici.

Myslím, že jsem našel řešení, jak tento problém řešit, nicméně i tak jsem velmi otevřen diskusi ohledně čistoty návrhu. Velmi důležité je vždy myslet na výkon celého řešení a bezpečnost, aby nedocházelo ke zbytečným chybám při transformaci dat.

Jako vhodné řešení se zdá použití pouze dvojice entit: Product a ProductVariant, přičemž varianta dědí ze základní produktu, proto má k dispozici i veškeré jeho vlastnosti a jen přidává informace o relacích.

Produkt jako takový ukládá jen základní typy a vlastnosti, tedy název (string), popisek (text), tagy (pole stringů), cena (float) a vlastnosti (json). Při načtení produktu z databáze proto není potřeba procházet stovky relací, které pro základní operace s produktem často nejsou potřeba. Například k ceně se přistupuje pomocí metody getPrice(): float a pokud je potřeba změnit chování a výpočet práce s cenami, stačí jen nainstalovat nový balík a v DIC změnit logiku pro vrácení entity s produktem, který přepíše chování getteru. Typicky se v getteru ověřuje existence obecného statického helperu, který nový balík definuje (a nastaví konfiguraci).

A jak řešit vyhledávání? Bohužel díky tomuto přístupu přestane fungovat běžné vyhledávání podle omezujících podmínek přímo v SQL, protože v databázovém sloupci skladujeme velký json s konfigurací produktu. Nyní je potřeba opět změnit přístup a uvědomit si, že hledání není odpovědností produktu, proto se v samostatné komponentě definuje proces, který bude jednou za nastavený čas získávat přehled všech produktů a indexovat je do samostatné vyhledávací databáze (typicky ElasticSearch) a produkt toto nemusí vůbec řešit. Informace o prioritách a významech jednotlivých sloupců lze obvykle získat ze zmergovaného konfiguračního souboru v DIC.

A jak řešíte závislosti na větších projektech Vy? Napište mi.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade