pátek 30. listopadu 2018

Refactoring, část IV: Odstraňování duplicit

Co je špatně?

Duplicitní kód se sice rychle a snadno generuje (podobně jako diplomky politiků), ale později se ve zlém vrátí tím spíš, čím víc "klonů" existuje. Scénář katastrofy je obvykle rutinní:
  1. Uživatel nahlásí chybu nebo je třeba implementovat novou vlastnost
  2. Chyba se opraví - čili klon se liší od svých pravzorů
  3. Jiný uživatel reklamuje chybu v jiném klonu
  4. Chyba se opraví - obvykle jinak než v prvním klonu.
  5. Ostatní klony jsou neopravené. Trpí stejným problémem? Netrpí jím? Jak to, že fungují správně? Je část klonovaného kódu v jejich případě zbytečná? Nebo nefungují správně, jen ještě nikdo v jejich případě chybu nereklamoval?
  6. Rozdíly narůstají, chyby někde mizí, jinde zůstávají nebo se dokonce mění. Technický dluh roste exponenciálně.

Konfigurace CPD, Copy And Paste Detector


CPD je součástí PMD maven pluginu, čili když už, nejspíš budete používat oba. Kontrolám PMD samotného se ale vyhnu, to si najděte jinde ;)
CPD porovnává soubory a hledá v nich opakující se sekvence "tokenů". Algoritmus lze trochu konfigurovat, viz dokumentace pluginu.

                <plugin>
                    <artifactId>maven-pmd-plugin</artifactId>
                    <version>3.11.0</version>
                    <executions>
                        <execution>
                            <id>cpd</id>
                            <goals>
                                <goal>cpd</goal>
                            </goals>
                            <phase>verify</phase>
                            <configuration>
                                <skip>${cpd.skip}</skip>
                                <minimumTokens>100</minimumTokens>
                                <excludes>
                                    <exclude>**/en/**/*</exclude>
                                    <exclude>**/generated/**/*</exclude>
                                </excludes>
                            </configuration>
                        </execution>
                        <execution>
                            <id>cpd-check</id>
                            <goals>
                                <goal>cpd-check</goal>
                            </goals>
                            <phase>verify</phase>
                            <configuration>
                                <skip>${cpd.skip}</skip>
                                <printFailingErrors>true</printFailingErrors>
                                <verbose>true</verbose>
                            </configuration>
                        </execution>
                    </executions>

Pokud plugin najde dvě stejné sekvence delší jak 100 tokenů, shodí build a vypíše nalezené duplicity. Ty pak budete muset řešit.

Řešení

Fuj: Přehodím řádky

Ano, z pohledu CPD jste z toho venku, ale po pravdě ... vážně myslíte, že jste se problému zbavili? Ve skutečnosti jste se zbavili jen jeho hlášení, problém trvá. Navíc jste si ještě zhoršili orientaci ve třídě.

Fuj: Zvýším minimumTokens

... a zbavím se hlášení. Problém trvá, ale co hůř, ono to často ani nezabere, protože těch duplicit je víc, byť i jen na málo řádcích. Pointa je právě v tom, co vlastně je ten "token". Ta stovka je docela rozumná, proto je taky jako default.

Exkluze

Ta funguje stoprocentně. Není ovšem určená k ignorování skutečných problémů, nýbrž k ignorování falešných hlášení problémů. Může být nalezená duplicita, skutečná duplicita, falešným hlášením? Je třeba si uvědomit, že cílem není zbavit se jakýchkoliv duplicit, ale zbavit se hrozících problémů s duplicitami, zlepšit architekturu, uklidit.

Do exkluze tudíž typicky dávám jednak kód generovaný, se kterým nic nenadělám, jednak třeba JPA entity, které mohou mít velmi podobné vlastnosti, přestože vzájemně nijak nesouvisí. To, že se něco stejně jmenuje, nemusí znamenat, že je to duplicitní - a CPD s oblibou hlásí stejné čtyři settery a gettery, jdoucí za sebou.

Problém může být, pokud nemáte žádnou konvenci, jak třeba JPA entity odlišit od jiných tříd. Hodně štěstí ;-)

Vyvedení do samostatné metody ve stejné třídě

