Thomas Kramer

IT-COW | April 2010

Webcast "Codequalität" (Teil 3) vom MSDN

By Administrator at April 30, 2010 16:33
Filed Under: .net-Framework, C#, Programmierung allgemein, Webcast-Reviews

Das dritte Video (Dauer 01:17) der Webcast-Reihe "Codequalität" (Autor: Golo Roden) ist der aktuelle Teil, denn das vierte (und damit letzte) Video ist noch nicht erschienen. Als Abspielprogramm favorisiere ich für Webcasts zur Zeit übrigens IrfanView, weil das schnell geladen ist und man besser zu einer bestimmten Position springen kann als mit anderen Programmen. Mein Review zum zweiten Teil ist hier zu lesen, folgend nun der Inhalt des vorletzten Videos dieser Reihe.

 

Eingeführt wird der Webcast natürlich mit einer Selbstvorstellung des Autors, welche diesmal mit 4 Minuten schon etwas lang anmutet - positiv hervorzuheben ist aber, das für Rückfragen direkt die eMail-Adresse des Autors eingeblendet wird. Nach dieser Einführung - und einer Retrospektive zu den vorangegangenen Teilen dieser Reihe - beginnt das eigentliche Thema, "Codequalität: Die innere Form".

 

Als erstes handelt der Autor die Frage ab, wann Klassen und wann Strukturen (Structs) verwendet werden sollten. Grundlegend handelt es sich bei normalen Klassen um Referenztypen und bei Structs um Wertetypen - für eine genaue Definition und Unterscheidung siehe Galileo Openbook Visual C# 2008. Nun benötigen Klassen die Garbage Collection, Structs dagegen nicht weil sie im Methodenkontext stehen und direkt abgeräumt werden sobald diese verlassen wird - das macht den grundlegenden Performance-Unterschied aus. Allgemein sagt man das Referenztypen nun bei Methodenaufrufen als Referenz (Call by Reference), und Werteteypen dementsprechend als Wert (Call by Value) übergeben werden - in C# stimmt das jedoch nicht mehr, denn es wird grundsätzlich alles als Wert übergeben, es sei denn man schreibt das Schlüsselwort ref davor. An dieser Stelle sei als Ergänzung das out-Schlüsselwort genannt, bei denen die Variable im Gegensatz zu ref nicht erst initialisiert werden muss vor der Parameterübergabe an die Methode.

 

Wenn nun die Parameter eh als Kopie übergeben werden, könnte der Einsatz von Structs sinnvoll sein, im Regelfall will man jedoch die Übergabe als Referenz, sagt Herr Roden - auch wegen der Performance-Nachteile wenn erst eine Kopie bei der Value-Übergabe erstellt werden muss. Das ist mir jedoch zu allgemein gesprochen, simple string und integer-Variablen habe ich immer als Kopie übergeben, aber bei umfangreichen Arrays stimmt das natürlich. Die Value-Übergabe stellt immerhin auch eine gewisse Sicherheit dar, das man innerhalb einer Methode nicht versehentlich die Aufrufparameter verändert - etwa anhand eines Schreibfehlers der Variablen. Daher finde ich das Default-Handling in C# mit der Value-Übergabe (ohne ref-Schlüsselwort) schon sinnvoll.

 

Wenn ein Wertetyp dagegen in einem Object gespeichert werden soll (etwa durch casting), findet ein sogenanntes Boxing statt - der umgekehrte Vorgang um aus einem Object den Wert wieder herauszuholen nennt sich dementsprechend Unboxing. Der Einsatz von Structs lohnt sich unter Umständen daher nicht mehr, wenn intensiv Boxing stattfindet, man macht sich den Performance-Vorteil durch die wegfallende Garbage Collection wieder zunichte. Als Zusammenfassung rät Herr Roden von Structs ab und rät dazu, immer Klassen zu verwenden. Als einzigen legitimen Ausnahmefall sieht Herr Roden den Aufruf von unmanaged Code per P-Invoke an.

 

