Proč je ten softwarový vývoj tak drahý?

Marian Benčat
Dec 28, 2018 · 13 min read

Obzvláště v posledních letech, kdy se věnuji nejen fullstack vývoji, ale i konzultacím a školení pozoruji častý problém s vývojem primárně backendového kódu. Velmi častá otázka je “Proč je ten náš vývoj tak drahý? Jak vyvíjet efektivně?”. Odpověď na takovou otázku není rozhodně jednoduchá, obzvláště, pokud vezmeme v potaz nějaké sekundární vlivy jako kvalita vývojářů, pracovní morálka atd.

V tomto článku bych si ale přál rozebrat pár aspektů, které vídám ve firmách poměrně často a vždy jsou jakýmsi “náboženstvím” pro vývojáře, vyplývající často z fiktivně-pragmatických pouček, které se pouze velmi těžce střetávají s realitou (jak to má ostatně náboženství vždycky :-)) . Pojďme se společně podívat na to, jak vypadá současný vývoj softwaru — primárně na backendu a co přidělává zbytečné obtíže.

Poznámka: Jednotlivé body nejsou seřazeny podle důlěžitosti. Některé body spolu velmi souvisí a rozdíly mezi nimi mohou být poměrně malé, přesto jde vždy o něco trošku jiného.


Multitier Architecture

Jako jedna z největších věcí, co přidávají na komplexitě a výsledné ceně aplikace, je jeji rozdělení na několik “vrstev” horizonálně — většinou jde o rozdělení na základní 3 vrstvy — prezentační vrstva, doménová (aplikační) vrstva a datová vrstva. Webový programátor pod těmito vrstvami je jistě schopný najít následující části v závislosti na tom, v jakém je postavení (producent/konzument), či o jakou jde aplikaci (desktop, web, api,…) :

Prezentační

  • controllery (MVC)
  • view/viewmodely (MVVM,…)
  • public API

Doménová

  • domain service
  • ui service / aplikační service (fasády)
  • doménové entity
  • utils

Datová

  • Unit of work
  • Repository
  • Query Objecty

Toto řešení (a téměř všechny) v sobě skrývá spoustu výhod, ale není to rozhodně zadarmo. Problémem na většině projektů je, že se takováto multitier architektura nasazuje plošně na veškeré projekty a co je ještě daleko horší — nasazuje se hned celá a to se vším všudy (rozebereme později v dalších bodech — například v “refactoring v evoluci softwaru”.

Přijde pak oblíbená otázka a oblíbená odpověď. “Proč ten software děláme tak složitě? Nejde to udělat lehčím a levnějším způsobem?”. Na jazyk se dere jednoduchá odpověď — “Ale to není prezentační web, ani jednoduchý eshop, ale velikánský škálovatelný projekt!”. Co přesně to přirovnání k prezentačnímu webu / jednoduchému eshopu znamená? Proč se ta aplikace rozděluje na několik vrstev horizontálně a je to opravdu nutné vždy dělat? Co to obnáší za režii navíc?

U prezentačního webu, případně jednoduchého eshopu uvažujeme běžně úplně jinou množinu “měnících se věcí” — dále jen “moving parts”, než je tomu u nějakého velkého portálového řešení a SW. V momentě, kdy uslyší programátor “moving parts”, už někde vytváří nějakej interface, viewmodel, DTO, POCO, PDO, abstrakci nad abstrakcí, univerzálnost nad univerzálností.

Pak se celý postup změní do stylu:
“Hmm, mám tady entitu Auto, takže vytvořím Auto, AutoRepository, AutoService, AutoController, AutoDTO, AutoVM, AutoDTOVM mapper,…”,

Navíc je mezi každou vrstvou “doporučované” :-) mít samostatný POCO objekt tak, aby ViewModel neopustil prezentační vrstvu, aby se naopak doménová entita nepohybovala v prezentační vrstvě atd. Skončíte tedy u tohoto:

