čtvrtek 27. ledna 2011

Princip jedné odpovědnosti

Princip jedné odpovědnosti (SRP - Single Responsibility Principle) je jeden z pěti základních principů metodologie SOLID objektového návrhu. Jedna z jeho nejčastějších definic zní:

Třída by měla mít právě jeden důvod ke změně.

Třída by tedy měla mít pouze jednu odpovědnost - pouze jednu logickou funkcionalitu. Svou definicí patří SRP mezi nejjednodušší principy objektového návrhu, nezkušení objektoví návrháři však tento princip často ve svých programech porušují.

Vysvětleme si problém na příkladě třídy Uzivatel:

public class Uzivatel
{
    public String Id { get; set; }
    public String Heslo { get; set; }

    public void Ulozit(IUloziste uloziste)
    {
        uloziste.Ulozit<Uzivatel>(this);
    }
}

Třída nese informace o uživateli (Id a Heslo) a zajišťuje jeho uložení do úložiště, např. databáze. Zkušenější čtenáři ihned rozpoznali porušení princip jedné odpovědnosti. První odpovědností třídy je reprezentace entity typu uživatel v systému (veřejné atributy Id a Heslo). Druhou odpovědností je uložení stavu objektu do úložiště - metoda Ulozit. Provedeme refaktorizaci do dvou samostatných tříd:

public class Uzivatel
{
    public String Id { get; set; }
    public String Heslo { get; set; }
}

public class UzivatelDao
{
    private IUloziste uloziste;

    public UzivatelDao(IUloziste uloziste)
    {
        this.uloziste = uloziste;
    }

    public void Ulozit(Uzivatel uzivatel) 
    {
        uloziste.Ulozit<Uzivatel>(uzivatel);
    }
}

Ze třídy Uzivatel jsme odstranili metodu Ulozit. Vznikla nová třída UzivatelDao, jejíž odpovědností je zpřístupňovat funkcionalitu související s úložištěm entit Uzivatel, tedy například i uložení.

Pro úplnost zjednodušená definice rozhraní IUloziste:

interface IUloziste
{
    void Ulozit<T>(T objekt);
}

Proč je nutné princip dodržovat

  • Čitelnost kódu. Někdy dochází k obavám, že rozdělením větší třídy na více menších jednoúčelových tříd se zvýší složitost systému a bude problematické porozumět celku. Opak je pravdou. Navigace ve třídách, které dělají to, co jejich název deklaruje, je mnohem efektivnější. Budete-li procházet velké třídy s více odpovědnostmi, budete zatěžování další funkcionalitou, která Vám bude překážet v nalezení toho, co hledáte. Robert C. Martin přirovnává třídy mající více odpovědností k velké krabici v garáži, ve které máte naházeno všechno různé nářadí. Jednoúčelové třídy naopak působí jako úhledně urovnané malé krabičky obsahující pouze související věci jednoho typu.
  • Robustnost. Rozdělením tříd podle odpovědnosti docílíme toho, že případné pozdější požadavky na změnu ovlivní pouze třídu, které by se měly týkat. Např. začnete evidovat nový atribut bool PovolenPristup třídy Uzivatel. Rozšíříte třídu Uzivatel, ale třída UzivatelDao tím nebude nijak zasažena.
  • Snížení vzájemné provázanosti tříd. Velké třídy budou potřebovat vazby na více jiných tříd. Např. v našem příkladě problematická třída Uzivatel odkazovala rozhraní IUloziste. Ale entita typu uživatel je přeci v systému na business vrstvě nezávislá na způsobu jeho uložení.
  • Testovatelnost. Třída mající pouze jednu odpovědnost je lépe testovatelná. Souvisí to také se snížením provázanosti tříd. Test bude omezen pouze na jednu funkcionalitu. Např. pro otestování třídy Uzivatel nebudete potřebovat instanci IUloziste.
  • Soudržnost. Malé třídy mají větší soudržnost, která je v OO návrhu doporučovaná. Více o soudržnosti v některém z dalších příspěvků.

Jak odhalit porušení principu jedné zodpovědnosti

