Slevomat Coding Standard

English version of the article is available on Medium.

Každý vývojářský tým by měl mít k dispozici kvalitní podpůrnou infrastrukturu, která pomáhá zajišťovat a vynucovat konzistentní výstupy všech jeho členů. Coding standard je jednou z mnoha věcí, které drží projekt pohromadě.

Ze zdrojového kódu by nemělo být poznat, kdo ho psal. Celý tým by měl mít jednotné zvyklosti. Některé (způsob formátování) jdou kontrolovat automaticky, jiné (např. dodržování navržené architektury) je potřeba řešit při code review.

Ve Slevomatu máme již rok a půl velmi striktní standard, který nám v každém pull requestu za pomoci PHP_CodeSnifferu kontroluje Jenkins. Před šesti měsíci jsme k základu v podobě Consistence Coding Standardu přidali řadu pokročilých sniffů, které jsme měli provizorně commitnuté v našem privátním repozitáři. Některé podporují i automatické opravy, což ulehčuje jejich integraci do projektu.

Na dnešek jsem čekal hodně dlouho. Tyto sniffy konečně vydáváme jako open-source pro veřejné použití. Vypíchnu zde ty, které považuji za nejzajímavější a nejužitečnější:

Nepoužité privátní properties a metody

SlevomatCodingStandard.Classes.UnusedPrivateElements

PHP_CodeSniffer je sice nevhodný pro statickou analýzu zdrojových kódů, protože umí analyzovat najednou pouze jeden soubor a nemá přístup k reflexi, ale některé specifické kontroly pomocí něj provádět jdou. Tento sniff detekuje nepoužité a write-only privátní properties a nepoužité metody, které lze bez obav smazat. Díky tomuto sniffu se pravidelně zbavujeme mrtvého kódu při refaktoringu a zbytečně injektovaných nepoužívaných závislostí v konstruktoru.

Čárka za posledním prvkem pole

SlevomatCodingStandard.Arrays.TrailingArrayComma

Čárka za posledním prvkem ve víceřádkovém poli zjednodušuje přidávání dalších prvků a zpřehledňuje verzovací diffy.

Zákaz Yoda podmínek

SlevomatCodingStandard.ControlStructures.YodaComparison

Pokud máte radši klasické pořadí při zápisu podmínek namísto Yoda stylu, tento sniff dokáže najít a dokonce i automaticky opravit prohřešky.

Abecedně seřazené uses

SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses

Dobrý coding standard by měl vynucovat sjednocení podoby veškerého kódu, co jde objektivně sjednotit. Tento sniff odbourává hádky o tom, jak řadit importy z jiných jmenných prostorů na začátku každého souboru.

Nepoužité uses

SlevomatCodingStandard.Namespaces.UnusedUses

Další detekce mrtvého kódu. Proč trpět nadbytečné uses, když je tento sniff umí najít a dokonce i smazat? Poradí si i s Doctrine anotacemi.

Začněte je používat ještě dnes!

Zmíněné sniffy nejsou jediné, které jsme dnes vydali. Jejich kompletní výčet a vyčerpávající návod k použití celého standardu, ale i třeba jen pár jednotlivých sniffů, společně se zdrojovými kódy naleznete na GitHubu. Kód považujeme za tak prozkoušený a stabilní, že jsme neváhali vydat verzi 1.0.0.

Detekce neuzavřených transakcí

Vezměte si následující kód:

public function processRow(Row $row)
{
    $this->databaseConnection->begin();
    try {
        // nějaké počáteční kontroly
        // $isProcessed = ...

        if ($isProcessed) {
            return;
        }

        // spousta práce se zpracováním row

        $this->databaseConnection->commit();
    } catch (\Exception $e) {
        $this->databaseConnection->rollback();
        throw $e;
    }
}

Spatřili jste tu chybu? V případě, že $isProcessed === true, dojde k opuštění metody bez commitu či rollbacku transakce, čímž zůstane v databázi otevřená až do uzavření spojení. A může zbytečně držet příliš dlouho zámky, o které mohou mít zájem ostatní vlákna aplikace.

Pokud by vám nevadily zamčené řádky, tak se vám stejně nebude líbit další důsledek tohoto bugu:

$this->databaseConnection->begin();
try {
    // volaná metoda vyhodnotí předaný řádek jako $isProcessed
    $this->processRow($fooRow);
    $this->databaseConnection->commit();
} catch (\Exception $e) {
    $this->databaseConnection->rollback();
    throw $e;
}

V tomto případě totiž commitujete či rollbackujete jinou transakci, než byste čekali!

S lepším transakčním API se nastalá situace dá detekovat:

$databaseTransaction = $this->databaseConnection->begin();
try {
    // volaná metoda vyhodnotí předaný řádek jako $isProcessed
    $this->processRow($fooRow);
    $databaseTransaction->commit();
} catch (\Exception $e) {
    $databaseTransaction->rollback();
    throw $e;
}

