čtvrtek 24. listopadu 2011

Rhino Mocks & spol. aneb imitujte rozhraní

Frameworky plné falše, náhražek, imitací a podvrhů ...

... a proto je máme tak rádi a proto je ke svému vývojářskému životu potřebujeme :-)

Nebojte se, jsme stále ještě ve světě férového programování. Tento příspěvek je krátkým motivačním úvodem do světa mock, fake, stub a dalších typů objektů, bez kterých se neobejde žádný vývojář, který to myslí vážně s psaním jednotkových (unit) a integračních testů.

Mock objekty a k čemu jsou dobré

Mock objekt vznikne jako fiktivní instance rozhraní nebo třídy. Této objektové imitaci pak můžete přiřadit chování, které očekáváte. Můžete také stnovit pravidla použití, které mohou být prověřeny na konci testu.

Předpokládám, že nejčastěji využíváte nebo budete využívat možnost "falešné" implementace rozhraní. V tomto případě oceníte techniku programování proti rozhraní (namísto programování proti implementaci). Je bežné, že třídy mají závislosti na jiných rozhraních. Pokud takovou třídu chcete pokrýt jednotkovými nebo integračními testy, musíte umět tyto závislosti vyřešit. Níže je uveden příklad který zastupuje tisícovku slov.

Většina mockovacích frameworků nabízí také validaci pořadí a počtu volání metod nebo přístupu k property mock objektu. Touto technikou můžete prověřit volání metod, které jsou pro potřeby testu důležité.

Rhino Mocks, Moq, NMock2, Isolator, TypeMock nebo Moles

Mockovacích frameworků pro .Net je několik. Liší se možnostmi toho, které třídy lze mockovat, intuitivností zápisu, možnostmi validačních pravidel, podporovanými verzemi .Net frameworků a dalšími aspekty. Přehledné srovnání je k dispozici na webu PHP vs .Net.

Nejrošířenějším volně dostupným frameworkem je Rhino Mocks. Zápis pravidel mock objektů je natolik intuitivní a variabilní, že pokryje většinu Vašich potřeb. Já osobně používám právě Rhino Mocks. Otázkou je, zda-li autor bude i nadále funkcionalitu rozvíjet. Pokud se nepletu, tak poslední update je někdy z roku 2009.

Ještě mám osobní zkušenost s NMock2. Pro mě je nevyhovující z toho důvodu, že názvy metod a vlastností se zapisují textově. Nejen že takový zápis zdržuje, nedá se využít IntelliSense, ale navíc není bezpečný pro refaktoring.

Mockování pro začátečníky

Mějme rozhraní zadefinovaná takto:

namespace Firma.Bll
{
    /// <summary>
    /// Rozhraní pro práci s kurzy.
    /// </summary>
    public interface IKurzyBll
    {
        /// <summary>
        /// Vrátí hodnotu kurzu měny.
        /// </summary>
        /// <param name="kodMeny">Kód měny.</param>
        /// <param name="datumPlatnosti">Datum platnosti.</param>
        /// <returns>Hodnota kurzu bez omezení přesnosti.</returns>
        decimal VratitHodnotuKurzuMeny(string kodMeny, DateTime datumPlatnosti);
    }

    /// <summary>
    /// Rozhraní pro práci s měnami.
    /// </summary>
    public interface IMenyBll
    {
        /// <summary>
        /// Převede částku v měně na částku domácí měny.
        /// </summary>
        /// <param name="kodMeny">Kód měny.</param>
        /// <param name="datumPlatnosti">Datum platnosti.</param>
        /// <param name="castkaVMene">Částka v měně.</param>
        /// <returns>Částka převedená na domácí měnu s přesností na dvě desetinná místa.</returns>
        decimal PrevestCastkuNaDomaciMenu(string kodMeny, DateTime datumPlatnosti, decimal castkaVMene);
    }
}

Třída implementující rozhraní IMenyBll vyžaduje při vzniku předání implementace IKurzyBll:

namespace Firma.Bll.Impl
{
    /// <summary>
    /// Implementace práce s měnami.
    /// </summary>
    public class MenyBll : IMenyBll
    {
        private IKurzyBll KurzyBll { get; set; }
          
        /// <summary>
        /// Pomocí constructor injection je vložena závislost na IKurzyBll.
        /// </summary>
        /// <param name="kurzyBll">Práce s kurzy.</param>
        public MenyBll(IKurzyBll kurzyBll)
        {
            KurzyBll = kurzyBll;
        }
 
        public decimal PrevestCastkuNaDomaciMenu(string kodMeny, DateTime datumPlatnosti, decimal castkaVMene)
        {
            decimal hodnotaKurzu = KurzyBll.VratitHodnotuKurzuMeny(kodMeny, datumPlatnosti);
            decimal castkaVDomaciMene = Math.Round(castkaVMene * hodnotaKurzu, 2, MidpointRounding.AwayFromZero);

            return castkaVDomaciMene;
        }
    }
}

A teď přichází chvilka slávy pro Rhino Mocks. Správnost implementace IKurzyBll nás v případě testování třídy MenyBll příliš nezajímá. Ale přesto ji potřebujeme. Bez ní implementaci MenyBll neotestujeme. V produkčním běhu aplikace může být IKurzyBll implementováno nad webovou službou, databázovou tabulkou nebo jiným způsobem. Pro účely testu by však bylo náročné takovou implementaci připravit. Jednoduchým řešením je vytvoření mock objektu, který naučíme vracet kurz podle potřeby našeho testu. Celá myšlenka by měla být zřejmá z kódu testovací metody:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;

namespace Firma.Bll.Impl.Test
{
    /// <summary>
    /// Unit testy pro třídu MenyBll.
    /// </summary>
    [TestClass]
    public class MenyBllTest
    {
        private const decimal HodnotaKurzuMenaEur = 23.530m;

        [TestMethod]
        public void PrevodCastkyEurTest()
        {
            // Repozitář pro správu mock objektů.
            MockRepository mocks = new MockRepository();

            // Vytvoření mock objektu pro rozhraní IKurzyBll.
            IKurzyBll kurzyBllMock = mocks.StrictMock<IKurzyBll>();

            string kodMeny = "EUR";
            DateTime datumPlatnosti = DateTime.Today;

            // Definice očekávaného chování.
            // Při volání VratitHodnotuKurzuMeny() s parametry "EUR" a dnešní datum 
            // vrací hodnotu 23.530m. Počet volání této metody není omezen.
            Expect.Call(kurzyBllMock.VratitHodnotuKurzuMeny(kodMeny, datumPlatnosti)).
                Return(HodnotaKurzuMenaEur).Repeat.Any();

            // Realizuje definici všech mock objektů.
            mocks.ReplayAll();

            // Vytvoření instance testované třídy s podvržením mock implementace IKurzyBll.
            MenyBll menyBll = new MenyBll(kurzyBllMock);

            decimal castkaEur = 10.50m;
            decimal ocekavanaCastkaCzk = 247.07m;
            decimal vracenaCastkaCzk = menyBll.PrevestCastkuNaDomaciMenu(kodMeny, datumPlatnosti, castkaEur);

            // Pokud je očekávaná částka různá od skutečně vrácené částky, 
            // dojde k výjimce a test skončí chybou.
            Assert.AreEqual(ocekavanaCastkaCzk, vracenaCastkaCzk);
        }
    }
}

Pokud patříte k vyznavačům TDD (vývoj řízeny testy), pak byste zřejmě postupovali tak, že ještě před vlastním implementováním metody PrevestCastkuNaDomaciMenu() napíšete tento a případné další testy. Tzn. zadefinujete očekávané cílové chování metody formou unit testů. Následná korektní implementace zajistí, že všechny testy začnou procházet.

Několik odkazů pro rychlejší rozjezd

čtvrtek 17. listopadu 2011

NConfig - řešení pro lokální konfigurace

Proč se nám může NConfig hodit

Přijdete ráno do práce, zvolíte ve Visual Studiu Get Latest Version nad celým projektem (řešením) a jdete si uvařit kafe. Pokračujete v implementaci nové funkcionality a spouštíte testy nad databází. Začaly se však objevovat chyby, které jsou hodně podezřelé. Vypadá to jako by v příslušné databázi nebyly struktury, které jste si vytvořili nově v rámci vývoje. Pátráte, jak je to možné a ztrácíte drahocené minuty. Když už začínáte být trochu zoufalí, uvědomíte si, že jste si aktualizovali lokální workspace. Podíváte se do konfiguračního souboru a zjistíte, že někdo změnil směrování na databázi! V historii změn zjistíte, že to byl Peter. V duchu si zanadáváte a zároveň si uvědomíte, že už jste párkrát udělali kolegům to samé. Omylem jste dali vrácení změn na server (Check-in) ve společném konfiguračním souboru (Web.config, App.config). Pokud však máte systémový přístup k řešení problémů, pokusíte se toto neustálé přepisování nějak elegantně vyřešit.

A možná by Vám mohl pomoci NConfig!

Jak získat NConfig

NConfig je .Net knihovna, jejíž použití ve Vašich projektech není nijak licenčně omezeno. Projekt je vyvíjen na serveru GitHub na adrese https://github.com/Yegoroff/NConfig.

Já jsem postupoval tak, že jsem si přes odkaz Download stáhnul celý adresář jako zip soubor. Následně jsem jej rozbalil, otevřel solution NConfig.sln a sestavil z projektu NConfig výslednou dll knihovnu ve verzi pro framework 4.0 (k dispozici je i projekt pro framework 3.5). Dále jsem již pracoval pouze s dll knihovnou.

Co NConfig umí

NConfig umí slučovat konfigurace z více konfigračních souborů. Umí také za běhu vybrat konfigurační soubor podle názvu aktuálního počítače. Tato funkcionalita je řešením pro výše uvedený motivační případ.

// Sloučí nastavení defaultního konfiguračního souboru a 
// Configs\Custom.config, resp. Configs\{NazevPocitace}.Custom.config
NConfigurator.UsingFiles(@"Configs\Custom.config").SetAsSystemDefault();

Autor tvrdí, že NConfig lze využít v aplikacích typu ASP.Net, ASP.Net MVC, WinServies, WinForms, WPF a konzolová aplikace.

Ukázka použití

Požadujeme, aby si vývojáři Peter a Steve mohli nastavit navzájem nezávislé lokální konfigurace pro připojení k databázím tak, aby nezasahovali do společného konfiguračního souboru.

Vytvoříme si jednoduchou konzolovou aplikaci a nareferencujeme NConfig.dll. Přidáme standardní App.config. Uživatelské konfigurační soubory umístíme do složky Configs. Ve složce vytvoříme defaultní Custom.config a konfigurační soubory pro Petera a Steva - PeterComputer.Custom.config a SteveComputer.Custom.config. Viz. obrázek. Nezapomeňte nastavit pro konfigurační soubory ve složce Configs vlastnost Copy To Output Directory na true.

Soubor App.config vypadá takto:

<?xml version="1.0"?>
<configuration>
 <appSettings>
  <add key="Database" value="AppDatabase"/>
  <add key="User" value="AppUser"/>
 </appSettings>
</configuration>

Soubor Custom.config vypadá takto:

<?xml version="1.0"?>
<configuration>
 <appSettings>
  <add key="User" value="CustomUser"/>
 </appSettings>
</configuration>

A například soubor PeterComputer.Custom.config vypadá takto:

<?xml version="1.0"?>
<configuration>
 <appSettings>
  <add key="Database" value="PeterComputerDatabase"/>
  <add key="User" value="PeterComputerUser"/>
 </appSettings>
</configuration>

Třída Program konzolové aplikace využije volání metody UsingFiles() třídy NConfig.NConfigurator, která sloučí původní App.config s Configs\Custom.config, případně s konfiguračními soubory podle spuštěného počítače:

class Program
{
    static void Main(string[] args)
    {
        // SwitchOnCustomConfig();

        Console.WriteLine(String.Format("User='{0}'", ConfigurationManager.AppSettings["User"]));
        Console.WriteLine(String.Format("Database='{0}'", ConfigurationManager.AppSettings["Database"]));
    }

    static void SwitchOnCustomConfig()
    {
        NConfigurator.UsingFiles(@"Configs\Custom.config").SetAsSystemDefault();
    }
}

Přehled scénářů:

