2010-08-27 5 views
5

Zurück Geschichte:.NET Refactoring, DRY. duale Vererbung, Datenzugriff und Trennung von Bedenken

So habe ich auf ein Architekturproblem für die letzten paar Nächte auf einem Refactor stecken, mit dem ich gespielt habe. Nichts Wichtiges, aber es stört mich. Es ist tatsächlich eine Übung in DRY, und ein Versuch, es so extrem zu nehmen, wie die DAL-Architektur ist vollständig DRY. Es ist eine vollkommen philosophische/theoretische Übung.

Der Code basiert teilweise auf einem der Refactorings von @JohnMacIntyre, die ich vor kurzem davon überzeugt habe, unter http://whileicompile.wordpress.com/2010/08/24/my-clean-code-experience-no-1/ zu bloggen. Ich habe den Code leicht modifiziert, wie ich es tendiere, um den Code eine Ebene weiter zu bringen - normalerweise, um zu sehen, welche zusätzlichen Kilometer ich aus einem Konzept herausholen kann ... jedenfalls sind meine Gründe weitgehend irrelevant.

Teil meiner Datenzugriffsschicht auf die folgende Architektur zugrunde:

abstract public class AppCommandBase : IDisposable { } 

Dieses enthält grundlegende Dinge, wie die Schaffung eines Befehlsobjekt und Bereinigung nach dem AppCommand entsorgt. Alle meine Kommandobasisobjekte sind davon abgeleitet.

Dies enthält grundlegende Dinge, die alle Lese-Befehle betrifft - speziell in diesem Fall das Lesen von Daten aus Tabellen und Ansichten. Keine Bearbeitung, keine Aktualisierung, kein Speichern.

abstract public class ReadItemCommandBase<T, FilterT> : ReadCommandBase<T, T> { } 

Diese enthält einige weitere grundlegende allgemeine Sachen - wie Definition von Methoden, die als ein einzelnes Element aus einer Tabelle in der Datenbank, in der die Tabellennamen, Schlüsselfeldnamen und Feldlistennamen sind definiert lesen erforderlich sein wird, erforderlich abstrakte Eigenschaften (zum von der abgeleiteten Klasse definiert werden.

public class MyTableReadItemCommand : ReadItemCommandBase<MyTableClass, Int?> { } 

Dies enthält spezifische Eigenschaften, die meine Tabellennamen, um die Liste der Felder aus der Tabelle oder die Ansicht, der Name des Schlüsselfelds zu definieren, ein Verfahren zum parsen die Daten aus der IDataReader-Zeile in mein Geschäftsobjekt und eine Methode, die den gesamten Prozess initiiert.

Nun, ich habe auch diese Struktur für meine Readlist ...

abstract public ReadListCommandBase<T> : ReadCommandBase<T, IEnumerable<T>> { } 
public class MyTableReadListCommand : ReadListCommandBase<MyTableClass> { } 

Der Unterschied ist, dass die Liste Klassen Eigenschaften enthalten, die Generation gehören zur Liste (d PageStart, PageSize, Sort und gibt eine IEnumerable) vs. Rückgabe eines einzelnen DataObject zurück (das erfordert nur einen Filter, der einen eindeutigen Datensatz identifiziert).

Problem:

Ich hasse, dass ich ein paar Eigenschaften in meiner MyTableReadListCommand Klasse habe, die in meiner MyTableReadItemCommand Klasse identisch sind. Ich habe darüber nachgedacht, sie in eine Hilfsklasse zu verschieben, aber während dies die Mitgliederinhalte an einer Stelle zentralisieren kann, habe ich immer noch identische Mitglieder in jeder der Klassen, die stattdessen auf die Hilfsklasse verweisen, die ich immer noch nicht mag.

Mein erster Gedanke war Dual-Vererbung würde dies gut lösen, auch wenn ich stimme, dass Dual-Vererbung ist in der Regel ein Code-Geruch - aber es würde dieses Problem sehr elegant lösen. Also, da .NET keine doppelte Vererbung unterstützt, wohin gehe ich von hier?

Vielleicht wäre ein anderer Refactor besser geeignet ...aber ich habe Probleme, meinen Kopf darum zu wickeln, wie ich dieses Problem umgehen kann.

Wenn jemand eine vollständige Code-Basis benötigt, um zu sehen, worüber ich rede, habe ich eine Prototyp-Lösung auf meiner DropBox unter http://dl.dropbox.com/u/3029830/Prototypes/Prototype%20-%20DAL%20Refactor.zip. Der betreffende Code befindet sich im DataAccessLayer-Projekt.

P.S. Dies ist nicht Teil eines laufenden aktiven Projekts, es ist eher ein Refaktor-Puzzle für meine eigene Unterhaltung.

Vielen Dank im Voraus Leute, ich schätze es.

+0

Als Referenz wird die Lösung, die Sie nicht mögen, als das Delegationsmuster bekannt: http://en.wikipedia.org/wiki/Delegation_pattern. –

+0

Auch: http://stackoverflow.com/questions/1224830/difference-between-strategy-pattern-and-delegation-pattern.Ich mag es, wie mson das Muster annimmt: "Delegierung bedeutet, dass ein einzelnes Objekt nicht für alles zuständig ist, sondern Verantwortlichkeiten auf andere Objekte delegiert. Der Grund dafür ist, dass es zwei grundlegendere Prinzipien der Softwareentwicklung durchsetzt durch Verringerung der Kopplung und Erhöhung der Kohäsion. " –

+0

Ich bin mir nicht sicher, ob es besser ist, dieses Design zu verwerfen und ein völlig neues zu entwickeln, oder es umzuformen, um etwas Sinnvolles zu finden. Die zunehmende Verwendung von Generika als Typparameter innerhalb einer generischen Definition ist ein sicheres Zeichen, dass Sie eine sehr feine Grenze zwischen Klarheit und Wahnsinn ziehen. Ich bin versucht zu sagen "benutze NHibernate ..." oh warte da, ich sagte es ... –

Antwort

4

Trennen Sie die Ergebnisverarbeitung vom Datenabruf. Ihre Vererbungshierarchie ist bei ReadCommandBase bereits mehr als tief genug.

Definieren Sie eine Schnittstelle IDatabaseResultParser. Implementieren Sie ItemDatabaseResultParser und ListDatabaseResultParser, beide mit einem Konstruktorparameter vom Typ ReadCommandBase (und vielleicht auch in eine Schnittstelle konvertieren).

Wenn Sie anrufen IDatabaseResultParser.Value() es führt den Befehl aus, analysiert die Ergebnisse und gibt ein Ergebnis vom Typ T.

Ihre Befehle konzentrieren sich auf die Daten aus der Datenbank abrufen und sie als Tupel von einer Beschreibung der Rückkehr (tatsächliche Tupel oder und Array von Arrays usw.), konzentriert sich Ihr Parser auf die Umwandlung der Tupel in Objekte des von Ihnen benötigten Typs. Sehen Sie NHibernates IResultTransformer für eine Idee, wie das funktionieren kann (und es ist wahrscheinlich auch ein besserer Name als IDatabaseResultParser).

Gunst Zusammensetzung über Vererbung.

an der Probe sah Nachdem ich werde noch weiter gehen ...

  1. Werfen AppCommandBase weg - es keinen Wert auf Ihre Vererbungshierarchie fügt hinzu, wie alle es tut, ist zu überprüfen, dass die Verbindung nicht null ist und offen und erstellt einen Befehl.
  2. Separate Abfrageerstellung von Abfrageausführung und Ergebnisanalyse - jetzt können Sie die Implementierung der Abfrageausführung erheblich vereinfachen, da es sich entweder um eine Leseoperation handelt, die eine Aufzählung von Tupeln zurückgibt, oder um eine Schreiboperation, die die Anzahl der betroffenen Zeilen zurückgibt.
  3. Ihr Abfrage-Generator könnte in einer Klasse zusammengefasst werden, um Paging/Sortierung/Filterung einzubeziehen. Es kann jedoch einfacher sein, eine Form begrenzter Struktur um diese herum zu erstellen, um Paging und Sortieren und Filtern zu trennen. Wenn ich dies tun würde, würde ich mich nicht darum kümmern, die Abfragen zu erstellen, ich würde einfach die SQL innerhalb eines Objekts schreiben, das mir erlaubte, einige Parameter (effektiv gespeicherte Prozeduren in C#) zu übergeben.

So, jetzt haben Sie IDatabaseQuery/IDatabaseCommand/IResultTransformer und fast keine Vererbung =)

+3

"Favorisieren Sie die Komposition über die Vererbung." <- Eine Lektion, die immer gelehrt und leicht vergessen wird, bis wir vergessen, dass sie uns im Hinterteil beißt. Gute Antwort. –

+0

Konzeptionell stimme ich nicht zu. Von einem hypothetischen Standpunkt aus gesehen, liegt der Sinn der Übung darin, zu sehen, wie weit das DRY-Prinzip gehen kann. Kann man zum Beispiel so extrem gehen, dass die Architektur * komplett * trocken ist? Oder gibt es einen limitierenden Faktor von OOP (zumindest in .NET), der verhindert, dass DRY zu diesem Extrem gebracht wird? – BenAlabaster

+0

Zu viel Wasser wird dich töten, genau wie zu wenig Wille. =) – Neal

0

Wie von Kirk erwähnt, ist dies das Delegationsmuster. Wenn ich dies tue, konstruiere ich normalerweise eine Schnittstelle, die vom Delegator und der delegierten Klasse implementiert wird. Dies reduziert den wahrgenommenen Code-Geruch, zumindest für mich.

+0

Ich habe diese Muster schon ausgecheckt, und ich muss ehrlich sein, nur weil sie "Muster" sind, nicht zufriedenstellen mich. Ob es ein wahrgenommener Code-Geruch ist oder nicht, ich kann mich einfach nicht dazu bringen, sie zu mögen. Also bin ich auf der Suche nach einer besseren Lösung. Danke, ich schätze die Informationen. – BenAlabaster

0

Ich denke, die Antwort ist ganz einfach ... Da .NET nicht Multiple inheritence nicht unterstützt, gibt es immer sein wird einige Wiederholungen beim Erstellen von Objekten ähnlichen Typs. .NET bietet Ihnen einfach nicht die Tools, um einige Klassen auf eine Weise wiederzuverwenden, die perfektes DRY ermöglicht.

Die nicht so einfache Antwort ist, dass Sie Codegenerierungstools, Instrumentierung, Code Dom und andere Techniken verwenden können, um die gewünschten Objekte in die gewünschten Klassen zu injizieren.Es schafft noch Vervielfältigung im Speicher, aber es würde den Quellcode (auf Kosten der zusätzlichen Komplexität in Ihrem Code Injection-Framework) vereinfachen.

Dies mag unbefriedigend erscheinen wie die anderen Lösungen, aber wenn Sie darüber nachdenken, tun dies Sprachen, die MI unterstützen, hinter den Kulissen, indem sie Delegierungssysteme anschließen, die Sie im Quellcode nicht sehen können.

Die Frage kommt auf, wie viel Aufwand sind Sie bereit, in setzen Sie Ihren Quellcode einfach zu machen. Denken Sie darüber nach, es ist ziemlich tiefgründig.

+0

Vererbung sollte ein letzter Ausweg sein. Mehrfachvererbung sollte der letzte Ausweg sein, wenn Sie wirklich wirklich wissen, was Sie tun (und selbst dann sollte es wahrscheinlich vermieden werden). MI sagt effektiv, dass c ein a und ein b ist, was wahrscheinlich bedeutet, dass c viel zu viel Verantwortung hat. – Neal

+0

@Neal - Da MI in C# nicht einmal eine Option ist, ist das nicht ein strittiger Punkt? – BenAlabaster

+0

@ Neal - Ich bin mir nicht sicher, was Ihr Punkt ist. Dies ist eine theoretische Übung, keine praktische. Ich stimme zu, dass MI in der Praxis problematisch ist. Aber es ist ein nützliches Werkzeug, um DRY zu erreichen. –

1

Ich denke, die kurze Antwort ist, dass in einem System, in dem die Mehrfachvererbung „für Ihren Schutz“ verboten wurde, Strategie/Delegation ist der direkte Ersatz. Ja, Sie immer noch mit einigen parallelen Struktur, wie zum Beispiel die Eigenschaft für das Delegatobjekt enden. Aber es ist so viel wie möglich innerhalb der Grenzen der Sprache minimiert.

Aber läßt von der einfachen Antwort Schritt zurück und einen weiten Blick nimmt ....

Eine weitere große Alternative ist die größere Designstruktur zu umgestalten, so dass Sie diese Situation vermeiden, in denen von Natur aus einer bestimmten Klasse des Verbundes besteht Verhalten von mehreren "Geschwister" - oder "Cousin" -Klassen darüber im Vererbungsbaum. Um es genauer auszudrücken, Refaktor zu einer Vererbung Kette eher als eine Vererbung Baum. Das ist leichter gesagt als getan. Es erfordert normalerweise, sehr unterschiedliche Funktionen zu abstrahieren.

Die Herausforderung, die Sie haben, wenn Sie diesen Kurs nehmen, den ich empfehle, ist, dass Sie bereits eine Konzession in Ihrem Design gemacht haben: Sie optimieren für verschiedene SQL in den Fällen "Artikel" und "Liste". Das zu erhalten, so wie es ist, wird dir in die Quere kommen, egal was passiert, weil du ihnen die gleiche Rechnung gegeben hast, also müssen sie zwangsläufig Geschwister sein. Ich würde also sagen, dass Ihr erster Schritt, um aus diesem "lokalen Maximum" der Design-Eleganz herauszukommen, darin besteht, diese Optimierung rückgängig zu machen und den einzelnen Gegenstand als das zu behandeln, was er wirklich ist: ein spezieller Fall einer Liste mit nur einem Element. Sie können immer wieder versuchen, eine Optimierung für einzelne Elemente wieder einzuführen. Aber warten Sie, bis Sie das Thema Eleganz angesprochen haben, das Sie gerade nervt.

Aber man muss anerkennen, dass jede Optimierung für etwas anderes als die Eleganz Ihres C# -Code eine Straßensperre in der Art von Design Eleganz für den C# -Code stellen wird. Dieser Kompromiß, genau wie das "Memory-Space" -Konjugat des Algorithmusentwurfs, ist grundlegend für die Art der Programmierung.

+0

Danke Chris, das ist sehr aufschlussreich. Ich hatte schon über diesen Ansatz nachgedacht, und Sie haben Recht, es erfordert eine mehr "Set-basierte" Taktik. Ich habe diesen Ansatz noch nicht verfolgt, aber ich denke, dass dieser Ansatz als nächstes untersucht werden könnte, um zu sehen, wie weit ich das erreichen kann. – BenAlabaster

0

Ich habe Ihr Szenario nicht gründlich untersucht, aber ich habe einige Probleme mit dem Doppelhierarchieproblem in C#. Um Code in einer Doppelhierarchie zu teilen, benötigen wir ein anderes Konstrukt in der Sprache: entweder ein Mixin, eine trait (pdf) (C# research -pdf) oder eine Rolle (wie in perl 6). C# macht es sehr einfach, Code mit Vererbung zu teilen (was nicht der richtige Operator für Wiederverwendung von Code ist), und sehr mühsam, um Code über Komposition zu teilen (Sie müssen den gesamten Delegationscode von Hand schreiben).

Es gibt Möglichkeiten, eine kind of mixin in C# zu bekommen, aber es ist nicht ideal.

Die Oxygene (download) Sprache (ein Object Pascal für .NET) hat auch eine interessante Funktion für interface delegation, die verwendet werden kann, um alle diese Delegierung Code für Sie zu erstellen.

Verwandte Themen