FK~

Moje odkazy

Ostatní odkazy

Close Windows
Nenajdete mě na Facebooku ani Twitteru
Rozpad EU
Jsem členem FSF
There Is No Cloud …just other people's computers.
Sane software manifesto / Manifest příčetného softwaru

Komplexita softwaru: Jak vzniká?

vydáno: 8. 3. 2020 23:34, aktualizováno: 22. 11. 2020 14:55

Dnes se podíváme na příčiny vzniku komplexity, a navážeme tak na předchozí díl této série.

komplexita (ilustrační obrázek)

Inherentní složitost

Software vyvíjíme na základě zadání – toto zadání obsahuje přirozeným jazykem psané požadavky, jak by se software měl chovat a jaké další vlastnosti by měl mít. Kromě souvislého textu může zadání obsahovat i různé diagramy nebo tabulky. Toto zadání pak implikuje určitou minimální složitost softwaru, pod kterou se nemůžeme dostat. Souvětí vedou např. na příslušné iffor konstrukce v kódu a tabulka třeba na switch nebo rozhodovací tabulky či stromy nebo na daty-řízený program. Ať už zadání implementujeme jakkoli, entropie zadání se promítne do entropie softwaru.

Stejně jako implementace může záviset na externích knihovnách, tak zadání může odkazovat i na jiné dokumenty, specifikace či normy. Nestačí tedy spočítat stránky, tabulky, diagramy atd. k tomu, abychom získali hrubou představu o složitosti – stejně jako u implementace je potřeba započítat i komplexitu závislostí. Mezi ně patří i oborové nebo národní standardy, bez jejichž dodržení bychom software nemohli dodat. Na hraně jsou firemní standardy – z pohledu vývojářského týmu sice jsou sice povinné, ale z objektivního hlediska představují spíše dodatečnou složitost. Implicitní předpoklady jsou rovněž sporné – pokud je něco „samozřejmé a každý ví, že se to takhle přece dělá“, tak stejně neškodí to v zadání explicitně uvést, je-li to skutečně povinná součást.

Občas se stane, že zadání je napsané s příliš velkou redundancí (rozkopírované texty, příliš mnoho zbytečných slov) a programátorovi se v té změti podaří najít tu původní myšlenku, která je ve skutečnosti jednodušší, takže jeho kód má pak nižší komplexitu než zadání. Tohle bývá chyba analytika, který v důsledku nějakých komunikačních šumů nebo neznalosti problematiky zanese do zadání zbytečnou složitost a duplicity. V takovém případě je samozřejmě správné to implementovat jednodušeji místo doslovného opisování, ale pro jistotu je dobré si takové zjednodušení nechat schválit od zadavatele.

Dodatečná složitost

Většinou se ale setkáváme spíš s opačným problémem – a to, že poměrně jednoduché zadání je realizováno velmi komplexním softwarem. Tuto složitost softwaru nikdo nepožadoval a vznikla (neřízeně) během implementace. Důvodů, proč k tomuto dodatečnému navýšení složitosti dochází, je více a často jsou míněné dobře.

Snad každý vývojář touží být jednou hrdinou a užít si tu chvíli, kdy přijde změnový požadavek a on bude moci vítězoslavně oznámit: „tohle už umíme, stačí to zapnout“ nebo „je to jednodušší než jste čekali“ případně neříkat nic a získat volný čas nebo peníze navíc, protože tu práci už má hotovou od minule. Můžeme se tedy chtít připravit na budoucí vývoj, o kterém si myslíme, že přijde, nebo na nevyslovené požadavky, na které zadavatel zapomněl, ale my víme, že při uvedení softwaru do provozu si najednou vzpomene. A nemusí to být jen funkční požadavky, ale i třeba příprava na několikanásobný růst zátěže či objemu dat a další optimalizace, nezřídka předčasné.

