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.
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();
}
No a to je vše. Příště se podíváme, jak jednoduše se s PostSharpem dá udělat logování.