Scénář Konfigurační soubor Hodnota klíče Database Hodnota klíče User
Není zapnutá podpora volitelných konfiguračních souborů - nevolá se metoda SwitchOnCustomConfig(). App.config AppDatabase AppUser
Je zapnutá podpora volitelných konfiguračních souborů - volá se metoda SwitchOnCustomConfig() a aplikace není spuštěna ani na počítači Petera ani Steva. Custom.config AppDatabase CustomUser
Je zapnutá podpora volitelných konfiguračních souborů - volá se metoda SwitchOnCustomConfig() a aplikace je spuštěna na počítači Petera. PeterComputer.Custom.config PeterComputerDatabase PeterComputerUser

neděle 13. listopadu 2011

Pravidla pro pojmenování objektů v C# - část 1

Problematika je pro svoji obsáhlost rozdělena do více příspěvků:

  1. Úvod, jazyk, názvy tříd
  2. Názvy vlastností (properties), polí (fields) a proměnných (variables)
  3. Názvy metod a argumentů
  4. Další pravidla, názvy balíčků, testovací třídy

Úvod

U programového kódu se předpokládá, že splňuje syntaktickou a sémantickou správnost. Tyto dva požadavky jsou nutné pro vlastní fungování výsledné aplikace. Aby jste kód mohli efektivně udržovat a rozvíjet, je neméně důležitá dostatečná čitelnost kódu a správné názvosloví. Možná se Vám již někdy stalo, že jste se vrátili k Vašemu staršímu kódu a dlouze jste se snažili vyčíst, co jste takovým zápisem vlastně sledovali. Chvíli sami sebe přesvědčujete, že toto nemůže být Váš kód, takhle nepřehledně byste to přeci nikdy nenapsali. Nakouknete do historie v repository a zjistíte, že jste to byli opravdu Vy. Trochu se zastydíte a slíbíte si, že příště si na kódu dáte více záležet.

Pokud se dostanete ke kódu převzatému od nějakého "kouzelníka" (v některých firmách se těmto lidem nesprávně říká guru), může se situace ještě více zdramatizovat. Mnohdy se stává, že některé části kódu pro Vás zůstanou zapovězeny navždy a Vy se smíříte s tím, že kód sice něco dělá, ale netušíte jak. Zasáhnout do takového kódu vyžaduje dostatek osobní odvahy a pokud nemáte dostatečné pokrytí kódu testy, můžete úpravou nadělat nevědomky pěknou paseku.

Jak se naučíte psát čitelný a dobře spravovatelný kód? Pouze praxí, znalostí níže uvedených pravidel, učením se z vlastních i cizích chyb, skupinovým posuzováním kódu (code review). V tomto miniseriálu o názvosloví se Vám pokusím ukázat několik pravidel, které doporučuje odborná literatura, a které se mi osvědčily ve vlastní praxi.

Jazyk

Na začátku projektu se musíte rozhodnout jaký jazyk použijete. Nejedná se o jazyk programový, ale lingvistický. Svět programování mluví anglicky, ale přítomnost mateřské češtiny nemusí být na škodu. Většina z nás má lepší vyjadřovací schopnosti v češtině, angličtina je však mnohdy výstižnější a nedává nám příliš velký manévrovací (dezinformační) prostor. Záleží také na jazykovém složení Vašeho týmu. Pokud děláte lokální projekty s homogenním českým týmem (mohou být přimícháni i slovenští kolegové), můžete využít češtinu. V případě jazykově heterogenního týmu na výběr nemáte a budete zřejmě komunikovat a pojmenovávat programové objekty v angličtině. Pokud se Vám ovšem nepodaří husarský kousek, kdy naučíte třeba němce spisovné češtině ;-)

Můžete se také rozhodnout, že některé nejnižší vrstvy programového systému budou čistě anglické a vyšší vrstvy naopak české. Například jedna podvrstva datové vrstvy komunikující s nějakým persistentním frameworkem bude anglická a vrstvy od aplikační logiky (business logic layer) výše (vrstva služeb, prezentační vrstva) budou české. Především ve vrstvě aplikační logiky můžete s výhodou využít češtiny a její (pro české vývojáře) přirozené srozumitelnosti. Samozřejmě za dodržení určitých pravidel a jednotnosti. Toto kombinování jazyků však nedoporučuji.

Určitě se však vyhněte tomu, že v rámci jedné třídy budete mít namíchány názvy z více jazyků. Výjimku tvoří metody a property, které dědí Vaše třída. Např. třída pojmenovaná jako Objednavka může přepisovat metodu GetHashCode() nebo Equals(). Podle stejné logiky nemíchejte jazyky v rámci jednoho jmenného prostoru nebo ještě lépe v rámci celé assembly.

Jazyk použitý pro názvosloví programových objektů by měl korespondovat s jazykem použitým pro objekty v persistentním úložišti (v databázi). Jednoduše řečeno by mělo platit, že tabulky a sloupce jsou pojmenované ve stejném jazyce jako odpovídající třídy a vlastnosti.

V dalším textu budu používat názvosloví v češtině a budu předpokládat, že většina pravidel se dá aplikovat i na angličtinu.