S vidinou toho, že to “nejspíše jednou budeme měnit” — budeme dávat jinou datovou vrstvu, budeme dávat jinou UI vrstvu atd., tvoříme desítky či stovky mapování mezi jednotlivými modely, protože “co kdyby”.

Všude promapování, které ve většině případů vinou programátora, nebo tvůrce pomocné knihovny (Mapper, ORM,…) stejně většinou nesplní svůj účel, protože je tam vždycky nějaká leaky abstrakce, kvůli které prostě nikdy nepůjde bez většího či menšího refaktoringu změnit datová, nebo UI vrstva. Už z principu věci tedy nikdy nebude N-tier architectura a její armáda mapperů samospásná. Chcete jeden příklad za všechny? Vemte si jakékoliv ORM na světě vás napadne, vždycky budete muset ohýbat část doménové vrstvy podle toho, co si vyžaduje ORM, nebo ten skutečný datový sklad pod tím. A je to naprosto normální, dokonce i z definice Repository patternu to tak vyplývá.

Repository pattern má sloužit k tomu, aby KÓD nevěděl, co běží pod repository (aby se data tvářila jako in-memory kolekce) a NE PROGRAMÁTOR, ten vždy musí brát v úvahu “underlayed providera”

Důkazů je nespočet, ať už těch obvious ( lazy / eager loading ), tak těch méně obvious (existence unit of work a používání v domain / application service — stanování scope databázové transakci). Zkuste si prostě nad repository dát metodu GetAll() stejně jako u in-memory kolekce a říkejte si, že vás abstrakce zase jednou zachránila!

Navíc namapování dat není ta jediná práce co vás čeká — co třeba mapování chyb? Pokud používáte nějaký inteligentnější způsob zpracování “chybového očekávaného chování” jako je třeba Result Pattern (obsahující informaci o chybě), budete mapovat jeden error code na jiný (protože podle “doporučení božstva” nesmí sdílet různé vstvy chybové kódy i přesto, že znamenají to samé ), pokud však řídíte logiku aplikace pomocí exceptions (i ty očekávané chyby jako je třeba duplicitní uživatelské jméno atp). Tak si to opravdu užijete. Buďto budete mít někde leaky abstrakci (catchovani chyb z repository v UI vrstvě), nebo budete krásně catchovat desítky exceptionu.

No a… jak to vypadá tedy u těch jednoduchých eshopů a prezentačních webů? Předpokládají, že nikdy nebudou vyměňovat prezentační vrstvu za jinou (třeba web za desktopovou aplikaci), že nikdy nebudou měnit ORM za jiné a pokud potřebují kvůli rychlosti napsat SQL / NoSQL query ručně? Tak ho prostě napíšou pro ten jeden příklad ručně více, či méně čistě. :-)

Takže v kódu takového webového eshopu prostě najdete:

  • PRIMITIVNÍ logiku v controlleru
  • použití repository v controlleru (nebo klidně třeba DBSetu z Entity Frameworku)
  • ORM s leaky abstrakcí (mapující atributy v “entitách” namísto externích fluent-konfiguracích jako to má třeba EF, či Mongo driver)
  • žádné mapující POCO objekty mezi vrstvami, maximálně viewmodely, kvůli bezpečnosti
  • Žádná vlastní abstrakce nad repository ORM frameworku
  • Žádné další abstrakce, třeba nad contextem (vlastní Unit-of-work)

Je to tak správně? Je velmi možné, že ano. Pravděpodobně žádnou abstrakci nepotřebují. Pokud dojde k některým změnám, prostě kód zrefaktorují, bude je to stát jistě nějaký čas, refactor je však zpravidla vždycky to řešení MENTÁLNĚ NEJJEDNODUŠŠÍ. Téměř vždy je tedy pro ně refactor prostě méně náročný, než nějaké složité abstrakce, které by stejně neudělali dobře.

Velká část rozdílu nutných investic do vývoje “velkého” sw a “malého” webu / eshopu je tedy o rozdílu myšlení. Při vývoji SW většinou vývojáři přemýšlejí bohužel často genericky stylem “To bude nutné třeba JEDNOU předělat, tak to na to teď připravím. Tady bude interface, tady mapování, tady nějaká anti-corruption layer” Přitom jistě existuje a je velmi často vhodnější i jiný přístup.