Důležité je samozřejmě získat zkušenosti s objektovým návrhem. Měli byste být také schopni se kriticky ohlédnout za dokončenou prací a zhodnotit dodržování SRP. V případě potřeby třídy refaktorovat.

Jedna poučka říká, že pokud při popisu významu třídy použijete spojku "a", měli byste zbystřit a prověřit dodržování SRP. Podezřelé jsou však i souvětí jiného typu. Pozor na třídy typu "reprezentace auta a výpočet jeho spotřeby", "sestava se zpracováním parametrů", "kreditní karta, která si umí sama strhnout poplatek", apod.

Dejte si pozor na slučování reprezentace entity a funkcionality jejího vytvoření. V těchto případech lze refaktorovat funkcionalitu vzniku instance třídy podle některého z návrhových vzorů pro vytváření objektů.

Další zdroje a ukázky

úterý 25. ledna 2011

Najdi chybu 01

Zadání

Vývojář implementuje práci s objednávkami. Potřebuje zformátovat popis objednávky pro další použití, např. odeslání emailem. K dispozici má třídy reprezentující osobu (Osoba) a objednávku (Objednavka). Rozmyslete si, co je v níže napsaném kódu špatně a proč:

public class Osoba
{
    public String Jmeno { get; set; }
    public String Prijmeni { get; set; }
    public String Titul { get; set; }
    public String VedeckaHodnost { get; set; }
}

public class Objednavka
{
    public Osoba Vystavil { get; set; }
    public Osoba Schvalil { get; set; }
    
    // ... další vlastnosti objednávky
}

   // ... někde v klientském kódu

    private String VratitPopisObjednavkyProEmail(Objednavka objednavka)
    {
        StringBuilder popisObjednavky = new StringBuilder();

        popisObjednavky.AppendFormat("Vystavil: {0}{1} {2}{3}\n", 
            (objednavka.Vystavil.Titul != null ? objednavka.Vystavil.Titul + " " : String.Empty),
            (objednavka.Vystavil.Jmeno != null ? objednavka.Vystavil.Jmeno : String.Empty),
            (objednavka.Vystavil.Prijmeni != null ? objednavka.Vystavil.Prijmeni : String.Empty),
            (objednavka.Vystavil.VedeckaHodnost != null ? ", " + objednavka.Vystavil.VedeckaHodnost  : String.Empty)
            );

        popisObjednavky.AppendFormat("Schválil: {0}{1} {2}{3}\n",
            (objednavka.Schvalil.Titul != null ? objednavka.Schvalil.Titul + " " : String.Empty),
            (objednavka.Schvalil.Jmeno != null ? objednavka.Schvalil.Jmeno : String.Empty),
            (objednavka.Schvalil.Prijmeni != null ? objednavka.Schvalil.Prijmeni : String.Empty),
            (objednavka.Schvalil.VedeckaHodnost != null ? ", " + objednavka.Schvalil.VedeckaHodnost : String.Empty)
            );

        return popisObjednavky.ToString();
    }


Řešení

Zřejmě došlo k porušení principu DRY (neopakování se). Zformátování celého jména osoby je na dvou místech v identickém tvaru a je předpoklad, že by se podobný kód mohl nadále množit. Důsledky tohoto častého programátorského nešvaru jsou:

  • Duplicitní kód. Problematické ladění. V naší ukázce je vidět, že pokud bude VedeckaHodnost == String.Empty, dojde k chybnému zformátování. Možná si toho autor na několika místech všimne a problém opraví. Ale nezapomene na některý výskyt?
  • Špatná čitelnost kódu. Informace o skutečném významu zformátování jména osoby je rozmělněna do čtyř řádků!
  • Snížení robustnosti kódu. Podařilo se Vám vyladit formátování jména osoby na všech místech aplikace. Jste spokojení. Pouze však do Té doby, než architekt systému vznese požadavek, že jméno osoby má být vždy zformátováno do tvaru Příjmení Jméno, Titul, Vědecká hodnost. To by Vám pak mohlo zkazit náladu ;-)
  • Vytvoření unit testů na správné formátování jména osoby je problematické.

Pokud si uvědomíte, že došlo současně k porušení pravidla zodpovědnosti třídy Osoba, začnete se blížit ke správnému řešení. Ano, třída Osoba by měla nabízet vlastnost CeleJmeno, v rámci které dojde k požadovanému zformátování. Dojde k úpravám, které nám kód pročistí, zlepší přehlednost a zbaví jej duplicit.

public class Osoba
{
    public String Jmeno { get; set; }
    public String Prijmeni { get; set; }
    public String Titul { get; set; }
    public String VedeckaHodnost { get; set; }

    public String CeleJmeno
    {
        get
        {
            return String.Format("{0}{1} {2}{3}",
                (String.IsNullOrEmpty(Titul) ? String.Empty : Titul + " "),
                Jmeno,
                Prijmeni,
                (String.IsNullOrEmpty(VedeckaHodnost) ? String.Empty : ", " + VedeckaHodnost)
                );
        }
    }
}

public class Objednavka
{
    public Osoba Vystavil { get; set; }
    public Osoba Schvalil { get; set; }
    
    // ... další vlastnosti objednávky
}

    // ... někde v klientském kódu

    private String VratitPopisObjednavkyProEmail(Objednavka objednavka)
    {
        StringBuilder popisObjednavky = new StringBuilder();

        popisObjednavky.AppendFormat("Vystavil: {0}\n", objednavka.Vystavil.CeleJmeno);
        popisObjednavky.AppendFormat("Schválil: {0}\n", objednavka.Schvalil.CeleJmeno); ;

        return popisObjednavky.ToString();
    }

pátek 21. ledna 2011

Principy DRY a WET aneb suché a vlhké programování

Princip DRY (Don't Repeat Yourself)

Princip DRY (Don't Repeat Yourself) vyjadřuje obecný princip neopakování se. Princip se dá aplikovat v mnoha oborech lidské činnosti. Z pohledu informačních systémů dostáváme definici:

Každý poznatek systému musí mít jedinou, jednoznačnou a určující implementaci.

Princip DRY nebo také DIE (Duplication Is Evil - Duplikace je Zlo) a jeho dodržování patří mezi základní hygienické zásady dobrého programátora. Nedodržování tohoto principu vede ke kódu typu WET (We Edit Terribly | Too much) - Editujeme strašně | příliš mnoho. V některých systémech a technologiích je porušování DRY běžné - HTML stránky, kód unit testů. Přepisování tohoto kódu podle principy DRY by bylo v tomto případě pracné a málo užitečné. V případě objektového programování je však dodržování principu DRY nezbytné a z dlouhodobého pohledu kriticky důležité.

V tomto článku si ukážeme, jaké zhoubné důsledky může porušování DRY principu přinášet. Podíváme se také na typické příčiny porušování a na techniky, které nám naopak jeho dodržování usnadní.

Důsledky porušení principu DRY

Každý z níže uvedených důsledků znamená navýšení pracnosti a nákladů projektu, vystresování programátora a obvykle i útok na vývojářovo lybido.

Zvýšený výskyt programových chyb. Nedodržení jednoznačnosti při implementaci jedné a té samé znalosti v systému vede ke množení podobného kódu a tím pádem i k nárůstu počtu chyb. Pokud duplikujete implementaci, je dosti pravděpodobné, že jste zduplikovali i programové chyby. Sémantické programové chyby se objevují rády při použití nulových a hraničních hodnot vstupních argumentů, v případě netradičních postupů a případů užití. V okamžiku, kdy se chyba projeví (a doufejme, že ji objevíte vy, případně tester, ale nedostane se až do produkčního prostředí), začnete s tradičním procesem ladění. Většinu chyb se v konečném čase podaří odladit. Ale v duplikovaném kódu musíte ladit na více místech a může se stát, že na některou z mnoha implementací stejné záležitosti stejně zapomenete. A to si pište, že se každá neodladěná chyba projeví v tu nejméně vhodnou dobu.

Pokud píšete jednotkové a integrační testy, jste nuceni duplikovat také kód testovacích tříd.

Problematická optimalizace a hledání úzkých hrdel systému. Nejen programové chyby ztěžují vývojářům život. Problematický návrh a implementace se projeví v neschopnosti dodržet výkonnostní požadavky na systém. Představte si situaci, kdy máte v programu tři metody, které implementují neefektivně identickou logiku. Vnější pohled na systém indikuje pomalost a vy chcete najít úzké místo - bottleneck. Použijete profiler a dostáváte statistiky volání jednotlivých metod. Počet vyvolání a celkový čas, převedený na procentuální podíl v běhu ke zbytku systému. Naše tři metody jednotlivě nefigurují v hitparádě pomalosti úplně nahoře. Ale v případě, že byste jejich logiku sloučili do jedné metody, rázem by byla favoritem na nejvyšší příčky. Tímto způsobem došlo k rozmělnění informací a jejich ukrytí. Optimalizace aplikace, které porušuje DRY může být hodně problematická.

Snížení čitelnosti kódu. Čitelný kód se lépe udržuje a ladí. DRY princip napomáhá zvýšení čitelnosti kódu. WET jej naopak toxikuje. Změna programátora starajícího se o příslušný subsystém napsaný s dodržením zásady DRY není kritická. Pokud si firma opečovává "senior" programátora, který jediný rozumí tomu, jak je stěžejní funkce systému implementována, je dost pravděpodobné, že tento programátor produkuje nesrozumitelný WET kód. V tomto případě by mělo být s dotyčným zahájeno kárné řízení a kód by se měl zavčasu přepracovat.

Problematická refaktorizace. Refaktorizace je denní chleba každého vývojáře. Pokud někdo říká, že nepotřebuje refaktorizovat je buď geniální programátor, lže nebo neví, co refaktorizace znamená. Pak je tu možná ještě jedna smutná varianta - používá IDE, které refaktorizovat neumí (bohužel znám z vlastní zkušenosti :-). Například jedna z refaktorizačních technik umožňuje z jednoho místa přejmenovat vlastnost třídy. Její název neodpovídal přesně jejímu významu, proto jste použili refaktorizaci a všechny reference na tuto vlastnost se nám automaticky upravily. Pokud jste nedodrželi DRY zůstalo Vám několik míst v kódu, kde se refaktorizace logicky projevit nemohla.

Snížení robustnosti systému. Robustností systému se v tomto případě rozumí stabilita systému v případě pozdějších zásahů do jeho implementace. Možná máte kus kódu typu pandořina skříňka, do kterého se bojíte zasáhnout, protože absolutně netušíte, kde všude se může chyba projevit. Možná Váš kód porušuje DRY a zřejmě i další principy dobrého návrhu kódu. Pokud například duplikujete logiku pro vyhodnocování oprávnění přístupu uživatele k nějaké entitě systému (např. bankovní účet) dostáváte se při budoucích úpravách do nepříjemných situacích. Ztrácíte pevnou půdu pod nohama a je otázkou času, kdy se stane něco hodně nepříjemného přímo v produkčním prostředí.

Příčiny porušení principu DRY

"Urychlující" programovací techniky označované jako Clone And Modify Programming nebo ekvivalenty Cut And Paste Programming, Copy And Paste Programming, Snarf And Barf Programming, RogueTile, Rape And Paste Programming. Pokud je na programátora kladen jako nejzásadnější požadavek rychlý vývoj, méně zkušení programátoři podléhají panice a uchylují se k těmto technikám. Místo jednoznačné parametrické implementace ve třídě, která by za implementaci měla mít zodpovědnost, vytváří implementační klony, které pouze mírně zmodifikují. Nárůst pracnosti je však v blízké budoucnosti výrazně vyšší, viz. důsledky.

Špatná komunikace ve vývojovém týmu. Pokud vývojáři a návrháři nedokáží mezi sebou efektivně komunikovat, dochází k duplicitám z důvodu neochoty hledat společné řešení.

Neexistující nebo špatná vývojářská dokumentace. Důsledkem je horší orientace vývojářů ve struktuře systému a neznalost existujícího aplikačního frameworku. I přes snahu dodržet všechny zásady dobrého návrhu v rozsahu působnosti jednoho vývojáře dochází k neúmyslné duplikaci funkcionality tříd systému přes více subsystémů spravovaných více vývojáři.

Nezkušenost. Vývoj software je o zkušenostech, které získáváte po celou dobu Vaší praxe. I absolvent s červeným IT diplomem při nasazení na první reálný projekt neprodukuje optimální kód. Problematické však je, pokud WET kód produkuje programátor s dlouhou praxí. Na to platí pouze zlaté pravidlo "poučit se z vlastních chyb".

Preventivní techniky

Návrhové vzory. Návrhové vzory jsou reakcí na potřebu standardizovat implementaci typických problémů. Znalost návrhových vzorů by měla být součástí vzdělání každého profesionálního objektového vývojáře. Vzory Vám pomohou v případě vytváření instancí tříd (Creational Patterns), definici vzájemných vztahů tříd (Structural Patterns), pravidel jejich chování (Behavioral Patterns) a v mnoha dalších. Návrh by měl předcházet vlastní implementaci. V případě dobrého návrhu minimalizujete následné riziko duplicit v kódu.

Refaktorizace. Zjistili jste, že se Vám v kódu vícekrát objevuje podobný blok kódu? Chyťte příležitost za pačesy a odseparujte opakující se kód do samostatné metody. Máte pocit, že jste třídu nebo jejího člena pojmenovali nevhodným způsobem, který klienta této třídy zmate a dojde k nepochopení pravé logiky? Ihned jej přejmenujte. Napsali jste třídu nebo metodu, která porušuje pravidlo jedné zodpovědnosti - dělá více věcí najednou? Refaktorujte ji na více menších tříd nebo metod.

Revize kódu (Code Review). Tato technika je zaměřena na kontrolu již existujícího kódu. Obvykle se provádí nejpozději před samotným vložením vyvíjeného kódu do produkčního kódu. Pro aplikaci této techniky musíte být alespoň dva. Autor kódu a ten, který kód reviduje - připomínkuje. Autor kódu by měl kódem provázet a vysvětlovat, proč implementoval problém právě takto. Revizor pak upozorňuje na problémová místa. Je běžné, že autorovi kódu začnou některé problémy samy docházet už jenom tím, že je popisuje nahlas někomu jinému. To je dobrá varianta. Horší je, pokud nikdo z účastníků problém neobjeví. Revize by se měli účastnit i další vývojáři. Pro většinu zúčastněných je přínosné a poučné vidět, jaké chyby dělají druzí. Naopak lze při revizi ukázat také povedenou techniku, fintu nebo třídu, kterou bude výhodné využívat i ostatními vývojáři.

Závěrem

Snad se společně shodneme na tom, že WET kód je nebezpečím pro Vaše projekty. Pro ty dlouhodobé je téměř smrtelný. Buďte při psaní kódu poctiví. Možná Vás to bude někdy stát více času navíc a nikdo to neocení, ale určitě se Vám vše zúročí. Buď při budoucím rozvoji Vašeho kódu nebo ve Vašem dalším profesním životě.

Řešené příklady

neděle 16. ledna 2011

Zvýrazňování syntaxe vkládaných zdrojových kódů na webových stránkách

Před založením svého blogu jsem si stanovil zásadní podmínku, kterou jsem musel být schopen u zvoleného poskytovatele zajistit. Jelikož píšu vývojářský blog, je nezbytné, aby byla syntaxe vkládaných zdrojových kódů správně zvýrazněná a tudíž lépe čitelná. Potřeboval jsem docílit něčeho takového (C# kód):

namespace Blog
{
    /// <summary>
    /// Příspěvek.
    /// </summary>
    public class Prispevek
    {
        public String Text { get; private set; }

        public Prispevek(String text)
        {
            if (String.IsNullOrEmpty(text))
            {
                throw new ArgumentNullException("Text příspěvku nesmí být prázdný.");
            }

            this.Text = text;
        }
    }
}

Inspirací mi bylo zvýrazňování syntaxe používané na serveru www.stackoverflow.com. Není sice úplně dokonalé (všimněte si např. podbarvení property Text, které by mělo být černé), je ale dostatečně názorné a vyžaduje minimální přizpůsobení zdrojového kódu před vložením. Výše uvedený zdrojový kód se do stránky vloží v tomto tvaru:

<pre>
<code>
namespace Blog
{
    /// &lt;summary&gt;
    /// Příspěvek.
    /// &lt;/summary&gt;
    public class Prispevek
    {
        public String Text { get; private set; }

        public Prispevek(String text)
        {
            if (String.IsNullOrEmpty(text))
            {
                throw new ArgumentNullException("Text příspěvku nesmí být prázdný.");
            }

            this.Text = text;
        }
    }
}
</code>
</pre>

Princip řešení

Řešení je založeno na dvou javascriptových modulech Prettify a jQuery.

Prettify dokáže v textu rozpoznat zdrojový kód jazyků typu C++, C#, Bash, dokumentů XML a dalších. Po vyhodnocení typu zdrojového kódu odekoruje jednotlivé elementy kódu příslušnou CSS třídou. Vám pak stačí pro danou třídu zadefinovat CSS styly, např. pro C# klíčová slova:

.csharp .kw1
{
    color: #0600FF;
}
Prettify se tedy spustí na klientovi až po načtení zdrojového kódu stránky. Prettify je využito pro zvýrazňování syntaxe na portálu code.google.com.

jQuery je výkonná knihovna, která zefektivňuje práci s HTML dokumentem na straně klienta. V našem případě se využije pro dohledání sekcí uzavřených mezi tagy <pre><code>. To je nutné pro určení rozsahu formátování pro Prettify.

Postup

1. Stáhněte si jQuery a Prettify.

2. Umístěte soubory prettify.js, prettify.css a jquery.js někam, kde budou dostupné pro zpracování na klientovi. V mém případě jsem si musel vytvořit webový prostor na google, kam jsem všechny potřebné soubory nauploadoval.

3. Do šablony stránky, do sekce <head> přidejte odkazované soubory (vysvětlení dále v textu), např. takto:

<script type="text/javascript" src="https://sites.google.com/site/robertdreslersite/jquery.js"></script>
<script type="text/javascript" src="https://sites.google.com/site/robertdreslersite/prettify.js"></script>
<script type="text/javascript" src="https://sites.google.com/site/robertdreslersite/styleCode.js"></script>
<link rel="stylesheet" type="text/css" href="https://sites.google.com/site/robertdreslersite/prettify.css"/>
<link rel="stylesheet" type="text/css" href="https://sites.google.com/site/robertdreslersite/langs.css"/>

4. Vytvořte si soubor styleCode.js s funkcí pro stylizaci kódu:

function styleCode() 
{
    if (typeof disableStyleCode != "undefined") 
    {
        return;
    }

    var a = false;

    $("pre code").parent().each(function() 
    {
        if (!$(this).hasClass("prettyprint")) 
        {
            $(this).addClass("prettyprint");
            a = true
        }
    });
    
    if (a) { prettyPrint() } 
}
a přidejte odkaz do šablony (viz. bod 3). Tato funkce nám najde s využitím jQuery sekce ke zformátování.

5. Do šablony sekce <head> přidejte ještě část javascriptového kódu, který zpřístupní funkci styleCode():

<script type="text/javascript">
  $(function() {

  styleCode();

  });
</script>

6. Teď už nám zbývá pouze ve vhodný okamžik vyvolat přeformátování zdrojových kódů. Vyvolání je vhodné umístit do šablony do události onLoad elementu <body>:

<body onload="styleCode()">

7. Zbývá vysvětlit význam souboru langs.css. Obsahuje formátovací styly pro elementy, které umí vygenerovat Prettify. Můžete použít můj nebo si zadefinovat vlastní.

Další informace

Můžete se podívat na původní článek, ve kterém se o řešení dozvíte více. Je zde vysvětleno i zvýrazňování syntaxe uživatelských vstupů - např. při zadávání komentářů na vývojářských fórech.

Zrození blogu

Některé nápady nosí člověk v hlavě dlouho a intenzita potřeby jejich realizace kolísá podle aktuální nálady a množství volného času. Podobné to bylo i s mým blogováním. Blogovat jsem chtěl začít již dávno a poměrně přesně jsem věděl, které oblasti se chci věnovat. Teprve dnes však zvítězilo novátorské duševno nad lenivostí hmoty a nakonfiguroval jsem si tento blog. Rád bych položil základ vývojářského fóra, na kterém bych informoval o problémových oblastech IT, které jsou pro mě něčím zajímavé.

Tak uvidíme ;-)