Samozřejmost, pokud je duplicitní kód jediné třídy. Nebo ne? Pokud na 10 řádkách máme 8 lokálních proměnných, tohle nebude smysluplná cesta. Můžeme je zkombinovat do jednoho výpočtu, jenže ... to zásadně zhorší čitelnost, takže taky ne. Takže jiná možnost.

Vyvedení do společného rodiče

Je to možnost, ale je třeba zvážit, zda vyváděná funkcionalita do rodiče opravdu patří. Pokud ostatním potomkům rodiče je tato funkcionalita cizí a nemají s ní nic společného, nedělejte to. Jedině že byste stvořili dalšího "mezirodiče", ale pokud mu nedokážete dát smysluplný název, budu se opakovat - nedělejte to; příliš vysoká hierarchie rodičů a potomků je další problém, čili je tu riziko, že odstraněním jednoho problému vytvoříte jiný.

Vyvedení do "utility" nebo "cizí" třídy

Utilitky, helpery, atp., jsou víceméně neobjektová věc, na druhou stranu netrpí problémy s dědičností, dobře se na ně píšou testy, takže tuhle možnost vůbec nezatracujte.
Podobně je možné, že víte o třídě, kam tento kód vlastně i logicky patří a lze ho odsud sdílet na potřebná místa. Předpokladem je, že vyvedenému kódu dokážete vytvořit nějaké pěkné smysluplné API. Ale to se vlastně týká každého "vyvádění".

Vyvedení do default metody interface

Óóó, jak snadné a efektivní!
Jenže to má háček - nese to riziko. Implementace z abstraktního rodiče, implementující stejnou metodu, má vyšší prioritu než jakýkoliv interface!!!
Vážně si rozmyslete, jestli vyváděný kód může opravdu sloužit jen jako pouhý "default". V případě kolizí pak musíte metodu stejně přetížit a z těla volat Iface.super.metoda(), jinak řečeno musíte říct, kterou z dostupných implementací chcete používat.
Pokud máte ale v rodiči metodu implementovanou, complier vám nic neřekne a neporadí, předpokládá, že to vážně chcete.

Silver bullet: přečtěte si to

Překvápko: někdy není třeba duplicitní blok "vyvádět", stačí oba duplicitní bloky naprosto stejně zjednodušit. Třeba odstranit 10 opakovaných přetypování. Nebo jen něco, co se na obou místech otravně opakuje, vyvést ven a sdílet. Výsledek? Ubyde tokenů, jak prosté, už to nevypadá jako hloupý copy and paste, jen něco, co dělá stejnou věc, a to už není známka copy and paste.

Svět je zase v pořádku :-)

pátek 27. července 2018

Paralýza bez analýzy


Kašleme na ně, radši vyhyneme!

Je známým faktem, že tzv. „antipatterny“ si lidé pamatují snáze než „patterny“, čili vzory. Týká se to i pojmu Analysis-Paralysis (analýza-paralýza), nebo naopak „Extinct by Instinct“ (vyhynutí instinktem). Přitom spousta vývojářů už někdy slyšela o dokumentech, kterým se říká „detailní návrh systému“, „specifikace požadavků“, UML diagramy, „use case“, „popisu chování“, … atd.

Typický vývojář se hrozně těší, až si do svého výtvoru prvně klikne myší, pošle mu zprávu a v ideálním případě dokonce dostane odpověď, až to zkrátka konečně začne něco dělat. A tak se vykašle na analýzy a dokumentaci, a začne bastlit, během čehož si teprve začne uvědomovat, co všechno ještě bude muset přidat, předělat, průběžně upravit svoje původní nápady, ale zároveň nerad maže to, co už udělal.
A když už to má všechno za sebou, nenávidí opravování chyb, a za nic na světě nezačne psát automatické testy – čím jsou náročnější, tím větší je odpor. A náročnější jsou tím víc, čím složitější je implementace i rozhraní, i čím složitější je popis chování aplikace.

No počkat, „popis chování aplikace“? Ten ale neexistuje, jeho jediná instance je ve vývojářově hlavě, a permanentně se mění, nakonec dojde ke vzpouře mysli, která ve výsledku začne prosazovat chyby jako chtěné vlastnosti. Vývojář se hrubou silou pokusí protlačit svůj mizerný geniální výtvor přes jakákoliv akceptační pravidla týmu, šéfů, zákazníka, kohokoliv, jen aby to měl za sebou.

Kolikrát se tohle zopakuje, než vývojář „vyhoří“?
Kolikrát se tohle zopakuje, než s ním tým ztratí trpělivost?
Kolikrát se tohle zopakuje, v kolika týmech, u kolika zaměstnavatelů, u kolika zákazníků?

Mockrát.
Zákazník si zvykne a bojí se, že po utracených milionech to jinde bude zase stejné.
Zaměstnavatel má problém sehnat jakéhokoliv vývojáře, snaží se udržet si i ty špatné.
Nejmocnější je podle mého soudu tým samotný, dobrý tým se musí nutně zbavovat členů, kteří mu kazí pověst i dílo, bez ohledu na to, že takovými změnami přidělává práci personalistům firmy i svým šéfům. Samozřejmě to je až krajní stav, kdy dotyčného nelze proškolit, kdy se nedokáže sžít s fungováním kvalitního týmu. Naopak pokud není kvalitní tým jako celek a nelze z něj přiměřeným úsilím kvalitní tým učinit, není důvod v něm setrvávat.

Žádné násilí ale není nutné.

Někteří vývojáři dokonce tvrdí, že je vysoká škola nic nenaučila. Zapomínají, co všechno se jim dostalo do podvědomí, přestože si nepamatují podrobnosti. Přehlížejí, že je škola naučila se nejen učit, ale i zapomínat nepotřebné detaily, vybírat jen to podstatné a detaily vyhledávat a chápat. Schválně, víte nebo aspoň tušíte, o čem je řeč? Jak často si na to vzpomenete při vývoji?

  • O(n)
  • B-tree
  • SHL, SHR
  • BCNF
  • Transformační matice
  • Konečný automat
  • Spolehlivost systému
  • Korelace jevů
  • Riemannův integrál
  • Svobodovy mapy
  • Násobení matic
  • Rekurze
  • Podmínka nutná, nikoliv postačující.
Vývojáři rádi vyvíjejí, vynalézají, ale často neradi zapisují myšlenky a ověřují je, hledají souvislosti. K tomu musí časem dospět, obvykle s trochou donucení kvalitním týmem. Ano, kvalitní tým píše dokumentaci a píše analýzy – ne však protože je někdo vyžaduje, ale protože jsou užitečné!

Jak překonat odpor


Jak? Sněním. Už máte něco za sebou, tak si vzpomeňte, jak jste se v tom programování ztráceli minule. A co všechno jste už mohli vědět předem, kdybyste si dali tu práci. Ne, nepotřebujete začínat UML diagramy a těmi obrázky, které vám vnutili ve škole. Začněte smluvním popisem, který proberete se zadavatelem – nemusíte hned psát knihu, stačí sepsat si na papír co víte, a co byste chtěli vědět:
  • co aplikace má dělat
  • co aplikace nemá dělat (hodí se vytyčit „hranice“, odkázat se na související funkcionality)
  • jakým způsobem bude interagovat s klientem (browser, ws, db, ...)
  • kdo je klientem (server, člověk, jiná aplikace, …)
  • kolik je klientů (jeden? tisíc? lze počet regulovat? jak?)
  • jaký bude mechanismus kontroly oprávnění, co vše bude zohledňovat?
  • pokud jde o uživatelské rozhraní, jak bude vypadat, jak se bude chovat (co bude na formuláři, co se stane, když uživatel klikne sem nebo tam, má mu textové pole napovídat a jak?)
Pak pokračujte o něco blíž programování ...
  • jaký bude vliv na zátěž HW, nároky na disk, paměť, sítě?
  • alternativy technologií i přístupů
  • rizika – kde se dá očekávat problém? Dá se předejít prototypováním, matematickým modelováním, sběrem a statistickou analýzou dat?
Po pravdě taky se mi nechce psát analýzy, ale chuť se dá vylepšit právě prototypováním (něco naprogramujete, ale pak to vyhodíte, jen si zkusíte nějakou cestu) a i samotným faktem, že z napsané analýzy se všemi těmi nápady kolem má její autor mnohem lepší pocit, už protože ho navštívila řada dalších nápadů a inspirací, co s čím souvisí a jak by to asi mohlo být dobře.

Pravda, tu chuť obvykle trochu pokazí zákazník nebo kolegové-oponenti, ale po pár iteracích mají nakonec lepší pocit všichni a těší se, až to uvidí „žít“.

Dvakrát měř, jednou řež


Pozitivní je též to, že už se obvykle nezmýlíte v odhadu pracnosti řádově, byť tento odhad obvykle všechny šokuje, nakonec se ho dokonce možná i podaří dodržet, což je známka profesionality – samozřejmě se objeví také nápady, co seškrtat nebo odložit.

Při vývoji software není tolik důležitý materiál, o to důležitější je ale čas, který se velmi špatně odhaduje. Pro vysoké odhady obvykle nemá zákazník pochopení, na druhou stranu se rozhodně nevyplatí mu lhát a dávat mu jakkoliv optimistický odhad. Mnohem lepší je dát odhad spíše pesimistický a později třeba i zákazníkovi nabídnout slevu (což se ovšem málokdy stává, dělá to ale výborný dojem). 

Ošizením analýzy nebo interních testů a kontrol se také nic získat nedá, naopak dodáním „milestone“ verze k otestování zákazníkem nebo dokonce zatažením zákazníka do testování aplikace se dá vyhnout spoustě nedorozumění i problémů při akceptaci.
Tím spíš je pro obě strany dobré mít vše „na papíře“ - nikdy by pak neměl nastat stav, kdy zákazník dluží dodavateli několik milionů za vývoj aplikace, které odmítá zaplatit s tím, že půl roku o dodavateli neslyšel a nakonec se dodavatel pokusil předat něco, co není zákazníkovi k ničemu.

Interní (hlavně automatické) testy a kontroly však rozhodně neslouží k náhradě zákazníka, nýbrž k pokrytí celé řady cílů. Zákazník téměř nikdy netestuje aplikaci kompletně, ale spíše ověřuje, jestli plní jeho očekávání. Při tomto ověřování může dojít i ke změně požadavků zákazníka a prodražení aplikace – následovat musí dohoda, jak bude změna financována a jak je velká oproti stávajícímu zadání.

Automatické testy naopak pokrývají na různých úrovních:
  • zafixování již implementovaného chování
  • otestování funkcionalit, které testera napadly, nicméně není žádná záruka, že na ně narazí při ověřování zákazník (!)
  • otestování hraničních případů
  • otestování různých uživatelských scénářů
Z toho celkem jasně vyplývá, že část pokrytí kódu testy je nutně duplicitní – a přitom je velmi obtížné dosáhnout pokrytí 100%. Ten háček je ovšem v tom, že klíčové není pokrytí kódu testy, nýbrž pokrytí všech možných (tj. i velmi nepravděpodobných) scénářů včetně chybových. Vypočtené pokrytí kódu testy je jen pomůcka, protože žádný software nikdy nedokáže posoudit, jaký rejstřík kombinací stavů a událostí vůbec může nastat, natož které jsou vlastně důležité. 

Ve výsledku tudíž není až tak důležité procento pokrytí, mnohem důležitější je procento nepokrytého kódu, tj. kódu, přes který žádný automatický test ze všech sad vůbec neprošel. Není zrovna tam chyba? Dělá to to, co chcete?
Samozřejmě ani naopak pokrytí kódu testy neříká, že pokrytý kód je správně – pouze to, že nějaký test tudy prošel. Znovu tudy opakuji, pokrytí kódu testy jsou jen pomůcka; podmínka nutná, nikoliv postačující.

Proč jsem sklouzl k testům?


Protože první testy nelze psát na základě ničeho jiného než je nějaká forma analýzy, obzvlášť u přístupu „test driven development“, alespoň pokud chcete minimalizovat psaní zbytečného kódu i testů, které později zahodíte nebo v horším případě neužitečně ponecháte v aplikaci, čímž pro změnu prodražíte budoucí údržbu.

Prostě to udělejte dobře – a ne, tohle vážně není Vodopádový model vývoje, vše se dělá iterativně. Už na začátku je ale dobré mít alespoň hrubou představu, co že to vlastně vyvíjíte, a během chvíle doženete „rychlíky“, kteří se řídí instinktem, díky kterému se ale brzy začnou točit v kruhu. 
 
Myslíte, že ne?

P.S.: Příště už radši něco praktického ... ;)