Martin Dybal

Vývojářský blog Martina Dybala

Podle kategorie

Aspektově orientované programování - Úvod

3. díl - Aspektově orientované programování - Úvod

Martin Dybal       24. 10. 2015       PostSharp a AOP       4176 zobrazení

Už jste někdy viděli metodu, která vypadá nějak takto?

public IEnumerable<IGrouping<TKey, T>> GroupWhile(
    IEnumerable<T> source,
    Func<T, T, bool> predicate,
    Func<T, T, TKey> keyOfGroup)
{
    Log.Debug("GroupWhile has started");
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (predicate == null)
    {
        throw new ArgumentNullException("predicate");
    }
    if (keyOfGroup == null)
    {
        throw new ArgumentNullException("keyOfGroup");
    }
    try
    {
        var enumerator = new EnumeratorWithPrevious<T>(source);
        while (enumerator.MoveNext())
        {
            if (!predicate(enumerator.Previous, enumerator.Current))
            {
                yield return CreateNewGroup(enumerator.Current);
            }
            Add(enumerator.Current);
        }
        yield return CreateNewGroup(enumerator.Current);
    }
    catch (Exception e)
    {
        Log.Error(e); 
        throw;
    }
    finally
    {
        Log.Debug("GroupWhile has ended");
    }
}

Věřím, že ano. Takováto implementace má ale několik problémů:

  • Metoda je příliš dlouhá.
  • Porušuje single responsibility principle.
  • Většina kódu metody se stará a ošetření vstupních parametrů a logování a ne o to, co má metoda skutečně dělat.
  • Je velmi pravděpodobné, že další metody se budou stejným způsobem starat o ošetření vstupních parametrů a logování, a tím budeme porušovat pravidlo don't repeat yourself

Díky aspektově orientovanému programování se dá metoda přepsat do této podoby.

[Log] 
public IEnumerable<IGrouping<TKey, T>> GroupWhile(
    [Required] IEnumerable<T> source,
    [Required] Func<T, T, bool> predicate,
    [Required] Func<T, T, TKey> keyOfGroup)
{
    var enumerator = new EnumeratorWithPrevious<T>(source);
    while (enumerator.MoveNext())
    {
        if (!predicate(enumerator.Previous, enumerator.Current))
        {
            yield return CreateNewGroup(enumerator.Current);
        }
        Add(enumerator.Current);
    }
    yield return CreateNewGroup(enumerator.Current);
}

Zápis je snáze čitelný a o víc než polovinu kratší. Jak je to možné a jak to funguje, vám ukáži v sérii článků o aspektově orientovaném programování. Ukážeme základy aspektově orientovaného programovaní za použití frameworku Postsharp. Většinu příkladů budu přejímat ze stránek Postsharpu.

Co je to aspektově orientované programování

Dalo by se říci že aspektově orientované programování je nástavbou objektově orientovaného programování. Hlavním cílem aspektově orientované programování je eliminovat opakující se části kódu. Nejvíce aspekty využijeme pro úkoly jako je logování, exception handling, řízení přístupu podle uživatelských práv, cacheování, implementace INotifyPropertyChanged a mnoho dalších podobných úkolů.

Základní terminologie

  • Joinpoint
    • Místo v kódu, které je možné obalit nějakou logikou pomocí Advice.
    • Například volání metod, inicializace třídy, přístup k vlastnosti instance třídy.
  • Advice
    • Kód, kterým se obalí joinpoint.
    • Například zápis do logu
  • Pointcut
    • Je množina joinpointů, které jsou obaleny stejnými advice
    • Například všechny metody z assembly budou používat stejné logování.
  • Mixin
    • Výsledný objekt, po spojení advice a joinpoint
  • Target
    • Objekt, který je ovlivněn aspektem

Praktická ukázka

Dost teorie pojďme si aspektově orientované programování ukázat v praxi. Předpokládejme, že máme metodu a potřebujeme zjistit, kdy došlo k jejímu volání.

private static void SayHello()
{
    Console.WriteLine("Hello, world.");
}

Nejjednodušším způsobem, jak toho docílit, je přidat výpis do trace.

private static void SayHello()
{
    Trace.WriteLine("SayHello has started!");
    Console.WriteLine("Hello, world.");
    Trace.WriteLine("SayHello has ended");
}

Tohle je sice funkční řešení, ale porušuje single responsibility principle a navíc nás nutí k opakování kódu. Předpokládejme, že máme ještě jednu metodu, u které taky chceme vědět, kdy se volá.

private static void SayGoodBye()
{
    Trace.WriteLine("SayGoodBye has started!");
    Console.WriteLine("Good bye, world.");
    Trace.WriteLine("SayGoodBye has ended");
}

Tohle by se dalo vyřešit jednoduchou dekorační funkcí.

private static void TraceInfoDecorator(Action action)
{
    Trace.WriteLine(string.Format("{0} has started!", action.Method.Name));
    action();
    Trace.WriteLine(string.Format("{0} has ended", action.Method.Name));
}

private static void SayHello()
{
    Console.WriteLine("Hello, world.");
}

private static void SayGoodBye()
{
    Console.WriteLine("Good bye, world.");
}

Čímž přestaneme porušovat single responsibility principle a začneme dodržovat i don't repeat yourself, jenže funkci nesmíme volat přímo, museli bychom používat volání:

TraceInfoDecorator(SayHello);
TraceInfoDecorator(SayGoodBye);

Což není zrovna praktické. Teď už se konečně dostáváme k aspektově orientovanému programování. Ve výsledku naše metody budou vypadat takhle.

[TraceInfo]
private static void SayHello()
{
    Console.WriteLine("Hello, world.");
}

[TraceInfo]
private static void SayGoodBye ()
{
    Console.WriteLine("Good bye, world.");
}

Nejdříve si vytvoříme TraceInfo atribut. Atribut je v C# třída, která dědí od System.Attribute a je dobrým zvykem přidat na konec názvu Attribute.

public sealed class TraceInfoAttribute : Attribute { }

Aby atribut byl zároveň postsharpovým aspektem, musí implementovat rozhraní IAspect ze jmenného prostoru PostSharp.Aspects a musí být serializovatelný. Ve stejném jmenném prostoru se nachází mnoho tříd pro usnadnění vytváření aspektů. Dnes použijeme třídu OnMethodBoundaryAspect. Je potřeba mít ve Visual Studiu doinstalovaný doplněk PostSharp. Dá se stáhnout přímo z Visual Studia. Automaticky se naistaluje ve verzi free. Po instalaci je potřeba restartovat Visual Studio.

image

Dále je potřeba přidat PostSharp do projektu. Stačí kliknout pravým na projekt v solution exploreru a zvolit "Add PostSharp to project". Upravíme náš atribut tak aby dědil od třídy OnMethodBoundaryAspect, přidáme mu atribut Serializable.

[Serializable] 
public sealed class TraceInfoAttribute : OnMethodBoundaryAspect { }

OnMethodBoundaryAspect se postará o obalení metody blokem try a přidá šest virtuálních metod. Jejich volání vypadá takhle.

OnEntry(args);
try
{
    //Kód metody 
    //…
    OnSuccess(args);
}
catch (Exception)
{
    OnException(args);
    throw;
}
finally
{
    OnExit(args);
} 

Parametr args předávaný metodám je typu PostSharp.Aspects.MethodExecutionArgs. Dále jsou zde další dvě metody OnYield a OnResume jak název napovídá, OnYield se volá při vyskakování z metody pomocí yield return nebo await a metody OnResume se volá při vracení do metody.

Naimplementujeme metody OnEntry a OnExit.

[Serializable] 
public sealed class TraceInfoAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Trace.WriteLine(string.Format("{0} has started!", GetMethodFullName(args)));
    }
 
    public override void OnExit(MethodExecutionArgs args)
    {
        Trace.WriteLine(string.Format("{0} has ended", GetMethodFullName(args)));
    }
 
    private static string GetMethodFullName(MethodExecutionArgs args)
    {
        return string.Format("{0}.{1}.{2}",
            args.Method.DeclaringType.Namespace,
            args.Method.DeclaringType.Name,
            args.Method.Name);
    }
}

Náš aspekt je již funkční, že funguje si můžeme jednoduše ověřit.

static void Main(string[] args)
{
    Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));

    SayHello();
    SayGoodBye();
}

image

No a to je vše. Příště se podíváme, jak jednoduše se s PostSharpem dá udělat logování.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

Mohlo by vás také zajímat

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

...

Celé mi to připadá jako zbytečně komplikované přesunutí kódu z místa A na místo B a ještě za použití nějakých knihoven třetích stran...

Pokud je někdo schopen stvořit něco jako public IEnumerable<IGrouping<TKey, T>> GroupWhile(IEnumerable<T> source, Func<T, T, bool> predicate, Func<T, T, TKey> keyOfGroup), potom je třeba takového člověka vůbec nepouštět k psaní kódu a ne hledat způsoby, jak řešit jeho prasárny.

nahlásit spamnahlásit spam -3 / 5 odpovědětodpovědět

Takže autory .NET frameworku a LINQ byste také radši k psaní kódu nepouštěl? Protože na funkcích velmi podobných té, které kritizujete, je postaven celý LINQ, podívejte se na signaturu funkce GroupBy nebo Join.

A to přesunutí kódu, o kterém mluvíte, není z jednoho místa A na jedno místo B, ale z tisíce míst A na jedno místo B - aspekty elegantně řeší exception handling, logování a mnoho dalších věcí, takže místo toho, abyste měl v aplikaci tisíc try-catch bloků, máte prostě jeden atribut, případně ty aspekty můžete aplikovat podle nějakých konvencí a obejdete se bez atributu úplně.

nahlásit spamnahlásit spam 3 / 3 odpovědětodpovědět

Věru mě nezajímá, jak je Framework napsaný uvnitř, ani sám žádný nepíšu. V obyčejných aplikacích by se takový humus těžko někde našel.

Co se týče logování. Čím bude lepší toto než způsob, jaký používám já: Všude kde je potřeba něco zaznamenat, zavolám potřebnou metodu ze singleton třídy a je hotovo. V případě nutnosti změny logovacího mechanizmu provedu změnu na jediném místě.