Názvy tříd

  1. Používejte PascalCase notaci. V C# se nepíše polozkaObjednavky, ale PolozkaObjednavky. Omlouvám se tomu, koho jsem takovou samozřejmostí urazil ;-)

  2. Používejte jednotné číslo. Instancí Vaší třídy je jeden objekt. Název třídy Objednavky proto nedává smysl. Správně je Objednavka.

  3. Název by měl být významový a jednoznačný. Pokud v systému zadefinujete třídy Objednavka, DataObjednavky a ObjednavkaInfo zaděláváte si na problémy. Na první pohled není zřejmé, v čem se třídy liší. Pokud všechny reprezentují objektovou entitu odpovídající objednávce v reálném světě, jedná se o chybnou duplicitu v objektovém návrhu. Pokud tomu tak není a např. třída DataObjednavky umí nějakou obchodní logiku, např. vrací určité statistiky nad objednávkami, pak je jistě název takové třídy zvolen chybně.

  4. Používejte názvy z domény řešeného problému. Od doménového experta by návrhář systému (alias implementátor) měl dostat doménový slovník, který definuje pojmy a pravidla, které má aplikace pokrýt. Každá doména má svoji terminologii, která je standardizovaná a zúčastnění jí rozumí. Musíte těmto pojmům rozumět také a držet se dané terminologie v názvech tříd a vlastností.

  5. Používejte vhodné a standardizované přípony. Je běžné, že se v systému objevuje více tříd související s danou entitou. V takových případech je vhodné standardizovat určité přípony a zadefinovat jejich jednoznačný význam. Podívejme se na příklad objednávky a s ní souvisejících tříd:

    • Objednavka [datová vrstva] - třída reprezentující objednávku a její vlastnosti (mapovány na databázové sloupce tabulky s objednávkami).
    • ObjednavkaDao [datová vrstva] - třída odpovědná za CRUD (Create, Read, Update, Delete) operace, např. načtení objednávky podle jejího Id.
    • ObjednavkaBll [vrstva obchodní logiky] - třída zajišťující realizaci obchodních pravidel pro práci s objednávkami, např. vykrytí objednávky.
    • ObjednavkaDto [vrstva služeb] - zjednodušená třída (jejíž objekty jdou serializovat) pro přenos přes vrstvu služeb u vícevrstvých aplikací.
    • ObjednavkaSluzba [vrsta služeb] - třída nabízející logiku práce s objednávkami na úrovni komunikační vrstvy.
    • ObjednavkaObsluha [prezentační vrstva] - třída plnící funkci presenteru v MVP nebo controlleru v MVC návrhovém vzoru pro prezentační vrstvy.
    • ObjednavkaPohled [prezentační vrstva] - třída zajišťující vykreslení objednávky v závislosti na vybrané prezentační technologii.

  6. Nepoužívejte v názvech názvy objektových typů. Pokud třída řeší překlad českých slov na německá vnitřně pomocí hešovací tabulky, nepoužívejte název CeskoNemeckySlovnikHashTable, ale pouze CeskoNemeckySlovnik. Klienta třídy nezajímá její vnitřní implementace. Je pro něj důležitá pouze veřejná část třídy.

  7. Nezkracujte na úkor čitelnosti. Pokud je to vhodné použijte víceslovný název třídy, který přesněji vymezí význam třídy. V době pomůcek pro efektivitu psaní kódu typu IntelliSense se dlouhých názvů bát nemusíte.

    Pozor na situace, kdy se nedá význam třídy popsat jednoduše. V těchto případech se může jednat o blikající kontrolku chybného návrhu třídy, konkrétně o porušení pravidla jedné zodpovědnosti. Např. třída AdresaABankovniUcetZamestnance zřejmě slučuje dvě věci (adresu a bankovní účet), které by měly být odděleny do dvou samostatných tříd.

  8. Nebojte se refaktorovat název. Nemusíte ideální název třídy vymyslet hned napoprvé. Ale pokud časem přijdete na výstižnější název, nebojte se jej refaktorovat. Kód musíte neustále vylepšovat.

  9. Nepoužívejte slovesa v názvech. Třída ZamestnanecMajiciDohodu má možná literárně hodnotný název, ale do názvu třídy něco takového nepatří. V tomto případě se zřejmě jedná navíc o chybný objektový návrh a chybnou dědičnost.

úterý 6. září 2011

Jak psát lepší kód s využitím StyleCopu

StyleCop - stylový policajt

StyleCop patří do rodiny nástrojů pro provádění statické analýzy kódu. StyleCop prověřuje kód a to bez nutnosti tento kód spouštět - proto statická analýza. StyleCop umí prověřovat (validovat) pravidla z několika oblastí - dokumentační pravidla, pravidla rozvržení, pravidla pro udržovatelnost, pravidla pojmenování, pravidla pro řazení, pravidla pro čitelnost, pravidla pro řádkování a mezery. Můžete si vytvořit i vlastní. Která pravidla nakonec budete chtít vynucovat záleží na Vámi zvoleném způsobu integrace tohoto nástroje do vývojového procesu.

StyleCop je zdarma ke stažení na CodePlexu, viz. stylecop.codeplex.com. Jako platforma použití se předpokládá Visual Studio 2008 nebo 2010 v edici minimálně Professional. V dokumentaci lze dohledat návod, jak docílit použití spolu s Visual Studiem 2005 nebo s Express edicí. StyleCop je v edicích Premium a Ultimate nativní součástí distribuce. Podobné integrace můžete dosáhnout dle návodu popsaného níže.

Ukázka použití StyleCopu

Nastal čas, aby nám StyleCop ukázal co umí. Fiktivní třída AntiStyleCopClass se tváří poměrně nenápadně. Je syntakticky správně a působí poměrně logicky strukturovaná. Posuďte sami:

using System;
using System.Text;
using System.Collections.Generic;

namespace StyleCopTest
{
    /// <summary>
    /// Třída, která se StyleCopu nebude líbit 
    ///</summary>
    class AntiStyleCopClass                                    /* line 10*/
    {
        private const string ODDELOVAC_TEXTU = " ";
        protected string oddelovacTextu = ODDELOVAC_TEXTU;

        /// <summary>
        /// Bezparametrický konstruktor.
        /// </summary>
        public AntiStyleCopClass() { }

        /// <summary>                                          /* line 20*/
        /// Konstruktor s definicí oddělovače textu.
        /// </summary>
        public AntiStyleCopClass(string oddelovacTextu)
        {
            this.oddelovacTextu=oddelovacTextu;
        }


        private void SoukromaMetoda(string argument1, string argument2,string argument3) {
                                                               /* line 30*/
            if ((argument1 == "") || argument1 == null) { return; }
            if (argument2 == String.Empty || argument2 == null )
            // Rychle opustit metodu
            {
                return;
            };
            if (String.IsNullOrEmpty(argument3)) { return; } else { Console.WriteLine(argument1 + argument2 + argument3); }
        }
        /// <summary>
        /// Veřejná metoda třídy.                              /* line 40*/
        /// </summary>
        /// <param name="argument1">Argument1.</param>
        /// <param name="parametr2">Argument2</param>
        public void VerejnaMetoda(string argument1, string argument2)
        {
            if (argument1 == null || argument2 == null) throw new ArgumentNullException("Takové argumenty Ti nesežeru.");

            // Vyvolání soukromé metody

            SoukromaMetoda(argument1,                          /* line 50*/
                oddelovacTextu, argument2
                );   

            //Console.WriteLine("Zpracování ok.");
        }

        /// <summary>
        /// Oddělovač textu.
        /// </summary>
                                                               /* line 60*/
        public string OddelovacTextu
        {
            get {return oddelovacTextu;}
            set
            {
                oddelovacTextu = value;
            }
        }

    }                                                          /* line 70*/