Častým důvodem pro “interface všude” je testování. Všude se objeví nějaký interface a pak se stejně v kódu (nebo jazyce — ahoj hlavně PHP) vyskytují statické cally na systémové funkce, co se nedají jednoduše stubnout. A ostatně, má to tak i třeba .NET (jen se koukněte, kde je v kódu DateTime.Now.) Třešničkou na dortu je pak už jen hodně špatně napsaný ambient context (statický context s “get” metodou).


YAGNI — You are not gonna need it

Ač tento termín může být pro někoho zcela neznámý, ve své podstatě je to ten nejzákladnější princip celého evolučního způsobu vývoje, agilní metodiky, continuous delivery a především continuous refactoring. Základním pilířem YAGNI je to, že děláme jen věci, které aktuálně potřebujeme.

Přesto, nikdy není dobré zcela dogmaticky se držet nějaké metodiky a i u YAGNI to tak je. Osobně se vždy zamyslím nad daným problémem a volím řešení na základě následující otázky:

Bude pak případný refactoring mnohonásobně časově a finančně náročnější, než pokud teď budu věnovat čas složitému a velmi univerzálnímu řešení?

Pokud není odpověď kladná — a že jen tak není, je lepší jít cestou primitivnějšího a především NÉ SUPER UNIVERZÁLNÍHO ŘEŠENÍ.

Nepleťte si YAGNI s tím, že není dobré nikdy nic dělat univerzálně. Jsou samozřejmě velmi dobré příklady, kdy ty věci univerzálně udělat.

Dělat věci univerzálně — i ty absolutně nejprimitivnější ale není opravdu nic jednoduchého, jak si brzy ukážeme. Navíc — uvědomme si jednu věc, je dosti pravděpodobné, že věc kterou v IT vymyslíme, už někdo před námi udělal a nemusí to být ani v posledních letech, ale třeba i desítky let nazpět — přeci jen, všechny aktuální “hity” nejsou novinky, pouze vybalené staré věci ze skříně po pradědečkovi programátorovi… a je jedno jestli je to architektura microservice, CQRS pattern, nebo třeba Redux :-)

Co tím chci říci? Věci, které je vhodné psát univerzálně, někdo již jistě univerzálně napsal a věnoval tomu obrovské úsilí → email sender, autorizační middleware, ORM,…


Refactoring v evoluci softwaru

Ve firmách je už poměrně zavedený “standard” říkat, že se vyvíjí agilně, ovšem velmi často to znamená agilně vyvíjet jen jednotlivé stavební bloky — agilně vylepšovat UI, agilně zlepšovat performance. Todle je trošku historické, kdy se na architecturu aplikace začalo hledět podobně, jako je tomu u architektury baráku — tedy bylo nutné navrhnout celou architekturu aplikace ještě před tím, než se napíše čárka kódu… Ještě před započtením prací bylo nutné říci, kde bude jaký pattern, jestli bude komunikace synchronní, či asynchronní, jaká se použije databáze a další věci. Už tento přístup sám o sobě NAPROSTO BEZVÝHRADNĚ forcuje vývoj do vodopádového modelu. Nelze dělat jednotlivé funkcionality, protože je třeba nejdříve naimplementovat složitou architekturu, narvat všude abstrakci, hlavně aby šlo vše v ideálním případě podle potřeby vyměnit a další věci.

Celý tento problém vychází z předpokladu, že architektura má být primárně neměnná věc, která se v průběhu vývoje nemůže téměř nijak měnit. K překvapení všech, tímto však netrpí pouze “klasické” monolitické architektury, ale i ty “nově objevené”, jako jsou microservicy. U microservice se toto projeví ještě více, než u velkého monolitu, jelikož jsou vyvíjeny paralelně a především jsou jejich potřeby často velmi rozdílné — více rozdílné než tomu bývá u monolitu.