Na jednu stranu je hloupost dělat krátkozraká rozhodnutí a nevidět dál než za konec aktuální iterace a požadavky momentálně čekající ve frontě. Na druhou stranu nemá smysl paralyzovat projekt a produkt přípravou na něco, co třeba nikdy nenastane – a nebo nastane, ale za výrazně jiných podmínek, než máme teď: např. až budeme mít tisíckrát víc transakcí, budeme mít i tolik peněz, že si budeme moci klidně dovolit stávající software zahodit a kompletně přepsat. Všechno bychom asi nikdy nezahazovali, nicméně nemá cenu se teď připravovat na nějakou situaci, když mezi tím dojde k zásadní změně pravidel hry (budeme mít řádově větší rozpočet a to, co nás trápí teď, nás trápit nebude a místo toho budeme řešit jiné problémy). Je otázka, jaká míra přípravy na budoucí požadavky, je správná. Když půjdeme na výlet, tak je normální si s sebou vzít nůž a nějaký ten nástroj, ale nepotáhneme bednu s nářadím, páječku a půlmetrové pákové kleště, pokud není zřejmé, že něco z toho budeme potřebovat. Když se na stole objeví láhev vína a všichni na ni bezradně zírají, tak můžeme zachránit situaci tím, že z kapsy vytáhneme vývrtku. Těch pár gramů, které jsme nesli, je neznatelných, a i kdybychom je nesli zbytečně, tak se nic neděje.

A stejné je to s vývojem softwaru a přípravou na budoucí změny a požadavky. Pokud lepší řešení připravené na budoucnost znamená jen nepatrné množství kódu a práce navíc, má smysl. Rozhodování, co ještě realizovat a co už ne, obvykle probíhá na základě intuice a zkušenosti. Ale dá se k tomu přistupovat i exaktněji – jde v principu o stejný případ, jako je řízení rizik – i zde násobíme pravděpodobnost nějaké budoucí události a výši škody (navýšení nákladů při implementaci odložené do budoucna). Už metodika RUP/OpenUP nám říká, že čím dříve chybu odhalíme a opravíme, tím bude oprava levnější (nejlevnější je v době, kdy teprve připravujeme zadání; trochu dražší je při vývoji a testech; a nejdražší je v produkci). S implementací požadavků je to stejné – některé změny může být hodně drahé zapracovat dodatečně, zatímco kdyby se s nimi počítalo už v době analýzy a přizpůsobila se jim architektura a návrh, tak nás to nebude stát skoro nic navíc. Dobrá architektura by sice měla umožňovat zavádět i zásadnější změny dodatečně, ale to nemění nic na tom, že dodatečná změna bude zpravidla dražší.

Dalším důvodem, proč dochází k navýšení komplexity je paradoxně snaha ušetřit si práci. A nemluvím tu o nějaké lenosti, kdy vývojář vynechá nějaké kroky nebo něco zanedbá. Mluvím o situaci, kdy vývojář naopak něco přidává – s dobrým úmyslem, že to ušetří práci. Tohle je vlastně smysl existence všech knihoven a frameworků a důvod proč nepíšeme naše programy jen s využitím možností daného programovacího jazyka a standardní knihovny. V počátcích se programy psaly v jazyce symbolických adres (JSA, assembler), pak přišlo C, potom C++, dále Java, PHP a další. Každý z těch vyšších a novějších jazyků nám měl ušetřit práci a zbavit nás nudného opakujícího se kódu, zvýšit bezpečnost a spolehlivost našeho softwaru. Stejná motivace je i za vznikem knihoven a frameworků, které se nad těmito jazyky staví – jakkoli nový a vysokoúrovňový jazyk a jeho standardní knihovnu začnou programátoři dříve či později vnímat jako příliš nízkoúrovňové a začnou nad nimi vytvářet další nadstavby. Komplexita počítačových systémů tak po celou dobu historie našeho oboru stoupá a vrší se na sebe další a další vrstvy s jakýmsi nejistým příslibem, že by vývoj softwaru měl být jednodušší. Tuhle myšlenku jako takovou nechci rozporovat a v principu s ní souhlasím – vhodně navržená abstrakce nebo vyčlenění části kódu do nějaké znovupoužitelné komponenty, knihovny či frameworku práci ušetří a taky může zvýšit spolehlivost či bezpečnost systému. Důležité ale je nezapomínat, proč to děláme a na jakém předpokladu naše rozhodnutí (přidat tu či onu závislost) stálo. Tento předpoklad bychom měli průběžně testovat – a pokud zjistíme, že platit přestal, tak z toho vyvodit patřičné závěry. Neměli bychom zapomínat na to, že samotný vyšší programovací jazyk (např. Java nebo PHP) spolu se svojí standardní knihovnou jsou samy o sobě frameworkem, stejně jako je frameworkem UNIX (resp. dnes převážně GNU/Linux) a že poskytují víc než dostatečnou úroveň abstrakce pro vývoj mnoha aplikací. Přidání jakékoli vrstvy nad ně bychom neměli vnímat jako samozřejmost a měli bychom toto rozhodnutí mít vždy dobře zdůvodněné a podložené.

