Bei der Planung des letzten Kundenwebs stellte sich mal wieder die Aufgabe eine Navigation zu erstellen, die auf der Datenbank unseres eigenen CMS ConLIVE! basiert. Jedes Web hat dabei eine eigene Datenbank, die jedoch auf einer größtenteils einheitlichen Tabellenstruktur basiert. Vor zwei Jahren entwickelten wir alle Webs noch mit classic ASP und es existierte hierfür eine selbstgeschriebene Library mit Standartfunktionen, u.a. um besagte Navigation zu generieren. Inzwischen sind auch wir auf .NET-Programmierung umgestiegen, allerdings musste deshalb zunächst auch für jedes Web individuell das Auslesen und Zusammenstellen aller Daten erfolgen da die bisherige Library unbrauchbar geworden war. Eine eher “schwüle” Situation, von DRY also keine Spur. Eine neue Library musste her!
In classic ASP hatten wir in jedem Web die jeweilige Datenbank in der Variablen “db” initialisiert. Da die Tabellennamen und deren Struktur gleich sind konnten wir in der Library mit einfachen SQL-Anweisungen die Daten abrufen. Natürlich könnte man es ähnlich auch jetzt in .NET machen, andererseits bieten O/R-Mapper wie Linq2Sql nicht zu Unterschätzende Vorteile, nicht zuletzt Intellisense, Typsicherheit und Syntaxkontrolle bereits zum Zeitpunkt des Kompilierens. Doch auch wenn die für unsere Anwendungszwecke benötigten Tabellennamen und -spalten unter den Webs identisch sind so werden doch immer mal individuelle Anpassungen vorgenommen. Im Falle von Linq2Sql bedeutet das, dass jedes Web seine eigene DBML-Datei erhält und wir keine globale für alle Webs verwenden. Doch wie dann eine global verwendbare Bibliothek erstellen? Die Antwort waren für uns die oft unterschätzten Interfaces kombiniert mit Typed Parametern.
Zunächst einmal ist es notwendig ein Interface der Klasse zu erstellen, welche bei Linq2Sql im DataContext die jeweilige Tabelle repräsentiert. Eine solche Schnittstelle enthält dabei lediglich die Signaturen von u.a. den Methoden ohne die jeweilige Implementation. Mit den so definierten Eigenschaften kann man dann arbeiten ohne die eigentliche Klasse zu kennen: Die Tatsache, dass sie das Interface implementiert garantiert das Vorhandensein der definierten Methoden und Ereignisse. Um ein solches Interface zu erstellen setzen wir den Cursor innerhalb der gewünschten Klasse und drücken die Tastenkombination STRG+R STRG+I (englisches Visual Studio 2010) oder wählen alternativ im Kontextmenü “Refactor” – “Extract Interface…”. Im folgenden Dialog (siehe Bild) können die in das Interface zu übernehmenden Methoden etc. ausgewählt werden. Nach einem Klick auf “OK” wird Schnittstelle automatisch erstellt und der Klasse zugewiesen. Letzteres ist jedoch problematisch, da die vom Linq2Sql-Designer generierte CS-Datei jederzeit wieder neu erstellt werden könnte. Unsere Klasse würde dann das gerade erstellte Interface nicht mehr implementieren. Da alle Klassen jedoch automatisch als “partial” erstellt werden können wir unsere Interface-Implementierung einfach in eine neue Datei auslagern und die Änderungen der automatisch erstellten Klasse wieder verwerfen:
public partial class Dept : IDept { }
Ausser den Klassen, die den benötigten Tabellen entsprechen benötigen wir ausserdem, auch noch den DataContext den Linq2Sql angelegt hat. Auch hier erstellen wir uns wieder ein Interface, welches die Klassen beinhaltet die wir im vorherigen Schritt ebenfalls mit einer Schnittstelle implementiert haben:
public interface IDataContext {
System.Data.Linq.Table<Dept> Depts { get; }
}
Wenn wir die jeweiligen Interfaces in ein neues Projekt verschieben, welches unsere künftige Library repräsentiert, fällt schnell ein Problem auf: Hier ist die im Interface verwendete Klasse “Dept” unbekannt. Man könnte jetzt auf die Idee kommen hier einfach statt Dept das eben erstellte Interface IDept einzusetzen. Doch wenn wir dann in unserem Web dem Linq2Sql DataContext unser Interface zuweisen wie schon der partiellen Dept-Klasse zuvor bekommen wir eine Fehlermeldung: Der DataContext implementiert unser Interface nicht da dort “Depts” vom Typ “Table<Dept>” ist und eben nicht “Table<IDept>” wie von der Schnittstelle gefordert. Wir müssen also auch in IDataContext “Table<Dept>” verwenden.
Die Lösung ist ein typisierter Parameter für das Interface zu verwenden:
public interface IDataContext<Dept>
where Dept : class, IDept {
System.Data.Linq.Table<Dept> Depts { get; }
}
Über die where-Bedingung geben wir an, dass Dept eine Klasse sein muss welche außerdem das Interface IDept implementiert. Jetzt können wir innerhalb unserer Library sowohl mit dem DataContext als auch mit der Dept-Tabelle über die Interfaces arbeiten:
public class ClDept<TDept>
where TDept : class, IDept {
private IDataContext<TDept> _db;
public ClDept(IDataContext<TDept> db) {
_db = db;
}
public string GetUrlFromDept(int idDept) {
var result = new StringBuilder();
var dept = _db.Depts.SingleOrDefault(d => !d.Flg_del && d.Id_dept.Equals(idDept));
if (dept.Equals(null))
throw new ArgumentOutOfRangeException("idDept", "There is no dept entry with this id.");
if (dept.Id_dept_parent.HasValue)
result.Append(GetUrlFromDept((int)dept.Id_dept_parent));
result.Append("/" + dept.Dept_name.CleanUrl());
return result.ToString();
}
}
Innerhalb der Webseite können wir auf unsere ausgelagerte Funktion nun ebenfalls recht einfach zugreifen nachdem wir die DLL referenziert haben. Zunächst müssen wir oben beschrieben die partiellen Klassen mit den Interfaces erweitert werden. Auf die Methode “GetUrlFromDept(int)” können wir ebenso einfach zugreifen:
var db = new Dms_meinweb(); //implementiert IDataContext<Dept>
var navHelper = new ClDept<Dept>(db);
string url = navHelper.GetUrlFromDept(25); // gibt bsp. “/Aktuelles/Veranstaltungen/Mainz” zurück
Fazit: Schreibt man für eine im grunde identische Funktionalität den Quellcode mehrfach (
von der Copy-Pasta ganz zu schweigen) macht man etwas verkehrt. Die
CCD sprechen von der Verletzung des
DRY-Prinzips, doch egal wie man es nennt: Es ist fehleranfällig, mühsam bei einer Änderung überall anzupassen, unübersichtlich und schlußendlich genau so unangenehm wie schwül-warmes Sommerwetter!
Share or Bookmark this Article: