Think First Development

Erst denken, dann programmieren

Durchsuche Beiträge in C#

Es hat sich herausgestellt, dass ist nicht der einzige bin, der von SnagIt heraus Bilder an Twitter schicken möchte. Nachdem es mich oft genug gestört hat, habe ich selber eine Lösung programmiert weiter lesen

Die Implementierung von Validierung ist ein leidiges Thema. Bei der Validierung muss man zwischen drei Arten des Validierungskontextes unterscheiden:

  • Validierung von atomaren Eigenschaften. Beispiel: Pflichtfelder.
  • Validierung eines einfachen Modells, bei dem sich die zu prüfenden Eigenschaften innerhalb einer Klasse befinden. Beispiel: Geburtstag liegt vor dem Todestag.
  • Validierung eines komplexen Modells bei dem mehrere Objekte oder Klassen involviert sind. Beispiel: Die Kombination Vorname und Nachname muss eindeutig sein.

Das ASP.NET MVC2 Framework unterstützt die Validierung von Eigenschaften mittels DataAnnotations. Mehr dazu in einem vorherigen Blogeintrag. Der Vorteil dieser DataAnnotations liegt in der Wiederverwendbarkeit der Attribute bei verschiedenen Eigenschaften.

Mit der Version 3 des ASP.NET MVC Frameworks ist eine Modellvalidierung in Kombination mit der Eigenschaftsvalidierung möglich. Da im Gegensatz zur Eigenschaftsvalidierung ist die Modellvalidierung nicht wiederwendbar und wird daher direkt in das Modell implementiert. Dazu muss die Klasse die Schnittstelle “IValidatableObject” mit der Methode “Validate” implementieren. Der Rückgabewert ist ein IEnumerable<of ValidationResults>, die sowohl Modellfehler als auch Eigenschaftsfehler enthält. Das kleine Beispiel zeigt die Verwendung der Modellvalidierung, sowohl für eigenschaftsbezogene Fehler, als auch für allgemeine Modellfehler.

image

image

Dass die clientseitige Validierung vor der serverseitigen ausgeführt wird ist logisch. Das bedeutet, das standardmäßig nur die Validierungen der integrierten DataAnnotations durchgeführt wird.

Überraschend ist allerdings folgendes serverseitiges Verhalten, wenn die clientseitige Validierung nicht aktiviert ist: Die DataAnnotation-Validierung werden vor der Modellvalidierung durchgeführt. Hat die DataAnnotation-Validierung mindestens einen Fehler, wird die Modellvalidierung nicht durchgeführt. Das Verhalten hat gewünschte oder unerwünschte Nebenwirkungen. Die serverseitige Validierung wird bei Fehlern mehrmals durchlaufen, einmal für die DataAnnotationen und einmal für die Modellvalidierung. Der klare Vorteil liegt hier, wenn die Modellvalidierung komplexe Datenbankabfragen macht, da diese bei einfachen Modellfehlern nicht durchgeführt werden. Im Gegenzug ist es nicht möglich, Validierungsfehler von DataAnnotations und Modellvalidierung gleichzeitig anzuzeigen.

Die Modellvalidierung ist ein wichtiger Schritt, um die Validierung zu vereinheitlichen. Damit werden individuelle Implementierungen und Konzepte durch ein gemeinsames Konzept abgelöst.

Das Ermitteln einer View folgt dem Mechanismus “Von Spezialisierten zum Generalisierten”. Das folgende Bild zeigt die Reihenfolge in Kombination mit Areas.

Diese Reihenfolge gilt für den Lookup von sowohl Views als auch Partial Views. Das gilt für jedes (!) Lookup, auch innerhalb von Views (z.B. bei Html.RenderPartial()). Diesen Umstand kann man sich zu nutze machen um Polymorphie abzubilden. Gegeben ist folgendes Modell.

image

Damit bietet es sich an, eine Shared View für “Contact” zu erstellen. Aber was passiert mit den Eigenschaften, die nur in den spezialisierten Klassen “Person” und “Company” existieren. Die ContactView ist für die “Contact” Basisklasse typisiert.

An dieser Stelle hilft die Lookup-Reihenfolge. Benutzt man in der Shared View ein RenderPartial(), kann man spezialisierten Views in den Controllern definieren.

image