Pak tu máme různé spontánní příčiny a chyby. Tedy situace, kdy to dotyčný nemyslel dobře nebo nemyslel vůbec a k navýšení komplexity došlo omylem či nešťastnou náhodou. Sem patří kopírování kódu nebo opakovaná implementace téhož – místo psaní znovupoužitelného kódu. Už v prváku někde na přednášce ze softwarového inženýrství se studenti dozví, že když píší stejný nebo podobný kód, že by ho místo toho měli dát do funkce/metody a tu opakovaně volat. Přesto v praxi často narazíme na duplicitní redundantní kód, který nám znesnadňuje čtení i úpravy – kvůli jedné změně je potřeba upravit kód na mnoha místech. U dynamicky typovaných jazyků je navíc obtížné tato místa vůbec najít. Analogií u relačních databází je (de)normalizace dat. Někdy se kopíruje i kód z jiných projektů a knihoven – místo, aby se na něj náš program jen odkázal jako na externí knihovnu. Mít zazálohované zdrojáky knihoven, které používáme, je jednoznačně správné, ale nakopírovat si je vedle zdrojáků naší aplikace zpravidla ne (tzv. vendoring).

K navýšení komplexity přispívá i nevhodná volba programovacího jazyka. Příliš nízkoúrovňové jazyky jako C svádí programátora k tomu, aby řešil technické detaily místo obchodní logiky aplikace. A je snadné v nich udělat bezpečnostní či jinou chybu. Teoreticky by i v C šlo díky vhodným abstrakcím psát vysokoúrovňový kód a aplikace, ale v praxi se to moc nevidí. Z hlediska závislostí by C mohlo mít přínos v tom, že jeho kompilátor může být jednoduchý, ale vzhledem k tomu, že většina programů se kompiluje pomocí GCC nebo Clangu, tak jde spíše o nevyužitý potenciál. Tyto kompilátory totiž podporují i C++, takže lze použít vyšší programovací jazyk s lepší správou paměti, aniž bychom přidávali další závislosti. C++ si s sebou táhne dost velkou historickou zátěž, takže to je trochu kontroverzní volba, nicméně moderní C++ je celkem fajn jazyk a pokud si z C++ člověk vybere vhodnou podmnožinu, může to být rozumné rozhodnutí. A pak tu máme mladší a čistější jazyky jako D, Rust nebo Java.

Příliš malá granularita má za následek, že přidáním závislosti na knihovně či aplikaci dojde k nepřiměřenému nárůstu komplexity. Malá granularita znamená, že daná komponenta spojuje příliš mnoho funkcí (a tudíž komplexity) do jednoho nedělitelného celku. Tato komponenta nám sice poskytuje něco, co potřebujeme, ale zároveň umí spoustu věcí navíc. A protože zlaté pravidlo nám říká: co sám nerad, nečiň druhým, měli bychom i my, když jsme na straně poskytovatele softwaru, umožnit jeho uživatelům si z něj vzít jen to, co potřebují, a nezatěžovat je komplexitou, o kterou nestojí. Jinak řečeno: špatná architekturanemodulární návrh vedou k nežádoucímu nárůstu komplexity v důsledku toho, že se lidé stanou závislí i na tom, co nepotřebují, a zavlečou si do systému více kódu, než je nutné.

Kromě běhových závislostí tu máme i závislosti nutné pro sestavení programu. Uživatel je běžně nevidí, protože se k němu dostanou již přeložené binární balíčky. Jde o kompilátory, staticky linkované knihovny a různé pomocné nástroje pro skriptování, testování atd. Je to taková skrytá hrozba, protože když v takovém nástroji či knihovně bude chyba, ohrožuje i konečného uživatele, který většinou ani netuší, že takovou závislost jeho software má. Velký problém je to pro autory distribucí, kteří software sestavují ze zdrojových kódů. Tyto otázky se řeší mj. na serveru bootstrappable.org. Některé programy závisejí dokonce na předchozích verzích sebe sama. Často se to týká kompilátorů. Někdy se tím autoři dotyčného softwaru i chlubí – ve smyslu: věříme svému softwaru natolik, že ho sami používáme. Na jednu stranu je to hezké, ale pro distributora nebo někoho, kdo si jen chce postavit minimalistický systém ze zdrojových kódů, je to docela problém, protože musí sestavit postupně všechny verze daného softwaru a reprodukovat tu historickou posloupnost – až k verzi, kdy ten software ještě sám na sobě nezávisel a stačil pro jeho překlad třeba jen céčkový kompilátor. Nejde o neřešitelný problém, ale jsou to nemalé náklady na čas a výpočetní výkon.

Během poslední dekády došlo také k obrovskému nárůstu komplexity v oblasti nástrojů podporujících provoz softwaru. Souvisí to s módním trendem tzv. DevOps (je tedy otázka, co je příčinou a co následkem, ale to teď nechme stranou). Zatímco dřív systémoví operátoři či administrátoři provozovali operační systémy a nad nimi aplikace (a tím myslím aplikace podporující byznys dané firmy či organizace tzn. přímo požadované tímto byznysem), k čemuž si napsali pár skriptů, tak dnes provozují stále rostoucí počet podpůrných aplikací a systémů, které nikdo z byznys uživatelů přímo nepožadoval. Někdo by dokonce řekl, že se IT více zaobírá samo sebou a požírá nám zdroje, aniž by mělo viditelný přínos. Tyto podpůrné systémy slouží k různému monitorování, sestavování, nasazování, orchestraci, generování grafů, logování, virtualizaci, kontejnerizaci, filtrování, směrování síťového provozu atd. Současně dochází k tomu, že část aplikační logiky se přesouvá na úroveň těchto podpůrných systémů a nástrojů a do jejich konfigurace – vzniká tak závislost a komplexitu těchto nástrojů je potřeba započíst do komplexity aplikací. Neříkám, že bychom se měli vrátit na stromy a štítit se použít jakoukoli novější či složitější technologii. Ale měli bychom přesně vědět, co a proč používáme a umět alespoň zhruba vyčíslit, co nás to stojí a co nám to přináší. A pokud tento poměr přestane být výhodný, měli bychom být připraveni sadu použitých technologií, nástrojů a postupů přehodnotit. Podobný případ je Cloud – od něj se slibovalo, že mj. sníží náklady na administraci… od té doby jste ale pravděpodobně žádného administrátora nepropustili a dnes je naopak posíláte na školení cloudu a kupujete jim podpůrné nástroje, aby byli schopní s cloudem pracovat nebo najímáte externí konzultanty, aby jim s ním pomohli. Zatímco dřív bývala administrace relativně pohodová a nenáročná práce, dnes se jedná o vysoce komplexní záležitost, která si „díky“ množství a složitosti používaných nástrojů svojí náročností často nezadá s programováním. Opět má smysl si tedy položit otázku: Kolik nás to stojí a co nám to přináší?

Motivace ke zvyšování komplexity zaváděním těchto nástrojů jsou různé. Někdy lidé upřímně věří, že to přinese užitek, někdy si chtějí pohrát s novou technologií, někdy si chtějí pojistit svoji pracovní pozici a učinit se nepostradatelnými tím, že budou jediní, kdo daný složitý systém umí obsluhovat, a někdy věří, že když danou technologii či postup používá nějaká velká a bohatá firma, bude to to pravé i pro nás.

Závěr a pokračování

Čas od času bychom se měli zastavit a zamyslet se nad komplexitou našeho softwaru – zda neseme v kapse navíc jen tu vývrtku, nebo zda táhneme na zádech těžkou bednu s nářadím. Současně bychom měli ověřit, zda stále platí ten základní předpoklad – a to, že nástroje by nám měly šetřit práci a zjednodušovat život. U každého kusu kódu, nástroje nebo závislosti je legitimní a správné si položit otázku: Kdo je požadoval a co od toho očekával? Kde tento požadavek nalezneme v zadání? Pokud na tuto otázku nedokážeme odpovědět, máme buď nekompletní zadání nebo jde o nadbytečnou komplexitu. Odpověď, že daný nástroj či postup používá nějaká velká korporace nebo že o nich teď vychází hodně článků na internetu, je špatná odpověď.

Příště se podíváme už trochu konkrétněji na možná řešení, která nám jednak umožní se s nadbytečnou komplexitou vypořádat a jednak jí při návrhu nového systému předejít.

Témata: [počítačová bezpečnost] [softwarové inženýrství] [softwarová architektura]

Komentáře čtenářů

Tento článek zatím nikdo nekomentoval

Přidat komentář

reagujete na jiný komentář (zrušit)
jméno nebo přezdívka
název příspěvku
webová stránka, blog
e-mailová adresa
nápověda: možnosti formátování
ochrana proti spamu a špatným trollům

Náhled komentáře