Lekce 8 - Best practices pro vývoj softwaru - Základní praktiky
V minulé lekci, Nejčastější chyby programátorů - Tvorba metod, jsme si ukázali nejčastější chyby a dobré praktiky pro tvorbu metod. Vyzkoušeli jsme si také návrhový vzor Method chaining.
V dnešním tutoriálu kurzu Best practices pro návrh softwaru se budeme věnovat základním praktikám. To jsou obecně známé praktiky v návrhu softwaru, které se během let osvědčily a pokud je budeme znát, ušetří nám spoustu nepříjemností.
Všichni jste se určitě někdy setkali se špatně napsaným softwarem, připomínajícím domeček z karet. Jakákoli změna v takovéto aplikaci je pracná a nebezpečná, protože může způsobit další chyby a ohrozit i budoucnost projektu. Abychom se takovým návrhovým chybám vyvarovali, nemusíme vynalézat kolo, stačí se poučit ze známých chyb ostatních a předejít jim. Dobrých praktik existuje pro návrh softwaru poměrně velké množství, my si dnes představíme ty nejzákladnější.
KISS
Začněme těmi nejjednoduššími a postupně přejdeme ke komplexnějším poučkám. Pravidlo KISS je akronym pro:
Keep It Simple, stupid!
Česky tedy:
Dělej to jednoduše, hlupáku!
KISS naznačuje, že často existuje řešení, které je jednoduché, relativně málo pracné a přinese uspokojivý výsledek. Tendenci ke komplikování aplikací mají zejména zákazníci, kteří nerozumí vnitřní struktuře aplikací a požadavky si vymýšlejí jak je to napadne. Bývá dobrým zvykem prodiskutovat nutnost nebo podobu alespoň některých požadavků. Často zjistíte, že zákazník vlastně potřebuje něco jiného, co umíte vyřešit elegantněji.
Příklad
Jako příklad si uveďme např. soukromé zprávy zde na ITnetwork. V době jejich programování měl Facebook u svých soukromých zpráv nějaký javascriptový mechanismus pro načítání dalších zpráv směrem nahoru a udržování formuláře pro psaní nové zprávy dole pod nimi. Zeptali jsme se sami sebe, zda toto opravdu potřebujeme programovat a záhy nás napadlo otočit pořadí zpráv. Formulář na novou zprávu byl nahoře a pro načítání nových zpráv se použil již existující skript, který načítá data AJAXem směrem dolů. Najednou jsme získali stejnou funkcionalitu za zanedbatelný vývojový čas, jen proto, že se změnil směr řazení. Toto jsou přesně situace, kdy se vyplatí promluvit se zákazníkem, pokud neprogramujete software pro sebe, případně zadání ještě zvážit.
Řešení soukromých zpráv od Facebooku
Řešení soukromých zpráv od ITnetwork
Z vlastní zkušenosti mohu potvrdit, že software je stále složitější a složitější, časem budete zjišťovat, že ve své aplikaci potřebujete další a další funkčnosti. Když je udržíte jednoduché, získáte konkurenční výhodu nad firmami, které vše bastlí přesně podle představy někoho, kdo IT nerozumí, a ve svém kódu se již téměř nevyznají.
SRP
Single Responsibility Principle, zkráceně SRP, říká, že každý kus kódu, např. třída, by měl být zodpovědný za jednu konkrétní věc. Když složíme aplikaci ze součástí, kdy se každá součást zaměřuje na jednu funkčnost a tu dělá dobře, máme poměrně vysokou šanci na úspěch.
SRP úzce souvisí s přidělením odpovědnosti. Tomu se do hloubky věnuje skupina návrhových vzorů GRASP.
Příklad
Praktikování SRP je např. tvorba manažerů pro různé entity v aplikaci,
svůj kód rozdělujeme mezi třídy CustomerManager
,
InvoiceManager
, StockManager
a podobně. Každá
třída je odpovědná za své entity. Nemáme jen jeden
Manager
.
SRP můžeme praktikovat i na metody, i když to není základní princip této poučky. Každá metoda by měla provádět jednu věc a její činnost bychom měli být schopni popsat bez spojky "a". Pokud si odpovíme něco ve smyslu "Tato metoda načte, vyfiltruje a zobrazí výsledky", měla by být metoda rozdělena na více metod.
SoC
Separation of Concerns, česky Rozdělení zájmu, je princip podobný SRP. Zde se ovšem zpravidla zaměřujeme na širší oblasti. Zatím, co SRP odděluje např. práci s fakturami od práce se zákazníky, SoC odděluje typicky aplikační logiku od prezentace nebo definici dat od další logiky. To znamená, že logické operace by měly být soustředěné v jiné části aplikace, než např. výpis dat uživateli. Určitě víte, že se hovoří o rozdělení aplikace do vrstev, o kterém si můžete blíže přečíst v kurzu Softwarové architektury a depencency injection. Známé architektury jako MVC, MVP, MVVM a podobně jsou vše implementace SoC.
Příklad - Míchání logiky s definicí dat
Nikdy nevkládáme rozsáhlou definici dat (což může být seznam všech PSČ v ČR) do třídy, která dělá nějakou logiku (např. tiskne štítek s adresou na tiskárnu). Když máme třídu, která na stovkách řádků definuje různá PSČ:
Map<String, String> seznamPsc = Map.of( "Praha 1", "10000", "Praha 10", "11000", ... ... ... );
Její odpovědností je očividně definovat data a už by neměla dělat nic jiného. Rozdíl by byl, kdyby třída tyto informace jen načítala krátkým kódem z databáze, pak může klidně dělat i další věci související s adresami.
DRY
Je jeden z nejdůležitějších principů v programování.
Don't Repeat Yourself
Česky pak:
Neopakujte se
Zejména začátečníci mají tendenci kopírovat kód z jednoho místa programu na druhý.
Jakmile se ve vaší aplikaci vyskytují kdekoli 2 stejné bloky kódu nebo třeba i 2 podobné kusy kódu, je automaticky špatně.
Příklad
Tato chyba je tak častá, že využijeme část jedné hry, která nám byla do redakce zaslána. Konkrétně jde o přepínání obrázků (spritů) podle směru a velikosti postavičky v jazyce JavaScript:
// right direction if (direction == 0) { if (image == 1) div.className += "spriteLiveRightL"; if (image == 2) div.className += "spriteLiveRightM"; if (image == 3) div.className += "spriteLiveRightS"; div.style.left = -200 + "px"; } // left direction if (direction == 1) { if (image == 1) div.className += "spriteLiveLeftL"; if (image == 2) div.className += "spriteLiveLeftM"; if (image == 3) div.className += "spriteLiveLeftS"; div.style.left = screenWidth + 100 + "px"; }
Na první pohled vidíme, že se 2 bloky kódu liší minimálně. Kód je jistě možné minimálně zkrátit jen na:
if (direction == 0) { let directionName = "Right"; div.style.left = -200 + "px"; } else { let directionName = "Left"; div.style.left = screenWidth + 100 + "px"; } if (image == 1) div.className += "spriteLive" + directionName + "L"; if (image == 2) div.className += "spriteLive" + directionName + "M"; if (image == 3) div.className += "spriteLive" + directionName + "S";
Kód výše funguje úplně stejně, ale má mnohem méně duplicit.
Představte si, že by se změnilo pojmenování obrázku z
...Right...
a ...Left...
na R
a
L
. Podívejte se, kolik kódu by se muselo přepsat u příkladu
porušujícího DRY a kolik u opraveného příkladu. Kód ovšem stále není
ideální.
DRY není jen o duplicitním kódu, ale o opakování jako takovém. Bezduchá dlouhá větvení bývají téměř vždy nahraditelná chytřejšími konstrukcemi, obvykle cykly nebo polem. Udělejme další úpravu:
if (direction == 0) { let directionName = "Right"; div.style.left = -200 + "px"; } else { let directionName = "Left"; div.style.left = screenWidth + 100 + "px"; } let images = [1: "L", 2: "M", 3: "S"]; div.className += "spriteLive" + directionName + images[image];
V našem případě jsme si pojmenování velikosti postavičky
L
, M
a S
uložili do slovníku pod
číslo, kterým se velikost (obrázek) vybírá. Není poté nic
jednoduššího, než vybrat písmenko velikosti podle klíče slovníku.
Nyní na kód aplikujme také pravidlo KISS. Obrázky můžeme totiž jednoduše pojmenovat číselně, aby směr a velikost v jejich názvu souhlasily s reprezentací těchto hodnot v kódu. Proč to dělat složitě? Obrázky stejně nejsou určené pro uživatele. Výsledek:
div.style.left = `${direction ? screenWidth + 100 : -200}px`; div.className += `spriteLive${direction}_${image}`;
Kód se zkrátil na 2 řádky z původních 18(!) a dělá úplně to samé. To vše jen za pomoci KISS a DRY. Představte si, co se stane, když Best practices aplikujete na celou aplikaci. Najednou nemusíte psát desítky tisíc řádků kódu a převálcujete svou konkurenci za několik měsíců. Dobré praktiky určitě nepodceňujte
Ukázkou DRY by mohly být carousely na ITnetwork, které lze různě nastavovat:
Carousel s fotografiemi
Carousel s HTML obsahem
To jistě ještě není nic velkolepého. Vnitřně ale navíc tyto carousely dědí z komponenty, která přepíná záložky:
Tabcontrol se zdrojovými kódy algoritmu pro různé jazyky
Mnoho programátorů by napadlo napsat carousel a záložky zvlášť, ale když se blíže zamyslíte, dělají v podstatě to samé, jen vypadají jinak. Jedna komponenta lze vytvořit jen menší obměnou té druhé.
Dalšími příklady DRY by mohlo být např. vytvoření kvalitních obecných CSS stylů, které nejsou omezené na konkrétní prvky na stránce a lze je parametrizovat, jako to má např. CSS framework Bootstrap:
<table class="table table-striped">
Výše uvedený kód nastaví tabulce základní styl a zároveň pruhování.
Shy
Možná znáte poučku:
Keep it DRY, shy, and tell the other guy.
Česky volně přeloženo jako:
Neopakuj se, piš stydlivě a řekni to ostatním.
DRY a opakování jsme si již vysvětlili, ale jak je to se stydlivým kódem? Stydlivý kód se chová úplně stejně, jako stydliví lidé. Komunikuje s ostatním kódem tedy jen když je to nezbytně potřeba a nemá ani více informací o ostatních, než sám nezbytně potřebuje. SHY je vlastně jiné označení pro zákon Deméter, viz níže.
LoD
Law of Demeter, tedy zákon Deméter, řecké Bohyně úrody, je v podstatě o low/loose coupling, tedy o udržování nejmenšího možného počtu vazeb mezi komponentami. Je definován třemi pravidly, kde zavádí pojem jednotka pro zapouzdřenou část kódu, tedy pro třídu:
- Každá jednotka by měla mít omezenou znalost o ostatních jednotkách, pouze jednotkách, které jsou dané jednotce blízké.
- Každá jednotka by měla "mluvit" pouze se svými "přáteli", nemluvte s cizími.
- Mluvte pouze se svými přímými přáteli.
Z pravidel je vidět, že každý objekt by měl být univerzální a naprosto odstíněný od zbytku aplikace. Znát minimum informací o celku a sdílet minimum informací o sobě celku.
Příklad
Praktický příklad si určitě dokážete představit. Jedná se o třídy s dobře zapouzdřenou vnitřní logikou a obecnou funkčností, aby nemusely znát detaily konkrétního systému a bylo je možné použít kdekoli. Např. vytvoříte generátor objednávek tak, aby jej bylo možné 100% přizpůsobit a nebyl závislý na potřebách jednoho informačního systému. Možná vás napadá, zda není plýtvání vývojovým časem a penězi vytvářet funkce, které v daném projektu nepotřebujete. To by plýtvání určitě bylo. S těmito funkcemi stačí jen počítat a navrhovat komponenty tak, aby do nich šlo v budoucnu něco snadno přidat.
LoD se týká také předávání závislostí, kterých by mělo být co nejméně a třída by měla záviset na abstrakcích, ne konkrétních třídách. O tom hovoří Dependency Inversion Principle, který je součástí pouček SOLID.
IoC
Princip Inversion of Control byste měli dobře znát. Jedná se o skupinu návrhových vzorů, jejíž součástí je i populární Dependency Injection, jediný správný způsob, jak v objektových aplikacích závislosti předávat. IoC tvrdí, že objekty v aplikaci by měly být řízené nějakým vyšším mechanismem, který vytváří instance tříd a předává jim potřebné závislosti. To je opačný přístup ke staršímu způsobu práce se závislostmi, kdy si třídy své závislosti vytahovaly samy ze systému, např. přes Singleton nebo jiné antipatterny. Jelikož se jedná o extrémně důležitou problematiku, připravili jsme pro vás o DI samostatný kurz Softwarové architektury a depencency injection, kde je podrobně vysvětlena na reálných příkladech.
V příští lekci, Best practices pro vývoj softwaru - Praktiky SOLID, si ukážeme praktické příklady a ilustrace dobrých praktik SOLID, principů SRP, Open/Closed, Liskov substitution a Interface segregation.