Das Ergebnis ist, dass im CompanyController zusätzlich die CompanyView “SpecialView” gerendert wird und im PersonController die PersonView “SpezialView”. Es wird also automatisch zuerst in den Views für den Controller nachgeschaut. Einziges Problem mit RenderPartial() ist eine Exception, wenn in einem dritten Controller die “SpezialView” nicht implementiert ist, die ContactView aber gerendert wird.

image  image

Zu bemerken ist abschließend noch, dass die SpezialViews auf die spezialisierten Klassen typisiert ist, also Person bzw. Company. Das MVC2 Framework übernimmt (vereinfacht dargestellt) die Umwandlung von Contact in Person bzw. Company.

Serverseitige Validierung

Das ASP.NET MVC2 Framework biete eine einfache Möglichkeit zur Validierung eines Modells: Data Annotations. Mit Attributen wird die Validierung für das Modell definiert. Im MVC2 Framework sind vier Validatoren enthalten: Required, StringLength, Range und RegularExpression. weiter lesen

Vor nicht allzu langer Zeit … habe ich im SQL Server 2008 ein Aggregat-Funktion vermisst, die es bereits in MYSQL gibt: GROUP_CONCAT. Damit ist es bei einer Gruppierung möglich, Werte zu verketten.

Es begab ich folgendes vereinfachtes Problem: Es war Tabelle mit Salaten, die Personen zum Grillen mitbringen. Das Ziel sollte ein Liste gruppiert nach dem Salat sein. Zusätzlich zur Zählung (COUNT) sollten noch alle Personen aufgelistet werden, die den entsprechenden Salat mitbringen (CONCAT).

imageimage

Google hatte für dieses Problem verschiedene Lösungen. Die einen waren zu kompliziertes SQL (un-Wartbarkeit!) und funktionierten nur für endlich Anzahl von Zeilen bzw. nicht wiederverwendbare benutzerdefinierte Funktion. Die eleganteste aber aufwändigste Lösung im Bezug auf das Deployment schien das Entwickeln einer eigenen Aggregat-Funktion “Concat”. An Ende war die eigene Aggregatfunktion das einfachste und das Deployment überschaubar (Adminsicher ;) ).

Vorgehen

  1. Erstellen der Assembly
  2. SQL Server anpassen
  3. Aggregatfunktion verwenden

.NET Assembly erstellen

Visual Studio 2010 unterstützt bei der Erstellung von SQL Server Assemblys über den “Neues Projekt” Wizard.

image

Anschließen kann über “Hinzufügen” eine neues Aggregat angelegt werden.

image

Das Klasse der Aggregatfunktion in der .NET Assembly ist selbsterklärend. Die Methoden werden mittels Reflection aufgerufen, daher ist keine separate Schnittstelle zu implementieren. Die Schnittstelle “IBinarySerialize” dient dazu, das in dem Klassenattribut deklarierte “Format.UserDefined” (MSDN) zu implementieren.

using System;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.IO;
using System.Text;

namespace SwkAggregates
{
    [Serializable]
    [SqlUserDefinedAggregate(
        Format.UserDefined, //use clr serialization to serialize the intermediate result
        IsInvariantToNulls = true, //optimizer property
        IsInvariantToDuplicates = false, //optimizer property
        IsInvariantToOrder = false, //optimizer property
        MaxByteSize = 8000) //maximum size in bytes of persisted value
    ]
    public class Concat : IBinarySerialize
    {
        /// <summary>
        /// The variable that holds the intermediate result of the concatenation
        /// </summary>
        private StringBuilder _intermediateResult;

        /// <summary>
        /// Initialize the internal data structures
        /// </summary>
        public void Init()
        {
            this._intermediateResult = new StringBuilder();
        }

        /// <summary>
        /// Accumulate the next value, not if the value is null
        /// </summary>
        /// <param name="value"></param>
        public void Accumulate(SqlString value)
        {
            if (value.IsNull)
            {
                return;
            }

            this._intermediateResult.Append(value.Value).Append(", ");
        }

        /// <summary>
        /// Merge the partially computed aggregate with this aggregate.
        /// </summary>
        /// <param name="other"></param>
        public void Merge(Concat other)
        {
            this._intermediateResult.Append(other._intermediateResult);
        }