Wann sollten Schnittstellen (Interfaces) und wann abstrakte Klassen verwendet werden? Das ist auch eine relevante Frage in der Team-Entwicklung. Wenn Klassen eine gewisse gemeinsame Basis haben kann man diesen Code in eine neue abstrakte Basisklasse verlagern und anschließend von ihr ableiten, so das der Basis-Code nur einmal implementiert werden muss - das ist der konkrete Vorteil von abstrakten Klassen. Aber da C# nur Einfachvererbung unterstützt, verbaut man sich mit dem Ableiten von solchen abstrakten Klassen den Weg von anderen, weiteren Klassen abzuleiten - als Beispiel wo das konkret nachteilhaft sein kann, nennt Herr Roden explizit das Test Driven Development.

 

Interfaces enthalten im Gegensatz zu abstrakten Klassen dagegen nur den "Kontrakt" - die Methodensignaturen - und zeigen auf welche Methoden eine Klasse implementieren muss, wenn sie von diesem Interface ableitet. Als Beispiel könnte man das IEnumerable-Interface nennen, mit dem davon abgeleitete Klassen die Methode GetEnumerator() implementieren müssen - Vorteil dieses speziellen Interfaces liegt darin, das durch davon abgeleitete Klassen mit einer foreach-Schleife iteriert werden kann, so spart man sich eine spezielle Zähler-Variable. In älteren Programmiersprachen muss man für diesen Zweck das Iterator-Design Pattern von Hand implementieren. Gemäß Bennungskonventionen für das .net-Framework sollten Interfaces mit dem Präfix I beginnen.

 

Sobald Interfaces veröffentlicht wurden gelten sie gemeinhin als unabänderlich, weil bei einer Änderung alle davon abgeleiteten Klassen händisch angepasst werden müssten. Zumindest für spätere Ergänzungen geht man daher dazu über, einfach eine neue Schnittstelle zu erstellen die von der alten ableitet, und in dieser neuen dann die ergänzenden Methodensignaturen einzufügen - mit dieser Lösung können alter und neuer Quellcode vereint werden. Problematisch ist das Ableiten einer alten in eine neue Schnittstelle aber dort, wo als Rückgabetyp die alte Schnittstelle erwartet wird, dann muss explizit gecastet werden. Anmerkung von mir: bei normalen Klassen empfiehlt sich für einen sanften Übergang veraltete Methoden mit dem Obsolete-Attribut zu markieren - für Interfaces funktioniert das natürlich nicht, weil die Methoden trotzdem implementiert werden müssen. Zusammenfassend rät Herr Roden dazu, Interfaces und abstrakte Klassen parallel einzusetzen und so die Vorteile beider Elemente zu nutzen.

 

Als nächstes spricht Herr Roden das Thema statische Klassen an, welche als Ersatz für das Singleton-Pattern benutzt werden können. Weil von statischen Klassen keine Instanz erstellt werden kann, ist das Singleton-Pattern weitgehend bedeutungslos geworden - im Vergleich gibt es zumindest noch den Unterschied, das von statischen Klassen auch nicht abgeleitet werden kann. Herr Roden empfiehlt an dieser Stelle den Einsatz des Service Locator-Patterns oder eines Dependency Injection-Framework, wie er auch in seinem Artikel schreibt - damit muss ich mich später einmal beschäftigen.

 

Zum Thema Konstruktoren von Klassen empfiehlt Herr Roden einen expliziten Standard-Konstruktor für jede Klasse zu erstellen, und den impliziten, automatisch erstellten Standard-Konstruktor gegebenenfalls mit dem Obsolete-Attribut zu markieren wenn er bereits vorhanden ist. An dieser Stelle empfiehlt er über den etwaigen Einsatz von Lightcore nachzudenken, wie er auch in seinem Blog geschrieben hat - an dieser Stelle kann ich noch nicht mithalten, damit werde ich mich zukünftig auseinandersetzen. Weitergehend sollte die Initialisierungslogik bei überladenen Konstruktoren in den größten parameterbehafteten Konstruktor verlagert werden und nicht in den parameterlosen Konstruktor - weil das dazu führt das die Initialisierungslogik als ganzes über mehrere Konstruktoren verteilt wird. Ich persönlich habe bisher die grundlegende Logik in den parameterlosen Konstruktor abgelegt, eventuell sollte ich das noch überdenken.

 