Pokud tedy opravdu chcete vyvíjet agilně, je nutné pohlížet i na samotnou architekturu jako EVOLUČNÍ ARCHITEKTURU, tedy takovou, která se průběžně vyvíjí spolu s vývojem produktu, tak jak přicházejí změnové požadavky a problémy. Budou změny, budou refactory, přijměte to!


Copy pastujte kde to dává smysl, enkapsulujte, kde máte

Nenávidím běžný enteprise-level kód. Pokud mi někdo něco takového poví, očekávám, že mi vyjede při chybě 250ti řádkový callstack, jelikož každej enterprise vocas musí svojí metodu rozdělit na 45menších, narvat všude 55 návrhových vzorů a nikde se mu nemůže objevit ani 10řádků 3x nakopírovaných, protože má pak velký “code duplication”.

Ti samí enterprise-level vývojáři nemají logiku tam kde mají mít a nejsou schopni říci, co jejich kód dělá hned druhý den. Co vídám nejčastěji je tedy kód, kde si programátor nedovolil zkopírovat 10x jeden kus kódu, protože by to podle něj nebylo dostatečně programátorský košér, na druhou stranu má zcela anemické objekty bez špetky logiky a vše má pak narvané 20x (pokaždé ale jinak zapsané v kódu) v anemických “rádoby doménových” servisách.

Pamatujte, že čtivost kódu je na prvních příčkách důležitosti. Proto se nebojte používat veškerých možností multiparadigmatického jazyka. Funkcionální přístup bude zpravidla mnohem čtivější pro manipulaci s daty, než imperativní a naopak.

Nebojte se kód kopírovat, pokud to dává naprostý smysl! Nevymýšlejte složité algoritmy na jednoduché problémy. Velmi často se bohužel setkávám s tím, že mají vývojáři často problém zkopírovat 5řádek přiřazení do proměnných, protože je to “duplicitní kód” a tak raději udělají nějaký mapper, nějakou faktorku — né proto, aby zaenkapsulovali nějaké chování, ale JEN PROTO, aby nekopírovali kód… Pak najednou nechtějí přiřadit 5 proměnných, ale jen 4, tak nabalí do tohoto jednoduchého mapperu další parametr, který říká, zda se má namapovat 4 nebo 5 proměnných atd. Kód pak skončí jako neskutečný balast plný provolávání metod, kterými se musí programátor prokousat jen proto, aby nebylo 3x v kódu:

(5, null, 11) // wtf?

Pokud chcete vidět další příklady, doporučuji checknout:

Zde jsem uvedl velmi primitivní příklad toho, kdy je mnohem vhodnější nějaký code-duplication, než nějaká metoda. Pokud kouknete na ten kód, jistě si povíte “takovou blbost bych přeci neudělal”, opak je většinou pravdou. Zkuste se občas nad svým kódem zastavit a zeptat se, jestli je nutné přidávat komplexitu do absolutních maličkostí, pokud nevyplývá z nějakého velmi silného doménového pravidla. Nedělejte metody jen proto, aby se vám neopakovaly řádky kódu, nikomu tím nepomůžete, ani sobě. VŽDY MŮŽETE VELMI JEDNODUŠE KÓD ZREFACTOROVAT A EXTRACTNOUT KÓD DO METODY, AŽ BUDETE MÍT DOSTATEK ZNALOSTI O DIVERZIFIKACI POTŘEBNÉ FUNKCE.

Pokud v budoucnu vznikne reálný důvod, proč nějakou část kódu extractnout do metody, takový refactor bude extrémně jednoduchý. Nikdo ale nevidí do budoucnosti a proto je nutné na každou metodu, každý ViewModel pohlížet jako na něco, kde je třeba maintainerovat.

Nepohlížejte na kód jako na řádky, které vás stály něco napsat, ale jako na řádky, které bude třeba udržovat a to bude stát chechtáky, spoustu chechtáků.

Každá metoda, každá knihovna co použijete a co hůře, každou knihovnu co napíšete — vždy jsou to i částečně pouta, která vám nedovolí udělat výjimku, jednoduchou změnu oproti běžnému postupu. Zamyslete se, máte dvě možnosti:

  1. Udělám / použiji univerzální (nebo rádoby univerzální) knihovnu, kterou pak budu někomu (nebo sobě na jiném projektu) distribuovat třeba formou balíčků. Analogicky — v tomto článku lze vnímat i naopak, kdy VYTVÁŘÍTE KNIHOVNU, PRO KTEROU PLATÍ

Plusy:

  • při použití není nikde code duplication
  • konzument reusuje hotový kód

Mínusy:

  • jakákoliv změna je většinou nereálná, nebo těžce realizovatelná, to zmanená, že tvůrce takové knihovny — univerzálního řešení (tím tvůrcem jste i často vy!), musí celý systém udělat neuvěřitelně komplexní a univerzální, to stojí hodně času a peněz.
  • pokud někdo zatlačí na tvůrce knihovny (váš kolega na vás), může to vézt pak k breaking changes, může to změnit chování. Každá závislost je něco, co musíte maintainovat, nebo ignorovat (pokud nechcete novější verzi s opravenými chybami).

2. Zkopíruji hotový kód z nějakého originu do svého solutionu a případně upravím

Plusy:

  • při použití není nikde code duplication
  • konzument reusuje hotový kód
  • mám naprostou svobodu změny, pokud potřebuji jiné chování, upravím si to
  • pro tvůrce to zmanená mnohem menší komplexitu tvorby takové knihovny — nemusí myslet na všechny případy (což prostě nikdy nejde).
  • tvůrce nemusí pravidelně ohýbat kód na případy na které nemyslel a tak často vytvářet breaking changes.

Mínusy:

  • nemáte updaty codebase (díky VCS lze u vlastních knihoven a projektů téměř ošetřit pomocí patchu)

Psát věci univerzálně je obtížnější, než se zdá

Napsat jakýsi svůj firemní foundation na kterým pak poběží vaše projekty, je strašně hezká myšlenka, ještě jsem ale za svůj život neviděl takové univerzální řešení. A to nejen na projektech, kde se účastním ať už jako vývojář, nebo konzultant, ale i co se týče světově používaných frameworků. Vždy se tam vyskytne něco, co vám ovlivní “vyšší vrstvu” a donutí vás do nějakého řešení (často nekoncepční prasárny), protože vám nic jiného nezbývá. Dám vám velmi jednoduchý příklad toho, jak je obtížné i tu nejjednodušší věc, která má být reusnutá mezi projekty — udělat univerzálně.

Níže je kód jakési base třídy pro všechny doménové entity, které se pohybují nejen v doméně, ale i dále se s nimi pracuje na datové vrstvě (repository).

Pointa třídy je jednoduchá, stanovuje unikátní identifikátor a přepisuje porovnávání instancí tak, aby se rovnaly v momentě, kdy se shodují identifikátory.

Velmi jednoduchá věc, která by šla použít napříč projekty? Jsou zde nějaké problémy? Napsat i takhle primitivní věc univerzálně prostě NEJDE, nikdy to nebude fungovat ani v polovině případů tak, jak bylo zamýšleno, prostě nemůžete znát budoucnost a pokud by to mělo být opravdu univerzální, ta třída bude mít 250 řádek a bude nutné s ní použít další desítku dalších tříd, které znatelně ovlivní vaši další architekturu.

Co je zde špatně? Hned 3 věci, řádky 4, 6 a 11. Nejmenším problémem je to, že je na tvrdku primárním klíčem určený Guid, který není vůbec benevolentní, co do něj lze uložit. Co když budeme chtít mít za klíč něco jiného než Guid? Některé systémy ho ani třeba nepodporují (leaky abstrakce z databáze). Lepším řešením by byl string (do kterého prostě serializujete téměř všechno), ještě mnohem lepším pak to, kdyby třída byla generická s parametrem TKey. A to máme zase další komplexitu, prootže pokud bude chtít jakýkoliv interface s tímto pracovat (třeba generické BaseRepository), tak už si musí předávat TKey. Už to tam cítíte?