    /// <summary>
    /// Takový malý appendix.
    /// </summary>
    public class JednaMalaTrida
    {
    }
}                                                            

Oko zkušeného vývojáře však zbystří a začne klopýtat po méně či více závažných proviněních proti formátovacím a kódovacím standardům jazyka C# a dobrým programátorským mravům. Necháme-li úřadovat StyleCop, dostáváme v tomto případě 46 porušení validačních pravidel. Všimněte si, každé pravidlo má jednoznačný identifikační kód a vysvětlující text porušení.

Line 2: SA1210: Using directives must be sorted alphabetically by the namespaces.
Line 9: SA1004: The documentation header line must start with a single space.
Line 10: SA1629: The documentation text within the summary tag must end with a period.
Line 10: SA1400: The class must have an access modifier.
Line 10: SA1202: All internal classes must be placed after all public classes.
Line 12: SA1310: Field names must not contain underscores.
Line 12: SA1202: All private fields must be placed after all protected fields.
Line 13: SA1600: The field must have a documentation header.
Line 13: SA1401: Fields must be declared with private access. Use properties to expose fields.
Line 18: SA1502: The constructor must not be placed on a single line. The opening and closing curly brackets must each be placed on their own line.
Line 23: SA1611: The documentation header must contain param tags matching the element's parameter list.
Line 25: SA1003: The spacing around the symbol '=' is invalid.
Line 28: SA1507: The code must not contain multiple blank lines in a row.
Line 29: SA1500: If a statement spans multiple lines, the opening curly bracket must be placed on its own line.
Line 29: SA1505: An opening curly bracket must not be followed by a blank line.
Line 29: SA1001: Invalid spacing around the comma.
Line 31: SA1501: A statement containing curly brackets must not be placed on a single line. The opening and closing curly brackets must each be placed on their own line.
Line 31: SA1122: Use string.Empty rather than "".
Line 32: SA1009: Invalid spacing around the closing parenthesis.
Line 33: SA1515: A single-line comment must be preceded by a blank line or another single-line comment, or must be the first item in its scope. To ignore this error when commenting out a line of code, begin the comment with '////' rather than '//'.
Line 33: SA1108: A comment may not be placed within the bracketed statement.
Line 36: SA1106: The code contains an extra semicolon.
Line 37: SA1501: A statement containing curly brackets must not be placed on a single line. The opening and closing curly brackets must each be placed on their own line.
Line 37: SA1107: A line may only contain a single statement.
Line 39: SA1514: A documentation header must be preceded by a blank line or must be the first item in its scope.
Line 44: SA1202: All private methods must be placed after all public methods.
Line 44: SA1612: The param tags in the documentation header must match the element's parameter list.
Line 44: SA1629: The documentation text within the param tag must end with a period.
Line 44: SA1632: The documentation text within the param tag must be at least 10 characters in length. Documentation failing to meet this guideline most likely does not follow a proper grammatical structure required for documentation text.
Line 44: SA1516: Adjacent elements must be separated by a blank line.
Line 46: SA1503: The body of the if statement must be wrapped in opening and closing curly brackets.
Line 48: SA1512: A single-line comment must not be followed by a blank line. To ignore this error when commenting out a line of code, begin the comment with '////' rather than '//'.
Line 50: SA1117: All method parameters must be placed on the same line, or each parameter must be placed on a separate line.
Line 50: SA1116: If the method parameters are on separate lines, the first parameter must begin on the line beneath the name of the method.
Line 51: SA1115: The parameter must begin on the line after the previous parameter.
Line 52: SA1111: The closing parenthesis or bracket must be placed on the same line as the last parameter in the parameter list.
Line 52: SA1009: Invalid spacing around the closing parenthesis.
Line 54: SA1005: The comment must start with a single space. To ignore this error when commenting out a line of code, begin the comment with '////' rather than '//'.
Line 57: SA1506: A documentation header line must not be followed by a blank line.
Line 61: SA1201: All methods must be placed after all properties.
Line 63: SA1504: An accessor can only be placed on a single line if all of the accessors in the property are placed on a single line.
Line 63: SA1012: Invalid spacing around the opening curly bracket.
Line 63: SA1002: Invalid spacing around the semicolon.
Line 63: SA1013: Invalid spacing around the closing curly bracket.
Line 70: SA1508: A closing curly bracket must not be preceded by a blank line.
Line 75: SA1402: A C# document may only contain a single class at the root level unless all of the classes are partial and are of the same type.

Tento výsledek by nás neměl znechutit, ale spíše motivovat. Chceme přeci vytvářet lepší kód?! Soustředíme se, postupně si pozorně pročítáme každou chybu a sjednáváme nápravu. Ve výsledku dostáváme třídu LovelyStyleCopClass, na které nás StyleCop již nenachytá:

using System;
using System.Collections.Generic;
using System.Text;

namespace StyleCopTest
{
    /// <summary>
    /// Třída, která se StyleCopu musí líbit.
    /// </summary>
    public class LovelyStyleCopClass
    {
        private const string OddelovacTextuDefaultni = " ";
        private string oddelovacTextu = OddelovacTextuDefaultni;

        /// <summary>
        /// Bezparametrický konstruktor.
        /// </summary>
        public LovelyStyleCopClass() 
        {
        }

        /// <summary>
        /// Konstruktor s definicí oddělovače textu.
        /// </summary>
        /// <param name="oddelovacTextu">Oddělovač textu.</param>
        public LovelyStyleCopClass(string oddelovacTextu)
        {
            this.oddelovacTextu = oddelovacTextu;
        }

        /// <summary>
        /// Oddělovač textu.
        /// </summary>
        public string OddelovacTextu
        {
            get
            {
                return oddelovacTextu;
            }

            set
            {
                oddelovacTextu = value;
            }
        }

        /// <summary>
        /// Veřejná metoda třídy.
        /// </summary>
        /// <param name="argument1">Argument1.</param>
        /// <param name="argument2">Argument2.</param>
        public void VerejnaMetoda(string argument1, string argument2)
        {
            // Vyvolání soukromé metody
            SoukromaMetoda(argument1, oddelovacTextu, argument2);   
        }

        private void SoukromaMetoda(string argument1, string argument2, string argument3)
        {
            if (argument1 == String.Empty || argument1 == null)
            {
                return;
            }

            if (argument2 == String.Empty || argument2 == null)
            {
                // Rychle opustit metodu
                return;
            }

            if (String.IsNullOrEmpty(argument3))
            {
                return;
            }
            else
            {
                Console.WriteLine(argument1 + argument2 + argument3);
            }
        }
    }
}