Abgeraten wird auch davon aus Konstruktoren virtuelle Methoden aufzurufen, weil das dazu führen kann das sozusagen die "tiefste Implementierung" - also die überschriebene Methode - aufgerufen wird bevor der Konstruktor dieser abgeleiteten Klasse abgearbeitet wurde. Das liegt daran das die Konstruktoren der übergeordneten Klassen (von welchen man abgeleitet hat) vor dem Konstruktor der eigenen Klasse aufgerufen werden. In Konstruktoren Exceptions zu werfen ist grundsätzlich legitim, was aber nicht für Konstruktoren statischer Klassen gilt - der Begriff Typinitialisierer wird synonym dazu verwandt, wie Herr Roden auch in seinem Blog weiter ausführt. Ein Typinitialisierer wird nur einmal ausgeführt und wenn dort eine Exception geworfen wird, ist dieser Typ für die ganze Anwendungslaufzeit nicht mehr zu benutzen.

 

In C# 4 gibt es als Neuerung Default-Parameter um mit überladenen Methoden zu sparen, Herr Roden rät jedoch persönlich davon ab. Diese Quelle stellt beides gegenüber, nimmt aber eine neutralere Haltung ein. Default-Parameter wurden in erster Linie eingeführt, um die Interoperabilität mit COM zu verbessern, um nicht endlose Male null angeben zu müssen. Bei mehreren überladenen Methoden verlagere ich übrigens den gemeinsamen Basis-Code wenn möglich in eine separate (interne) Methode, die von beiden überladenen externen Methoden aufgerufen wird - bei überladenen Methoden sollte eben Redundanz vermieden werden, nachträglich ist das aber nicht immer möglich ohne den Code nochmal komplett testen zu müssen. Herr Roden rät außerdem dazu, bei überladenen Methoden auf die immer gleiche Parameterreihenfolge zu achten.

 

Eigenschaften/Properties vs. Methoden: ein Getter mit Zugriffsmodifizierer private, demgegenüber aber ein Setter mit Zugriffsmodifizierer public sollte vermieden werden. Wenn nur ein Setter benötigt wird, sollte dieser über eine eigene Methode implementiert werden. An dieser Stelle empfiehlt Herr Roden einmal den Blick auf das INotifyPropertyChanged-Interface. Allgemein sollte immer eine Methode anstatt einer Property verwendet werden, wenn deutlich mehr als das Schreiben und Lesen eines Wertes passiert.

 

Im Team sollte außerdem festgelegt werden wann versiegelte Klassen verwendet werden, also solche die mit dem Zugriffsmodifizierer sealed gekennzeichnet sind. Statische Klassen als solche sind implizit als sealed markiert. Desweiteren empfiehlt der Autor das Benutzen des Template-Entwurfsmusters. Der virtual-Zugriffsmodifizierer sollte generell mit Bedacht eingesetzt werden, auch weil zuerst ermittelt werden muss welches Objekt in der Hierarchie am weitesten unten steht, es somit Performance-Nachteile mit sich bringt.

 

Es sollten keine Fehlercodes anstatt Exceptions verwendet werden. Verbreitet ist dennoch der Einsatz von Magic Numbers, diese habe ich auch schon verwendet nur der Begriff als solcher war mir noch nicht bekannt. Exceptions kosten heute nicht mehr viel Performance, innerhalb einer Schleife erzeugt kann das aber doch durchaus der Fall sein. Ganz verbietet es sich, Exceptions für Logik einzusetzen, dem stimme ich zu - auch sollten Exceptions nicht verschluckt und auf jeden Fall gesondert abgehandelt werden. Für eigene Exceptions sollte immer von Exception und nicht mehr von ApplicationException abgeleitet werden, außerdem sollten Exceptions namentlich immer auf den Suffix -Exception enden. An dieser Stelle will ich nochmal an die Pascal-Case-Benennungskonvention erinnern, die im letzten Teil besprochen wurde.

 

Herr Roden bietet über seine eMail-Adresse webmaster [at] goloroden.de auch an, einen professionellen Codereview durchzuführen. Aufgefallen ist mir auch das dieser Autor sich intensiv im myCSharp-Forum engagiert. Die Beiträge in seinem Blog muss ich mir noch genauer durchlesen.

 

Für weitere Neuerungen zu C# 4 und Visual Studio 2010 ist dieser Link sicher auch empfehlenswert, muss ich später noch genauer durchsehen.

 

Update 16.05.2010: Wie ich gesehen habe wurde die Webseite guidetocsharp.de wohl auch von Herrn Roden initiiert.

 

Mein Review zum vierten Teil der Reihe ist mittlerweile hier einsehbar.

 

Monats-Liste