        /// <summary>
        /// Called at the end of aggregation, to return the results of the aggregation.
        /// </summary>
        /// <returns></returns>
        public SqlString Terminate()
        {
            string output = string.Empty;
            //delete the trailing comma, if any
            if (this._intermediateResult != null
                && this._intermediateResult.Length > 1)
            {
                output = this._intermediateResult.ToString(0, this._intermediateResult.Length – 2);
            }

            return new SqlString(output);
        }

        public void Read(BinaryReader r)
        {
            _intermediateResult = new StringBuilder(r.ReadString());
        }

        public void Write(BinaryWriter w)
        {
            w.Write(this._intermediateResult.ToString());
        }
    }
}

Hinweis: Die Assembly muss keinen Strong Name besitzen. Es lassen sich auch eine nicht-signierte Assemblys in den SQL Server hochladen. Ob das Bestandteil der neuen Sicherheitspolitik von Microsoft ist wage ich zu bezweifeln.

Also bleibt nur noch das Kompilieren der DLL und anschließend das Integrieren in den SQL Server.

SQL Server Integration

Die SQL Server Integration besteht aus zwei Schritten.

  1. Hochladen der Assembly in den SQL Server
  2. Erstellen einer Aggregatfunktion mit einem “API-Call” auf die Klasse in der DLL

Das Script enthält auch die optionalen Schritte, wenn das Ausführen von Assemblys in der Datenbank noch nicht aktiviert wurde. Das Script ist durch die Kommentierung (hoffentlich) selbsterklärend.

– Add assembly to database
CREATE ASSEMBLY SwkUtilities FROM ‘d:\SwkAggregates.dll’
WITH PERMISSION_SET=SAFE;
GO

– Create aggregate function which calls the concrete "Concat" class
CREATE AGGREGATE Concat(@input nvarchar(4000))
RETURNS nvarchar(4000)
EXTERNAL NAME [SwkUtilities].[SwkAggregates.Concat];
GO

– Enable cls code execution. It is disabled by default
sp_configure ‘clr enabled’, 1;

GO

– Deploy the "clr enabled" setting in sql server
reconfigure
GO

Zu bemerken ist, dass die Assembly im SQL Server einen symbolischen Namen “SwkUtilities” erhält. Der API-Call erfolgt über diesen symbolischen Namen. Außerdem wird die DLL wird in den SQL Server hochgeladen und ist damit Bestandteil der Datenbank. Das hat natürlich Auswirkungen auf Backup und Restore Szenarien. Bei einem Backup werden die Assemblies mitgesichert d.h. es entfällt ein FileSystem Backup. Im Gegenzug werden mit einem Restore der Datenbank auch die Assemblies wiederhergestellt.

SQL Verwendung

Ab jetzt heißt es viel Spaß mit der neuen Aggregat Funktion und der geplanten Grillfete ;) .

SELECT Salat, Count(*), dbo.Concat(Person)
FORM Grillfest
GROUP BY Salat

Mvc2_Area-SupportArea Support

In der zweite Version des MVC Frameworks für ASP.NET wurden sog. “Areas” eingeführt. Mit Areas können Controller thematisch gruppiert und als Unterprojekt verwaltet werden. Jede Area bekommt eine eigene Ordnerstruktur mit “Controllers”, “Models” und “Views” und enthält damit zu den Controllern die entsprechenden Views und Models. Eine Area kann als separates Modul betrachtet und entwickelt werden. Große und komplexe Projekte können in kleine überschaubare Module gegliedert werden.

Vorteile ergeben sich besonders, wenn Module tatsächlich unabhängig voneinander implementiert werden. So können wiederkehrende oder zentrale Komponenten wie “Blog”, “Wiki” oder “Benutzeradministration” zentral implementiert und weiterentwickelt werden. Projekte, welche diese Komponenten verwenden, können auf diese Module verweisen oder bei kundenspezifischen Anpassungen einen bestimmten Entwicklungsstand verwenden und anpassen.

Areas anlegen und verwenden

Areas werden im Visual Studio über das Kontextmenü “Hinzufügen” –> “Bereich/Area” hinzugefügt. Nach der Eingabe des Namens werden die Verzeichnisse und die Infrastruktur für das erweiterte Routing (“[Area-Name]AreaRegistration.cs”) angelegt. Die Area-Registration erbt von einer Klasse “AreaRegistration” um die Routen separiert pro Modul definieren zu können. Dadurch wird eine absolute Unabhängigkeit der Module realisiert.

Für Links innerhalb der Applikation kann weiterhin die “Html.ActionLink()” Methode verwendet werden. Es muss nur ein weiteres Routing-Schlüsselwort “Area” in den RouteValues für die Ziel-Area definiert werden. Zu beachten ist allerdings die polymorphe Signatur der Methode.

<li><%: Html.ActionLink("Admin", "Index", "Default", new {Area = "Admin"}) %></li>
<li><%: Html.ActionLink("Admin", "Index", "Default", new {Area = "Admin"}, new {}) %></li>

Die 2. Zeile scheint gleich zu sein, da der Parameter leer ist. Der leere Parameter bewirkt aber den Aufruf einer anderen Methode wegen einer anderen Methodensignatur. Im ersten Fall wird der Parameter mit der Area Definition als “HtmlAttributes” interpretiert. In zweiten Fall wird der leere Parameter als “HtmlAttributes” interpretiert und die Area Definition wird zu "RouteValues”.

Die Methode “ActionLink()” arbeitet relativ zu der aktuellen Area. Befindet man sich in der Root Area ist alles kein Problem. Befindet man sich allerdings innerhalb einer Area wird als Default die aktuelle Area verwendet. Wichtig wird das bei zentralen Menüs oder in der Masterpage. Die Masterpage eines Standard MVC2 Projektes enthält Links auf die Startseite (DefaultController –> Index) ohne Angabe einer Area. Innerhalb einer Area wird daraus der Link auf die Startseite der Area (Area/DefaultController –> Index). Der Link unabhängig von der Area auf die Startseite muss also mit leerer Area erfolgen:

<li><%: Html.ActionLink("Admin", "Index", "Default) %></li>
<li><%: Html.ActionLink("Admin", "Index", "Default", new {Area = ""}, new {}) %></li>

Zwei Controller, ein Name

Gibt es in einem Projekt zwei Controller mit dem gleichen Namen funktioniert die automatisch generierte Default-Route nicht mehr. Das MVC Framework ist nicht in der Lage ohne Angabe des Namespaces den richtigen Controller zu finden, das MVC nur den Kurznamen kennt. Der Default-Route muss in einem solchen Fall der Namespaces des Default-Controlles als Parameter mitgegeben werden.

routes.MapRoute(
    "Default", // Routenname
    "{controller}/{action}/{id}", // URL mit Parametern
    new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameterstandardwerte
    new [] {"SmartHome.Mvc"} // Namespace
);

Shared Views

Bei der Verwendung der Shared Views ist zu berücksichtigen, dass bei automatisch ermittelten Controls erst in der betreffenden Area und anschließend in der Root Area nachgeschlagen wird. Interessant werden diese Aspekte bei z.B. EditorTemplates oder DisplayTemplates. So können Views zentral bereitgestellt und für Areas separat überschrieben werden.

Mvc2_Page_Lookup

Fazit

Areas sind eine sehr gute Erweiterung des Frameworks. Sind die Stolpersteine bekannt können Areas intuitiv verwendet werden. Besonders im Hinblick auf die modulare Entwicklung von Komponenten bieten sich hier viele Möglichkeiten. Durch das Verwenden eines Team Foundation Servers und Workspacedefinitionen ist ein Zusammenstecken einer Applikation sehr einfach, da wirklich auf die Module innerhalb eines andern Ordners im TFS verwiesen werden kann.

Disclaimer: Diejenigen, die sich gerne unvorbelastet an die FizzBizz Coding Kata heranwagen möchten, sollten hier besser nicht weiterlesen. Der Artikel wird verschiedene Lösung zeigen ;)

Einleitung

Unser firmeninternes Coding Dojo hatte eine relativ einfache Kata: FizzBuzz. Zwar ist die Aufgabe einfach und überschaubar, aber dafür sind die Erweiterungen vielfältig. Bei dieser Kata konnte man das Prinzip von Unit Test und Test Driven Development verdeutlichen. Erweitert man die Implementierung, kann die Implementierung der alten Anforderungen direkt getestet werden. Man merkt also, ob man über die neue Anforderung die alten Anforderungen vergessen hat. Als Organisator habe ich mir in Vorfeld Gedanken über die mögliche Implementierung gemacht, da ich einer der beiden Programmierer war. Bei der Kata musst ich dafür Sorgen, dass die Schnittstellen/Vorgaben so definiert sind dass der Test keine weiteren Vorgaben für die Implementierung macht als die Schnittstelle. Folgendes Klassengerüst war also vorgegeben:


public class FizzBuzzer
{
    public IEnumerable<string> FizzBuzz(int maxZahl)
    {
        throw new NotImplementedException();
    }
}

Das Pair-Programming-System ist auch in ein verändertes Ping-Pong geändert worden: Ich habe einen Test geschrieben, mein Co-Programmierer hat den Test implementiert und ich habe den nächsten Test geschrieben.

Dojo Lösung

Die Lösung im Dojo war die einfachste aller Möglichkeiten…eine Implementierung der Methode und das wars. Selbst als die Erweiterungen kamen wurde der FizzBuzzer höchstens um zwei Konstruktor-Parameter erweitert. Und damit die Tests laufen gabe es einen Default-Konstruktor mit dem Standard “3″ und “5″.


public class FizzBuzzer
{
    #region Felder
    private int _n;
    private int _m;
    #endregion

    #region Konsturktor: (), (n, m)
    public FizzBuzzer() : this(3,5)
    {
    }

    public FizzBuzzer(int n, int m)
    {
        _n = n;
        _m = m;
    }
    #endregion

    public IEnumerable<string> FizzBuzz(int maxNumber)
    {
        //Plausibilitäts prüfung
        if (maxNumber < 1)throw new ArgumentException("Nur Zahlen größer 0 sind erlaubt");
        if (maxNumber > 100000) throw new ArgumentException("Nur Zahlen kleiner oder gleich 100000 sind erlaubt");

        //Liste anlegen
        List<string> words = new List<string>();

        //Alle Zahlen durchlaufen und prüfen
        for (int curNumber = 1; curNumber <= maxNumber; curNumber++)
        {
            //Zahl durch n und m teilbar?
            if ((curNumber % _n) == 0 && (curNumber % _m) == 0)
                words.Add("FizzBuzz");
            else if ((curNumber % _m) == 0)
                words.Add("Buzz");
            else if ((curNumber % _n) == 0)
                words.Add("Fizz");
            else
                words.Add(curNumber.ToString());
        }

        //Ergebniss zurückliefern
        return words;
    }
}

Das ist natürlich nur eine von vielen Varianten einen FizzBuzz zu implementieren. Der Ansatz bei dieser Implementierung ist eine Klasse mit einer Funktion, die alle Varianten enthält. Versucht man eine Erweiterung mit beliebig vielen Fizz-Buzz-Zahlen und Wörtern, würde man die Zahl/Wort Kombination wahrscheinlich in einem Dicitonary ablegen und den zusammengesetzten String (FizzBuzz) für jeden Key des Dictionarys erweitern. Diese Lösung funktioniert nur, weil der Alorithmus zum Ermitteln des FizzBuzz gleich ist und nur durch eine Zahl parametrisiert ist. Ändert sich der Algorithmus oder ist der Algorithmus zum Zeitpunkt der Implementierung unbekannt, ist diese Lösung ungünstig oder funktioniert gar nicht mehr.

Delegaten als Parametrierung

Die Lösung, über die Werte zu Parametrieren reicht also nicht aus, da auch der Algorithmus und die Anzahl der Funkionen (Delegaten)variabel sein kann. Damit muss es eine Lösung geben, mit der ich Funktionen übergeben kann. Und von diesen Möglichkeiten gibt es in C# viele.

Schnittstellen

Der Klassiker ist eine Schnittstelle IFizzBuzz oder eine abstrakte Basisklasse mit unterschiedlichen Implementierungen für die verschiedenen Algorithmen. Mehr oder weniger parametrisierte Implementierungen dieser Schnittstelle werden an eine “Add” Methode des FizzBuzzers übergeben.


public class InterfaceFizzBuzzer
{
    private List<IFizzBuzz> buzzList = null;

    public InterfaceFizzBuzzer()
    {
        buzzList = new List<IFizzBuzz>();
    }

    public void Add(IFizzBuzz aBuzz)
    {
        buzzList.Add(aBuzz);
    }