Způsoby integrace StyleCopu do vývojového procesu

Způsob použití je odvislý od fáze, ve které chcete statickou analýzu kódu provádět:

  • Ruční spouštění. V tomto případě si musíte StyleCop nainstalovat lokálně. Po instalaci se StyleCop zaintegruje do vývojového prostředí Visual Studia a v Solution Exploreru Vám přibude nová kontextová volba StyleCop. Validaci můžete spustit nad jedním nebo více cs soubory, nad projektem nebo libovolným podstromem řešení. Problémový výsledek validace se projeví přidáním varování (warning) v okně s chybami kompilace (Error List). Kontextová nabídka na varováních, která přidal StyleCop obsahuje volbu Show Error Help. Ta zobrazí pro každé pravidlo pěkně vypracovanou dokumentaci, ve které se mimojiné dozvíte, proč je porušení pravidla problémové a jak jeho porušení vyřešit. Tento přístup je vhodný pro seznámení se se StyleCopem a v situaci, kdy integrujete StyleCop do vývoje projektu, který již běží.
  • Spouštění v rámci kompilace projektu. Tento přístup je logický, neboť kompilace projektu je okamžik, kdy Visual Studio (konkrétně MSBuild) vyhodnocuje nejen syntaktickou správnost, ale také další pravidla. Přidání StyleCopu znamená rozšířit .csproj konfigurační soubor o vyvolání targetu, který je součástí distribuce StyleCopu:
    ...
    <!-- Kompilace projektu - standardní řádek v každém kompilovatelném projektu -->
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> 
    <!-- Integrace StyleCopu - nově přidaný řádek -->  
    <Import Project="c:\Program Files\MSBuild\Microsoft\StyleCop\v4.4\Microsoft.StyleCop.Targets" /> 
    ...
    
    Modifikací tohoto přístupu je varianta, kdy umístíte všechny potřebné knihovny StyleCopu včetně .Targets souboru do repository. Pak budete v .csproj souborech odkazovat relativní cestou. Tímto způsobem zajistíte pro ostatní členy týmu bezinstalační integraci StyleCopu. Počítejte s tím, že přidání validace do kompilace projektu Vám nepatrně prodlouží dobu kompilace.
  • Spouštění v rámci denního buildu. Můžete nechat StyleCop prověřovat Váš kód během denního serverového buildu, který je určen pro kontinuální zajištění kvality kódu. V rámci tohoto buildu můžete provádět různé techniky statického i dynamického validování. V tomto případě je nutné přehledným způsobem zobrazit případná porušení například na vývojovém týmovém portálu. Tento způsob spouštění poskytuje důležité informace pro vedoucího vývojáře a pro tým zajišťující kvalitu vývoje.

Výše uvedené přístupy lze samozřejmě kombinovat a dle mého názoru je to i užitečné. Pro vývojáře je nejlepší, aby byl na porušení upozorněn co nejdříve. Tedy nejpozději při kompilaci projektu.

Bez pomocných nástrojů to půjde tězce

Pokud si stanovíte přísná pravidla na vytvářený kód, počítejte s tím, že režie na vytváření kódu se zvýší. Než si na nová pravidla zvyknete, bude to chvíli trvat. Brzy se však naučíte dodržovat konvence s takovou samozřejmostí, že StyleCop ani nebude mít mnoho práce. Navýšení pracnosti je však pouze relativní. Kód je součástí projektu dlouhé roky a tak je nutné odlišovat primární náklady související s prvotním vytvářením kódu a sekundární náklady spojené s jeho údržbou a pozdější modifikací. A právě v redukci sekundárních nákladů je StyleCop silný. Každá pozdější práce se správně napsaným kódem je efektivnější a začíná se Vám úročit dodržování pravidel StyleCopu.

Dodržování některých pravidel je bez pomocných nástrojů hodně pracné. Např. pravidlo SA1202: ElementsMustBeOrderedByAccess požaduje, aby pořadí členů třídy bylo setříděno podle přístupnosti (public, internal, protected internal, protected, private). Ruční hlídání tohoto pravidla by mohlo být pracné. Existuje však nástroj Regionerate, který Vám zautomatizuje správné setřídění členů.

Při dodržování mnoha dokumentačních pravidel Vám ulehčí práci doplněk GhostDoc.

Určitě se vyplatí používat další doplňky typu CodeRush. Např. jedno z pravidel SA1210: UsingDirectivesMustBeOrderedAlphabeticallyByNamespace požaduje, aby using direktivy byly setříděny podle abecedy. Ruční editace nepředstavitelná, s CodeRush je to hračka. Stačí podržet kurzor myši nad problémovou oblastí a máte navrženu úpravu. Pro CodeRush existuje doplněk, který nastaví chování CodeRush podle pravidel vynucovaných StyleCopem.

Pro editaci .csproj souborů používám doplněk PowerCommands. Ten umí také třídit usingy, viz. předchozí odstavec.

Která pravidla zvolit?

Parametrizaci pravidel má StyleCop uloženou v souboru Settings.StyleCop. Pro práci s konfiguračním souborem je dodáván vizuální editor. Vyberte si pravidla vhodná pro Váš projekt a uložte konfigurační soubor do rootu Vašeho adresářového stromu se zdrojovými kódy. StyleCop při spuštění prochází nadadresáře a hledá konfigurační soubor, kterým se následně řídí. Můžete některé podstromy s kódem nakonfigurovat přísněji, jiné benevolentněji.

Níže uvádím výpis pravidel, které používáme v našich projektech. Můžete se nechat inspirovat.