V tomto případě je databázová transakce reprezentovaná odděleným objektem, takže je vždy jednoznačně určeno, s jakou konkrétní transakcí pracujeme.

Jak ale detekovat, že nám v aplikaci zůstává viset nevyřešená transakce? Pomůžeme si destruktorem:

class DatabaseTransaction
{

    //...

    public function __destruct()
    {
        if (!$this->resolved) {
            // destruktor nemůže vyhazovat výjimky
            trigger_error('Unresolved transaction!', E_USER_NOTICE);
        }
    }

    //...

}

Kdy ale PHP destruktor objektu zavolá?

The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

V závislosti na podobě implementace DatabaseConnection se destruktor zavolá jakmile PHP opustí metodu, ve které proběhlo přiřazení $databaseTransaction, a nebo taky ne, třeba v případě, že si všechny transakce ukládáme do interního pole pro pozdější odkazování.

Pokud se tedy destruktor transakce zavolá ihned po opuštění metody, tak v zalogované chybě dostaneme stack trace, ve které snadno dohledáme to místo, kde je špatně ošetřená transakce, a budeme ho moci opravit.

Pokud se destruktor zavolá až při shutdown sekvenci, tak už nám stack trace chyby o místu, kde vzniká neošetřená transakce, nic neřekne. V případě, že máte velkou mnohovrstevnatou aplikaci s mnoha transakcemi, se takové místo hledá velmi těžko.

Přišel jsem ale s funkčním řešením, které v této situaci debugging transakce usnadní. V PHP lze instanciovat výjimku, aniž bychom jí vyhazovali, a takto vytvořená výjimka si v sobě nese stack trace z místa svého vzniku!

class DatabaseTransaction
{

    /** @var UnresolvedTrasactionException */
    private $originException;

    public function __construct(DatabaseConnection $databaseConnection/*, ...*/)
    {
        // ...
        $this->originException = new UnresolvedTrasactionException();
    }

    public function __destruct()
    {
        if (!$this->resolved) {
            \Tracy\Debugger::log($this->originException);

            // destruktor nemůže vyhazovat výjimky
            trigger_error('Unresolved transaction!', E_USER_NOTICE);
        }
    }

    //...

}

Pro každou započatou transakci tedy vytvořím výjimku, kterou v případě nevyřešené transakce zaloguji a získám tak místo, kde chybná transakce vznikla. V destruktoru stále vyvolávám notice, aby si programátor této chyby všiml i během vývoje, kdy běžně složku s logy nesleduje.

Z hlediska čistoty a architektury kódu jde samozřejmě o dost neobvyklé až šílené řešení, které bych nikdy při code review nevpustil do běžné business logiky aplikace, kde si vývojář musí vystačit s běžnými vyjadřovacími prostředky a návrhovými vzory z OOP, ale v tomto případě jde o infrastrukturní pomocnou záležitost, která se v celé aplikaci vyskytuje právě jednou a nijak neovlivňuje architekturu zbytku aplikace. Pro usnadnění debuggingu a tedy ušetření času vývojáře jsem ochotný překročit hranici, ke které bych se běžně vůbec nepřiblížil.

What is Code?

Naprosto epický článek extrémní délky (38 tisíc slov) se spoustou animovaných ilustrací, který popisuje vše okolo vývoje software, co potřebujete vědět. Od pochopitelného popisu, jak funguje procesor a co všechno se musí stát, aby se znak stisknutý na klávesnici zobrazil na obrazovce, přes programovací jazyky, knihovny, algoritmy, debugging, datové struktury, databáze a verzování, až po vývojářské konference, flamewary a management. Pokud zvažujete nebo již máte kariéru v softwarovém vývoji, tohle je pro vás skutečně must-read.

Začněte monolitem

Martin Fowler reaguje na aktuální módu mikroservisní architektury tvrzením, že začínat vývoj nové aplikace od mikroservis je nebezpečné, protože dopředu nevíte, jak do nich projekt rozdělit, a spíše si špatně aplikovanou architekturou uškodíte.

Any refactoring of functionality between services is much harder than it is in a monolith. But even experienced architects working in familiar domains have great difficulty getting boundaries right at the beginning. By building a monolith first, you can figure out what the right boundaries are, before a microservices design brushes a layer of treacle over them.

Mikroservisy jsou sice výhodnější z dlouhodobého hlediska, protože lze vývoj projektu rozdělit a škálovat do více nezávislých týmů, ale zároveň s sebou nese i nevyhnutelnou režii při navrhování a implementaci API, reagování na selhávající požadavky, nemožnost stahovat si všechna data z jedné databáze naráz apod. A pokud vyvíjíte něco nového, nevíte, zdali daný projekt bude mít takový úspěch a růst, aby se mikroservisy a pomalejší příchod na trh vyplatily.