    public IEnumerable<string> FizzBuzz(int maxNumber)
    {
        IList<string> result = new List<string>();

        for (int i = 1; i <= maxNumber; i++)
        {
            // Result ermitteln
            var numberResult = string.Empty;
            foreach (var item in buzzList)
                currentRes += item.GetForNumber(i);

            // Ergebnis hinzufügen
            if (string.IsNullOrEmpty(numberResult))
                result.Add(i.ToString());
            else
                result.Add(numberResult);
        }

        return result;
    }

Die Schnittstelle mit einer entsprechenden Implementierung sieht dann wie folgt aus:


public interface IFizzBuzz
{
    string GetForNumber(int number);
}
class SimpleFizzBuzzImpl : IFizzBuzz
{
    private int _baseNumber;
    private string _word;

    public SimpleFizzBuzzImpl(int baseNumber, string word)
    {
        _baseNumber = baseNumber;
        _word = word;
    }

    public string GetForNumber(int number)
    {
        if (number % _baseNumber == 0) return _word;
        return string.Empty;
    }
}

Lamba Expressions

Lamba Expressions finde ich für kleinen Aufgaben genial. Man kann Logik an eine Funktion übergeben, ohne dafür entsprechende Schnittstellen, Delegaten oder abstrakte Basisklassen zu definieren. Der besondere Charme liegt dabei in der Übergabe in Logik, die nur einmal benutzt wird, wie bei “where” in LINQ. Die Lamba-Version des FizzBuzzer hat auch eine “Add” Methode, nimmt aber einen Parameter vom Typ Func<int, string> statt des Interfaces an.


public class LambdaFizzBuzzer
{
    private List<Func<int, string>> buzzList = null;

    public LambdaFizzBuzzer()
    {
        buzzList = new List<Func<int, string>>();
    }

    public void Add(Func<int, string> aBuzz)
    {
        buzzList.Add(aBuzz);
    }

    public IEnumerable<string> FizzBuzz(int maxNumber)
    {
        for (int i = 1; i <= maxNumber; i++)
        {
            var currentRes = string.Empty;
            foreach (var item in buzzList)
                currentRes += item.Invoke(i);
            yield return string.IsNullOrEmpty(currentRes) ? i.ToString() : currentRes;
        }
    }
}

Die Verwendung des FizzBuzzer ist dann entsprechend mit Lambda-Expressions.


LambdaFizzBuzzer fb = new LambdaFizzBuzzer();
fb.Add(i => (i % 3 == 0) ? "Fizz" : string.Empty);
fb.Add(i => (i % 5 == 0) ? "Buzz" : string.Empty);

Fazit

Es gibt unterschiedliche Varianten, Code als Parameter an eine Methode zu übergeben. Interfaces und Lambda Expressions habe ich hier gezeigt. Delegaten und abstrakte Basisklasse sind hier zwar nicht vorgestellt, unterscheiden sich aber konzeptionell nicht großartig von Interfaces und Lambda Expressions. Der architekturelle Unterschied oder Frage ist, ob man den Code wiederverwenden möchte und die Algorithmen Teil der Architektur sind oder ob es “flüchtiger Code” wie Lambda Expressions sind.

In einem Kommentar einer meiner Blogeinträge kam die Frage auf, ob man nicht eine Extension Method für das ‘??’-Konstrukt machen kann. Da habe ich mir die Frage gestellt, ob das überhaupt geht. Denn letztlich würde ja eine Methode auf einem Null-Objekt aufgerufen. Hier ein bißchen Code um den Sachverhalt zu verdeutlichen.


    public class Program
    {
        static void Main(string[] args)
        {
            // NULL-String deklarieren
            string str = null;
            // Funktioniert NICHT, da es eine Klassenmethode ist
            var bar = str.Contains("elo");

            // Funktioniert, da es eine Extension Method ist
            var foo = str.IfIsNull("Hello");
            // Funktioniert auch! isFoo == "foo"
            var isFoo = ((string)null).IfIsNull("foo");
        }
    }