Dokumentační pravidla (Documentation Rules)
SA1600: ElementsMustBeDocumented
SA1601: PartialElementsMustBeDocumented
SA1602: EnumerationItemsMustBeDocumented
SA1603: DocumentationMustContainValidXml
SA1604: ElementDocumentationMustHaveSummary
SA1605: PartialElementDocumentationMustHaveSummary
SA1606: ElementDocumentationMustHaveSummaryText
SA1607: PartialElementDocumentationMustHaveSummaryText
SA1608: ElementDocumentationMustNotHaveDefaultSummary
SA1610: PropertyDocumentationMustHaveValueText
SA1611: ElementParametersMustBeDocumented
SA1612: ElementParameterDocumentationMustMatchElementParameters
SA1613: ElementParameterDocumentationMustDeclareParameterName
SA1614: ElementParameterDocumentationMustHaveText
SA1615: ElementReturnValueMustBeDocumented
SA1616: ElementReturnValueDocumentationMustHaveValue
SA1617: VoidReturnValueMustNotBeDocumented
SA1618: GenericTypeParametersMustBeDocumented
SA1619: GenericTypeParametersMustBeDocumentedPartialClass
SA1620: GenericTypeParameterDocumentationMustMatchTypeParameters
SA1621: GenericTypeParameterDocumentationMustDeclareParameterName
SA1622: GenericTypeParameterDocumentationMustHaveText
SA1624: PropertySummaryDocumentationMustOmitSetAccessorWithRestricedAccess
SA1625: ElementDocumentationMustNotBeCopiedAndPasted
SA1626: SingleLineCommentsMustNotUseDocumentationStyleSlashes
SA1627: DocumentationTextMustNotBeEmpty
SA1628: DocumentationTextMustBeginWithACapitalLetter
SA1629: DocumentationTextMustEndWithAPeriod
SA1631: DocumentationTextMustMeetCharacterPercentage
SA1632: DocumentationTextMustMeetMinimumCharacterLength
SA1636: FileHeaderMustContainFileName
SA1643: DestructorSummaryDocumentationMustBeginWithStandardText
SA1644: DocumentationHeadersMustNotContainBlankLines
SA1645: IncludedDocumentationFileDoesNotExist
SA1646: IncludedDocumentationXPathDoesNotExist
SA1647: IncludeNodeDoesNotContainValidFileAndPath

Pravidla rozvržení (Layout Rules)
SA1500: CurlyBracketsForMultiLineStatementsMustNotShareLine
SA1501: StatementMustNotBeOnSingleLine
SA1502: ElementMustNotBeOnSingleLine
SA1503: CurlyBracketsMustNotBeOmitted
SA1504: AllAccessorMustBeMultiLineOrSingleLine
SA1505: OpeningCurlyBracketsMustNotBeFollowedByBlankLine
SA1506: ElementDocumentationHeadersMustNotBeFollowedByBlankLine
SA1507: CodeMustNotContainMultipleBlankLinesInARow
SA1508: ClosingCurlyBracketsMustNotBePrecededByBlankLine
SA1509: OpeningCurlyBracketsMustNotBePrecedededByBlankLine
SA1510: ChainedStatementBlocksMustNotBePrecededByBlankLine
SA1511: WhileDoFooterMustNotBePrecededByBlankLine
SA1512: SingleLineCommentsMustNotBeFollowedByBlankLine
SA1513: ClosingCurlyBracketMustBeFollowedByBlankLine
SA1514: ElementDocumentationHeaderMustBePrecededByBlankLine
SA1515: SingleLineCommentMustBePrecededByBlankLine
SA1516: ElementsMustBeSeparatedByBlankLine

Pravidla pro udržovatelnost (Maintainability Rules)
SA1119: StatementMustNotUseUnnecessaryParenthesis
SA1400: AccessModifierMustBeDeclared
SA1401: FieldsMustBePrivate
SA1402: FileMayOnlyContainASingleClass
SA1403: FileMayOnlyContainASingleNamespace
SA1404: CodeAnalysisSuppressionMustHaveJustification
SA1405: DebugAssertMustProvideMessageText
SA1406: DebugFailMustProvideMessageText
SA1407: ArithmeticExpressionsMustDeclarePrecedence
SA1408: ConditionalExpressionsMustDeclarePrecendence
SA1409: RemoveUnnecessaryCode
SA1410: RemoveDelegateParenthesisWhenPossible

Pravidla názvosloví (Naming Rules)
SA1300: ElementMustBeginWithUpperCaseLetter
SA1301: ElementMustBeginWithLowerCaseLetter
SA1302: InterfaceNamesMustBeginWithI
SA1303: ConstFieldNamesMustBeginWithUpperCaseLetter
SA1304: NonPrivateReadonlyFieldsMustBeginWithUpperCaseLetter
SA1305: FieldNamesMustNotUseHungarianNotation
SA1306: FieldNamesMustBeginWithLowerCaseLetter
SA1307: AccessibleFieldsMustBeginWithUpperCaseLetter
SA1308: VariableNamesMustNotBePrefixed
SA1309: FieldNamesMustNotBeginWithUnderscore
SA1310: FieldNamesMustNotContainUnderscore

Pravidla pořadí (Ordering Rules)
SA1201: ElementsMustAppearInTheCorrectOrder
SA1202: ElementsMustBeOrderedByAccess
SA1203: ConstantsMustAppearBeforeFields
SA1204: StaticElementsMustAppearBeforeInstanceElements
SA1206: DeclarationKeywordsMustFollowOrder
SA1207: ProtectedMustComeBeforeInternal
SA1208: SystemUsingDirectivesMustBePlacedBeforeOtherUsingDirectives
SA1209: UsingAliasDirectivesMustBePlacedAfterOtherUsingDirectives
SA1210: UsingDirectivesMustBeOrderedAlphabeticallyByNamespace
SA1211: UsingAliasDirectivesMustBeOrderedAlphabeticallyByAliasName
SA1212: PropertyAccessorsMustFollowOrder
SA1213: EventAccessorsMustFollowOrder

Pravidla pro čitelnost (Readability Rules)
SA1100: DoNotPrefixCallsWithBaseUnlessLocalImplementationExists
SA1102: QueryClauseMustFollowPreviousClause
SA1103: QueryClausesMustBeOnSeparateLinesOrAllOnOneLine
SA1104: QueryClauseMustBeginOnNewLineWhenPreviousClauseSpansMultipleLines
SA1105: QueryClausesSpanningMultipleLinesMustBeginOnOwnLine
SA1106: CodeMustNotContainEmptyStatements
SA1107: CodeMustNotContainMultipleStatementsOnOneLine
SA1108: BlockStatementsMustNotContainEmbeddedComments
SA1109: BlockStatementsMustNotContainEmbeddedRegions
SA1110: OpeningParenthesisMustBeOnDeclarationLine
SA1111: ClosingParenthesisMustBeOnLineOfOpeningParenthesis
SA1112: ClosingParenthesisMustBeOnLineOfOpeningParenthesis
SA1113: CommaMustBeOnSameLineAsPreviousParameter
SA1114: ParameterListMustFollowDeclaration
SA1115: ParameterMustFollowComma
SA1116: SplitParametersMustStartOnLineAfterDeclaration
SA1117: ParametersMustBeOnSameLineOrSeparateLines
SA1118: ParameterMustNotSpanMultipleLines
SA1120: CommentsMustContainText
SA1122: UseStringEmptyForEmptyStrings
SA1123: DoNotPlaceRegionsWithinElements