When you begin a new application, how sure are you that it will be useful to your users? It may be hard to scale a poorly designed but successful software system, but that‘s still a better place to be than its inverse. As we‘re now recognizing, often the best way to find out if a software idea is useful is to build a simplistic version of it and see how well it works out. During this first phase you need to prioritize speed (and thus cycle time for feedback), so the premium of microservices is a drag you should do without.

Objektivní srovnávání technologií

David Grudl na Twitteru vyzval vývojáře ke srovnání vývoje webové aplikace v Nette oproti řešení v JavaScriptu. Ať už by v rychlosti vyhrála jakákoli ze soutěžících technologií, zastávám názor, že to nic nevypovídá o její vhodnosti pro dlouhodobý vývoj a údržbu seriózních aplikací.

Tato srovnání mají mnoho společného se syntetickými benchmarky. To, co se v nich testuje, neodpovídá tomu, co v běžném provozu aplikace provádí, a zároveň hrubý výkon frameworku není to hlavní kritérium, které by vývojáře při výběru mělo zajímat.

Při vývoji není důležité, za jak dlouho dokáže vývojář na zelené louce nabušit první verzi aplikace podle pevného zadání, ale řada jiných kritérií:

  • Rozšiřitelnost a udržovatelnost aplikace, jak snadné je ve zdrojovém kódu provádět změny
  • Testovatelnost a pokrytí testy
  • Stav a předatelnost zdrojových kódů jinému vývojáři či týmu
  • Přívětivost uživatelského rozhraní
  • Dostupnost a cena vývojářů se znalostí technologie
  • Dokumentace, komunita a budoucnost technologie

Sám jsem se před třemi lety účastnil Souboje frameworků. Úkolem bylo, podobně jako v Davidově výzvě, za jeden den vyvinout e-shop s administrací. Každý ze soutěžících k úkolu přistoupil jinak a výsledné pořadí ne nutně odpovídalo tomu, jak by si daná aplikace vedla dlouhodobě. Např. vítězný Jakub Vrána ke tvorbě administrace použil svůj Adminer Editor, což mu v soutěži ušetřilo spoustu času, ale generovaná administrace na základě definice tabulek v relační databázi neposkytuje žádný prostor pro customizaci chování a tudíž přináší nižší uživatelský komfort.

Do srovnání samozřejmě promlouvá i zkušenost jednotlivých účastníků – a to jak celkově s programováním, tak s konkrétní soutěžní technologií.

Jak tedy postupovat při výběru technologie, když ji nám nepomůže vybrat jednodenní hackaton? Když jsme na konci loňského srpna začínali s vývojem nového nákupního košíku na Slevomatu, věděli jsme, že chceme, aby celý fungoval jako single-page aplikace a veškerá komunikace se serverem probíhala pomocí AJAXu. Potřebovali jsme technologii, která by do šablony na straně klienta promítala stav dat na serveru. Jako vhodné kandidáty jsme vybrali React, Knockout a šablonovací engine Handlebars. Strávil jsem tři dny porovnáváním těchto technologií tak, že jsem v každé naimplementoval první krok košíku, a posléze porovnával, která technologie je nejbližší tomu, jak ve firmě vývoj probíhá, odhadoval, s čím by mohly být v budoucnu problémy a jak snadno nám to či ono pomůže řešit. Vybral jsem Knockout a košík jsme o dva měsíce později úspěšně spustili bez nějakých větších problémů na klientu ani na serveru.

Vybral jsme tedy správně? Zádrhel spočívá v tom, že to nelze určit. Jak by vývoj probíhal v případě, kdybych zvolil React? Možná by byl košík hotový o dva týdny dříve, ale po spuštění bychom přišli na nepříjemný a těžko odladitelný bug. Možná bychom termín nestihli. Možná by byl při překreslování DOMu výkonnější, ale kodér by nedokázal upravovat jeho šablony. Pokud by nás opravdu zajímalo, zdali jsme vybrali dobře, mohli bychom se pokusit celé dva měsíce vyvíjet paralelně v obou technologiích zároveň, což by jednak stálo více peněz, a druhak bychom opět narazili na různou úroveň zkušeností vývojářů, srovnání by tedy opět nebylo objektivní.

Pokud by mě po dokončení košíku v Knockoutu zajímalo, jak by si vedl React, a pustil se do implementace v něm sám, opět bych nedokázal technologie srovnat objektivně, protože jsem při vývoji první verze nabral zkušenosti, které bych v Reactu aplikoval a dokázal se tak vyhnout některým slepým uličkám. Spravedlivé srovnání tedy nejenže nelze získat od více různých vývojářů, ale ani od jednoho.

Při výběru technologie berte vpotaz, jaká od ní máte očekávání a zdali je dokáže splnit, dovednosti a zkušenosti lidí, kteří s ní budou pracovat, a moc se netrapte úvahami, jak by projekt mohl dopadnout, pokud byste se na začátku rozhodli jinak. 1

  1. A určitě nehleďte na výkonnostní benchmarky, ankety popularity a co používá vaše oblíbená osobnost.