    internal static class StingExtension
    {
        public static string IfIsNull(this string hello, string alternateText)
        {
            if (hello == null) return alternate;
            return hello;
        }
    }

Aber warum funktioniert das? Weder erscheint eine Compiler Fehlermeldung, noch der ReSharper jammert und zur Laufzeit wird auch keine Exception geworfen. Der Grund liegt in der Übersetzung von Extension Methods durch den Compiler. Eine Extension Method erscheint zwar als Klassenmethode, wird aber durch den statischen Aufruf der Methode ersetzt. Und eine statischer Methodenaufruf ist für Null-Objekte möglich und die Prüfung innerhalb der Methode verhindert die Exception. Damit wirft der Aufruf einer Extension Method auf “Null”-Objekte keine NullReferenceException. Klingt komisch, ist aber so.

Wer kennt das nicht: “NullReferenceException”. Entweder prüft man im Code ständig auf NULL oder man riskiert unschöne Programmabstürze. Durch ständige “if (… == null) {} else {}” Blöcke wird der Code schnell unleserlich und damit schwierig zu verstehen. Hier sind ein paar Varianten von NULL-Prüfungen und anderen schlanken, aber effektiven Varianten, um den Code leserlicher zu gestalten.

Beispiel

Das folgende Beispiel zeigt eine klassische “if…then..else” Abfrage mit vielen Zeilen Code um lediglich den Standardtext “leer” zurück zu geben, falls die Eigenschaft “Name” des Objekt NULL ist, ansonsten die Eigenschaft “Name” des Objektes.

if (myObject.Name == null)
{
    return "leer";
}
else
{
    return myObject.Name;
}

Variante 1

if (myObject.Name == null)
    return "leer";
else
    return myObject.Name;

Variante 2

// entweder so ...
myObject.Name == null ? return "leer" : return myObject.Name;
// oder so ...
return myObject.Name == null ? "leer" : myObject.Name;

Variante 3

return myObject.Name ?? "leer";

Dieses mal ein etwas ausführlicher Blog zum Thema meines Vortrages bei bonn-to-code. Meine viel zu kurzen 20 Minuten hatten eine volle Agenda mit verschiedenen Varianten des Observer Pattern bis hin zu einem Message Dispatcher ;) . Log geht’s!

 

Events statt Attach/Detach

Das Observer Pattern nach GOF war den meisten (oder allen) Zuhörern bereits bekannt, also konnte direkt mit der ersten .NET Modifikation begonnen werden. Die Logik des Attach und Detach von Beobachtern lässt sich wunderbar durch Events und EventHandler ersetzen. Events implementieren genau diese Funktionalität: Eine Liste von Funktionen zu verwalten und letztlich diese aufzurufen. Durch diesen Umbau ist der Code deutlich schlanker geworden und der Verwaltungscode wurde aus dem Subjekt entfernt.

Statusänderungen im größeren Kontext

Das Observer Pattern bietet eine sehr gute Möglichkeit, Statusänderungen an betroffene Ansichten durchzureichen. Dabei wird das Hollywood Prinzip (“Don’t call us, we call you”) beachtet, nachdem das Subjekt die Beobachter informiert und nicht die Beobachter regenmäßig das Subjekt überprüfen. Ein Problem entsteht, wenn mehrere Subjekte zu unterschiedlichen Zeitpunkten angezeigt werden sollen. Ein gutes Beispiel ist eine Liste aus Subjekten mit verschiedenen Vorschau-Ansichten. Nun wird es schwierig, das neue Subjekt an die unbekannte Anzahl an Ansichten zu senden.

Die Lösung hierfür ist ein SubjectNotifier mit statischen Events zum Verteilen von Nachrichten an alle Beobachter. Jeder Beobachter kann mittels SubjectStateChanged eine auf Statusänderung und mittels SubjectSelected auf eine Subjektänderung reagieren. Über die Raise* Methoden können beliebige Ansichten die entsprechenden Event auslösen, falls sich der Status oder das Subjekt geändert hat.

Fazit

Diese Variante, Statusänderungen eines Subjekts über einen SubjectNotifier zu publizieren, hat mehrere Vorteile. Alle Subjekte (auch neue Instanzen) können über einen zentralen Dispatcher publiziert werden. Es erweitert das Hollywood Prinzip aus dem klassischen Observer Pattern insoweit, dass nicht nur der Beobachter mit DEM Subjekt verbunden ist, sondern das der Beobachter noch nicht einmal das Subjekt kennt sondern von irgendwelchen Subjekten benachrichtigt wird. Ein weiterer Vorteil ist, dass das Subjekt absolut keine Logik zum Verteilen und Verwalten von Statusänderungen enthält. Das Subjekt ist unabhängig von interner Verwaltungslogik die nur einzelne Applikationen betrifft und z.B. in Konsolenanwendung nicht benötigt wird.

… dieses war der erste Streich, doch der zweite folgt sogleich!

Powered by WordPress Web Design by SRS Solutions © 2010 Think First Development Design by SRS Solutions