Pravidla pro řádkování a mezery (Spacing Rules)
SA1000: KeywordsMustBeSpacedCorrectly
SA1001: CommasMustBeSpacedCorrectly
SA1002: SemicolonsMustBeSpacedCorrectly
SA1003: SymbolsMustBeSpacedCorrectly
SA1004: DocumentationLinesMustBeginWithSingleSpace
SA1005: SingleLineCommentsMustBeginWithSingeSpace
SA1006: PreprocessorKeywordsMustNotBePrecededBySpace
SA1007: OperatorKeywordMustBeFollowedBySpace
SA1008: OpeningParenthesisMustBeSpacedCorrectly
SA1009: ClosingParenthesisMustBeSpacedCorrectly
SA1010: OpeningSquareBracketsMustBeSpacedCorrectly
SA1011: ClosingSquareBracketsMustBeSpacedCorrectly
SA1012: OpeningCurlyBracketsMustBeSpacedCorrectly
SA1013: ClosingCurlyBracketsMustBeSpacedCorrectly
SA1014: OpeningGenericBracketsMustBeSpacedCorrectly
SA1015: ClosingGenericBracketsMustBeSpacedCorrectly
SA1016: OpeningAttributeBracketsMustBeSpacedCorrectly
SA1017: ClosingAttributeBracketsMustBeSpacedCorrectly
SA1018: NullableTypeSymbolsMustNotBePrecededBySpace
SA1019: MemberAccessSymbolsMustBeSpacedCorrectly
SA1020: IncrementDecrementSymbolsMustBeSpacedCorrectly
SA1021: NegativeSignsMustBeSpacedCorrectly
SA1022: PositiveSignsMustBeSpacedCorrectly
SA1023: DereferenceAndAccessOfSymbolsMustBeSpacedCorrectly
SA1024: ColonsMustBeSpacedCorrectly
SA1025: CodeMustNotContainMultipleWhitespaceInARow
SA1026: CodeMustNotContainSpaceAfterNewKeywordInImplicitlyTypedArrayAllocation
SA1027: TabsMustNotBeUsed

StyleCop nabízí podporu pro přidávání vlastních validačních pravidel. Vytvořit vlastní pravidlo není příliš složité. Příklad by se možná mohl objevit na tomto blogu jako článek v budoucnu. Pokud bude poptávka :)

Závěrem

Pokud Vás článek zaujal, můžete připsat komentář. Můžete se také zeptat na zkušenosti z praxe i na další aspekty použití StyleCopu.

Používáte jinou statickou analýzu? Jakou? Jak jste s ní spokojeni?

pondělí 27. června 2011

Zbavte se nemrtvého kódu

Všichni to známe. Kód, který se dlouho vyhříval na výsluní programátorovy přízně se stal jednoho dne nechtěným. Byl nahrazen pěknější a odladěnější implementací. Programátorovi však bylo líto přímo odstranit výsledek svého dřívějšího snažení a proto kód pouze zakomentoval. Co kdyby se někdy v budoucnu konalo zmrtvýchvstání některé části kódu? Tím však pouze oddálil jeho nezvratný osud a připustil, aby se mrtvý kód zjevoval jako přízrak stojící v temných zákoutích definicí tříd a strašil náhodné zvědavce prohlížející tyto třídy.

Mrtvý (nebo spíš nemrtvý) kód bývá součástí každého projektu. I u nás se objevil a to ve větším než malém množství. V okamžiku, kdy jsme zaktivovali statickou analýzu kódu, nám začaly nabíhat varování o nesprávně zakomentovaném kódu. Byl to pro nás impuls, abychom se nad přítomností takového kódu zamysleli. A výsledný verdikt je jednoznačný - přítomnost nemrtvého kódu je v produkční vývojové větvi nepřípustná.

Důvody, proč je nemrtvý kód škodlivý:

  • Snižuje čitelnost kódu. Ať už se jedná o zakomentovaný samostatný řádek nebo o celou metodu třídy, vždy narušuje čistotu kódu a ztěžuje Vám orientaci v kódu. Známá poučka říká, že poměr času strávený čtením a orientací v kódu a vlastního psaní kódu je 5 : 1. Proto každý nadbytečný řádek kódu je škodlivý.
  • Zvyšuje míru nejistoty. Po nějaké době i autor zakomentování kódu ztrácí přehled o důvodech, proč kód zakomentoval a především proč jej přímo neodstranil. Není si jistý, zda-li neplánoval znovupoužití kódu a proto nenachází odvahu se takovéhoto kódu zbavit. Nemrtvý kód začne v projektu po čase zapáchat a není to nic příjemného ani pro ostatní členy týmu, kteří do Vaší třídy zavítají.
  • Komplikuje prohledávání kódu. Hledaný výraz se objeví i ve výsledcích vyhledávání, které jsou v nemrtvém kódu. To je minimálně otravné.
  • Komplikuje slučování změn ve vývojových větvích a umisťování změn na server. Nemrtvý kód může být zdrojem dalších konfliktů, které budete muset zbytečně řešit.

Abyste mohli beze strachu odstraňovat nepotřebný kód, měli byste používat verzovací systém. Může se stát, že si odmažete část implementace, ke které se potřebujete vrátit. Právě v těchto okamžicích Vám začne být Váš verzovací systém ještě sympatičtější. Prostě o žádný kód napsaný dříve a umístěný na server nepříjdete. Pokud však nepříjdete přímo o celý server.

Pokud byste přesto chtěli tyto kódové zombíky pěstovat a potřebovali byste poradit klávesovou zkratku pro rychlé zakomentování, můžete se podívat na příspěvek jednoho nebohého kodéra. Začíná svůj text slovy "Kód neodstraňuji, nikdy nevíte, kdy bude znovu potřeba" ... a následně to v komentářích pěkně schytal ... ;-)