Todle je pořád ten nejmenší problém. O trošku větší problém je komentář na řádce 4 — “Will be automatically filled”. V komentáři chybí na konci jen “Hope so”, jelikož tato třída nemá Guid povinný v constructoru a jeho setter není private. Takže v knihovní třídě pevně věříme, že za nás konzument udělá něco, co jsme si řekli, že je nutné a nezaručili.

A teď ten největší problém — jádro pudla. Třída upravuje porovnávání instancí, dvě instance se rovnají v momentě, kdy jejich ID je shodné. Todle však nelze takto v tomto případě nikterak zaručit. Říci o dvou domain entitách, že jsou shodné můžeme většinou pouze v případě, že:

a) jsou doménové entity immutable

b) celá datová vrstva je postavena na identity mapě (toto samo o sobě nestačí).

Pokud nemáme tedy zaručené některé další kroky, může být porovnávání instancí na základě ID zdrojem opravdu velkých problémů (dobrým příkladem je třeba práce s entitou nad více DbContexty entity frameworku — tam to projde i přes identity mapu a zachrání to až databáze kvůli PK violation výjimce.)

Závislost na externích knihovnách dovedla především Javascriptová komunita na úplně novou úroveň, kde mají “na všechno balíček”. Na zjištění jestli je něco pole — což je jedna řádka kódu, mají také IsArray balíček. Toto je ale opačný příklad, který vede k tomu, že se pak hroutí internety :-)


Co tedy s tím?

Nejzlatější pravidlo je prostě “neexistuje perfektní věc”. Neexistuje vždy ideální knihovna, neexistuje ideální postup, neexistuje univerzální framework. Nikdo to ještě nedokázal a nezkoušejte to ani vy, nepiště plošně všechno univerzálně a složitě “na první dobrou”. Snažte se dobrat co nejdříve prvního funkčního prototypu a iterativně vylepšujte. Nikdy nic není forcenuto jako konečné řešení, tak si tyto ploty nestavte.

Nesnažte se za každou cenu distribuovat veškerý váš firemní codebase skrze balíčkovací systémy, dostanete se jen a pouze do dependency hellu. Netvořte jakési firemní etalony pro jednotlivé vrstvy vašich projektů, pokud nemáte velké zkušenosti z RŮZNORODÝCH PROJEKTŮ.


Velkým problémem současné doby je také podlehání obrovskému hypeu kolem škálovatelnosti a řešením s tím spojenými. Zjistěte si opravdu reálnou předpokládanou zátěž vašeho systému a popřemýšlejte, co bude opravdu nejvíce vytěžováno — CPU bound věci se škálují strašně jednoduše, oproti tomu I/O bound zátěž a její moderní škálování přináší opravdu hodně starostí navíc (primárně s konzistencí dat a atomicitou operací).


Slovo závěrem

Dejte si dobrý pozor na programátorské poučky a pravidla. Velmi často bohužel fungují jen na papíře a jejich SLEPÉ DODRŽOVÁNÍ vám může přinést těžké spaní. Je poměrně obvyklé, že při setkání s reálným světem narazíte na funkční omezení — propustnost sítě, výkon databáze, škálovatelnost…

Většina řešení co fungují a jsou oblíbené, jsou naprosto primitivní věci, které s sebou ale často nesou jistý boilerplate. Buďte tedy open-minded a nezahazujte hned vše jen proto, že se tam X krát opakuje nějaký kus kódu, často to má svůj dobrý důvod a tím důvodem je ještě častěji možnost velmi jednoduchého refactoringu a vysoká čtivost kódu.

Téměř vždy je vývoj jakási osa mezi levným začátkem / dražším refactorem a mezi drahým začátkem a levnějším refactorem. Drahý začátek má ale smysl pouze v případě, kdy pozdější refactoring skutečně přijde a kdy pozdější refactoring bude DOSTAČUJÍCÍ ŘEŠENÍ.

Jakmile budu mít trošku času, připravím pár kódových příkladů :-)

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