Co se týče řešení vyjímek. Mějme dejme tomu dvacet, třicet míst, na kterém je potřeba řešit vyjímky trochu jiným způsobem, nelze tedy použít něco univerzálního. Jak by tohle pomohlo, aniž by bylo nutné srát se s implementací nějaké (jistě jako obvykle zbytečně komplikované) infrastruktury a tím ztrácet čas?

nahlásit spamnahlásit spam -1 / 3 odpovědětodpovědět

Jak je Linq napsaný uvnitř, vás nemusí zajímat pouze do té doby, než ho chcete více využívat a nebo napsat nějaké jeho rozšiřující metody. Navíc ta metoda v ukázce vůbec není podstatná.

Když chcete do logu zaznamenat výjimku tak ji musíte nejdřív odchytit, zapsat do logu a poté, pokud ji neumíte zpracovat, poslat dál a hlavně musíte pamatovat na to že se má v daném místě logovat. To je hodně kódu navíc a musíte spoléhat na to že máte v logu opravdu vše, rači tam toho mám víc a vyfiltruju si to, než zjišťovat co tam není.

Infrastruktura je zbytečná leda na malých projektech. Na velkém projektu se vám určitě vrátí, pokud máte dostatečně inteligentní vývojáře, pokud architekturu a použité technologie tým nechápe a použije je špatně, pak je to opravdu ztrácení času.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Takže pokud vám v LINQ nějaká metoda chybí, tak si ji prostě nenapíšete a budete čekat do další verze, jestli se MS nesmiluje a neudělá vám ji tam? Zřejmě píšete velmi jednoduché aplikace a ještě k tomu sám. Logovat pomocí singleton objektu je taky vhodné pouze v malých projektech, může to dělat problémy jak při testování (na což máte asi taky malé projekty) anebo ve chvíli, kdy potřebujete část věcí logovat na jedno místo, část věcí na jiné atd., například kvůli tomu, že ne každý člověk v týmu má přístup na produkční server a nechcete mu ukazovat úplně všechny logy, ale třeba jen ty, které neobsahují žádná citlivá data apod., takových nebo podobných situací jsem zažil desítky.

Ano, pokud při každé výjimce potřebujete udělat něco jiného, pak se aspekty použít nedají. Akorát já jsem tak v 90% projektů potkal spíš opačnou situaci - výjimka může nastat na desítkách nebo stovkách míst a potřebuji se zachovat stejně - zalogovat, co se stalo, pak uživateli zobrazit okénko, že nastala nějaká chyba, a pokud jsem uvnitř nějaké transakce, tak udělat rollback.

A pozor na to, i z malých projektů bez infrastruktury se po pár letech někdy stávají velké projekty, kde ta infrastruktura bude citelně chybět. A přesvědčit zákazníka, že teď už by to vážně chtělo přepsat, není vždy jednoduché.

nahlásit spamnahlásit spam 2 / 2 odpovědětodpovědět

- LINQ je sám o sobě tak mocný, že jsem nikdy neměl potřebu ho rozšiřovat.

- Pokud potřebuju logovat na více různých míst, stačí mi přidat potřebného TraceListenera do konfiguračního souboru.

- Zřejmě píšete velmi jednoduché věci, pokud se všude chováte k vyjímkám stejně. To já ji někde potřebuju zaznamenat, někde zahodit, někde zobrazit zprávu uživateli, jinde ne.

- Ještě nikdy jsem nepsal věc, která by později nešla lehce rozšířit a to bez jakékoliv infrastruktury nutné pro podobná zvěrstva.

- Testy nepíšu, jsem odpůrce unit testů. Uživatelské rozhraní (část, která potřebuje testovat nejvíce) se testuje extrémě špatně a u aplikací, jaké píšu já, by bylo napsání efektivního testu aplikační logiky mnohem složitější, než napsání samotné aplikace. A testování je o podvržení nějaké funkční části testovacím zmetkem a už to mi připadá k smíchu, protože se to nikdy ani nebude blížit skutečnému provozu.

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

souhlasím s Vámi.

Zmiňovanému kódu nepomůže AOP, ale komentář.

Log co mi zaloguje vyjimku je mi na prd, když k tomu nepřibalí věci, kvůli kterým vyjimka vznikla. Předpokladem pro toto tvrzení je že AOP nemá věšteckou kouli.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Nevím jak by komentář pomohl, ta metoda není složitá, jen dělá víc věcí najednou.

AOP opravdu nemá věšteckou kouli, ale má IL kód a z toho si zjistí vše co potřebuje. Jak logovat pomocí Log4Net pomocí PostSharpu se můžete podívat v druhém článku. Myslím si že takovéhle logování je dostatečné http://www.dotnetportal.cz/blogy/16/Mart...

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Nikdy jsem neslyšel horší názor a postoj než ten, že komentář prozradí v rámci troubleshootingu více jak samotný log aplikace. Buďto jste nikdy nedělal troubleshooting nebo neprogramujete pro firemní oblasti, ale pro vlastní zájmy.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback