SOLID-Prinzipien anwenden

13

Ich bin ziemlich neu in den SOLID- Designprinzipien. Ich verstehe ihre Gründe und Vorteile, aber ich kann sie nicht auf ein kleineres Projekt anwenden, das ich als praktische Übung zur Anwendung der SOLID-Prinzipien umgestalten möchte. Ich weiß, dass es nicht notwendig ist, eine perfekt funktionierende Anwendung zu ändern, aber ich möchte sie trotzdem überarbeiten, damit ich Designerfahrung für zukünftige Projekte sammeln kann.

Die Anwendung hat die folgende Aufgabe (eigentlich viel mehr als das, aber lassen Sie es uns einfach halten): Sie muss eine XML-Datei lesen, die Definitionen für Datenbanktabellen / Spalten / Ansichten usw. enthält, und eine SQL-Datei erstellen, die zum Erstellen verwendet werden kann ein ORACLE-Datenbankschema.

(Hinweis: Bitte diskutieren Sie nicht, warum ich es brauche oder warum ich kein XSLT verwende. Es gibt Gründe, aber sie sind nicht thematisch.)

Zunächst habe ich mich nur mit Tabellen und Einschränkungen befasst. Wenn Sie Spalten ignorieren, können Sie dies folgendermaßen angeben:

Eine Einschränkung ist Teil einer Tabelle (oder genauer Teil einer CREATE TABLE-Anweisung), und eine Einschränkung kann auch auf eine andere Tabelle verweisen.

Zuerst erkläre ich, wie die Anwendung im Moment aussieht (ohne SOLID):

Derzeit verfügt die Anwendung über eine "Table" -Klasse, die eine Liste von Zeigern auf Einschränkungen der Tabelle sowie eine Liste von Zeigern auf Einschränkungen enthält, die auf diese Tabelle verweisen. Immer wenn eine Verbindung hergestellt wird, wird auch die Rückwärtsverbindung hergestellt. Die Tabelle verfügt über eine createStatement () -Methode, die wiederum die createStatement () -Funktion jeder Einschränkung aufruft. Diese Methode verwendet selbst die Verbindungen zur Eigentümertabelle und zur referenzierten Tabelle, um deren Namen abzurufen.

Dies gilt natürlich überhaupt nicht für SOLID. Beispielsweise gibt es zirkuläre Abhängigkeiten, die den Code in Bezug auf erforderliche Methoden zum Hinzufügen / Entfernen und einige Destruktoren für große Objekte aufgebläht haben.

Es gibt also ein paar Fragen:

  1. Sollte ich die zirkulären Abhängigkeiten mithilfe der Abhängigkeitsinjektion auflösen? In diesem Fall sollte die Einschränkung die Tabelle owner (und optional die Tabelle, auf die verwiesen wird) in ihrem Konstruktor erhalten. Aber wie könnte ich dann die Liste der Einschränkungen für eine einzelne Tabelle durchgehen?
  2. Wenn in der Table-Klasse sowohl der Status von sich selbst (z. B. Tabellenname, Tabellenkommentar usw.) als auch die Links zu Einschränkungen gespeichert sind, handelt es sich dabei um eine oder zwei "Verantwortlichkeiten", die sich auf das Prinzip der Einzelverantwortung beziehen?
  3. Falls 2. richtig ist, sollte ich nur eine neue Klasse in der logischen Business-Schicht erstellen, die die Links verwaltet? Wenn ja, wäre 1. offensichtlich nicht mehr relevant.
  4. Sollten die "createStatement" -Methoden Teil der Table / Constraint-Klassen sein oder sollte ich sie auch verschieben? Wenn ja, wohin? Eine Manager-Klasse pro Datenspeicherklasse (dh Tabelle, Einschränkung, ...)? Oder lieber eine Managerklasse pro Link erstellen (ähnlich 3.)?

Immer wenn ich versuche, eine dieser Fragen zu beantworten, renne ich irgendwo im Kreis.

Das Problem wird natürlich viel komplexer, wenn Sie Spalten, Indizes usw. einbeziehen. Wenn Sie mir jedoch mit der einfachen Sache "Tabelle / Einschränkung" helfen, kann ich den Rest möglicherweise selbst herausfinden.

Tim Meyer
quelle
3
Welche Sprache benutzt du? Könntest du wenigstens ein paar Skelettcodes posten? Es ist sehr schwierig, die Codequalität und mögliche Umgestaltungen zu diskutieren, ohne den tatsächlichen Code zu sehen.
Péter Török
Ich benutze C ++, aber ich habe versucht, es aus der Diskussion herauszuhalten, da Sie dieses Problem in jeder Sprache haben könnten
Tim Meyer
Ja, aber die Anwendung von Mustern und Refactorings ist sprachabhängig. ZB schlug @ back2dos in seiner Antwort unten AOP vor, was offensichtlich nicht für C ++ gilt.
Péter Török,
Weitere Informationen zu SOLID-Prinzipien finden Sie unter programmers.stackexchange.com/questions/155852/…
LCJ

Antworten:

8

Sie können von einem anderen Standpunkt aus beginnen, um hier das "Prinzip der einheitlichen Verantwortung" anzuwenden. Was Sie uns gezeigt haben, ist (mehr oder weniger) nur das Datenmodell Ihrer Anwendung. SRP bedeutet hier: Stellen Sie sicher, dass Ihr Datenmodell nur für die Aufbewahrung von Daten verantwortlich ist - nicht weniger, nicht mehr.

Also , wenn Sie Ihre XML - Datei zu lesen , gehen, ein Datenmodell daraus erstellen und schreiben SQL, was sollte man nicht tun , ist alles in Ihre implementieren TableKlasse , die XML oder SQL spezifisch ist. Sie möchten, dass Ihr Datenfluss wie folgt aussieht:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Der einzige Ort, an dem XML-spezifischer Code platziert werden sollte, ist beispielsweise eine Klasse mit dem Namen Read_XML. Der einzige Ort für SQL-spezifischen Code sollte eine Klasse wie sein Write_SQL. Natürlich werden Sie diese beiden Aufgaben möglicherweise in weitere Unteraufgaben aufteilen (und Ihre Klassen in mehrere Manager-Klassen aufteilen), aber Ihr "Datenmodell" sollte von dieser Ebene keine Verantwortung übernehmen. Fügen Sie daher createStatementkeiner Ihrer Datenmodellklassen ein hinzu, da dies Ihrem Datenmodell die Verantwortung für SQL überträgt.

Ich sehe kein Problem, wenn Sie beschreiben, dass eine Tabelle dafür verantwortlich ist, alle ihre Teile aufzunehmen (Name, Spalten, Kommentare, Einschränkungen ...), das ist die Idee hinter einem Datenmodell. Sie haben jedoch beschrieben, dass "Tabelle" auch für die Speicherverwaltung einiger seiner Teile verantwortlich ist. Dies ist ein C ++ - spezifisches Problem, mit dem Sie in Sprachen wie Java oder C # nicht so leicht konfrontiert werden. Die C ++ - Methode, diese Verantwortung loszuwerden, besteht darin, intelligente Zeiger zu verwenden und den Besitz an eine andere Ebene zu delegieren (z. B. die Boost-Bibliothek oder Ihre eigene "intelligente" Zeigerebene). Aber Vorsicht, Ihre zyklischen Abhängigkeiten können einige Smart Pointer-Implementierungen "irritieren".

Noch etwas zu SOLID: Hier ist ein schöner Artikel

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

SOLID anhand eines kleinen Beispiels erklären. Lassen Sie uns versuchen, das auf Ihren Fall anzuwenden:

  • Sie benötigen nicht nur Klassen Read_XMLund Write_SQL, sondern auch eine dritte Klasse, die die Interaktion dieser beiden Klassen verwaltet. Nennen wir es a ConversionManager.

  • DI Prinzip Anwendung könnte bedeuten , hier: ConversionManager sollte nicht Instanzen erstellen Read_XMLund Write_SQLvon selbst aus . Stattdessen können diese Objekte über den Konstruktor injiziert werden. Und der Konstruktor sollte eine solche Signatur haben

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

wo IDataModelReaderist eine Schnittstelle, von der Read_XMLerbt, und IDataModelWriterdas gleiche für Write_SQL. Dadurch können ConversionManagerErweiterungen geöffnet werden (Sie können ganz einfach verschiedene Leser oder Autoren angeben), ohne dies ändern zu müssen. Wir haben also ein Beispiel für das Open / Closed-Prinzip. Überlegen Sie, was Sie ändern müssen, wenn Sie einen anderen Datenbankanbieter unterstützen möchten - idealerweise müssen Sie nichts in Ihrem Datenmodell ändern, sondern stellen stattdessen einen anderen SQL-Writer bereit.

Doc Brown
quelle
Während dies eine sehr vernünftige Übung von SOLID ist, beachten Sie, dass es gegen die "alte Schule Kay / Holub OOP" verstößt, indem Getter und Setter für ein ziemlich anämisches Datenmodell erforderlich sind. Es erinnert mich auch an den berüchtigten Rant von Steve Yegge .
user949300
2

Nun, Sie sollten in diesem Fall das S von SOLID anwenden.

Eine Tabelle enthält alle darin definierten Einschränkungen. Eine Einschränkung enthält alle Tabellen, auf die sie verweist. Einfaches und einfaches Modell.

Was Sie dabei beachten, ist die Fähigkeit, inverse Lookups durchzuführen, dh herauszufinden, auf welche Einschränkungen in einer Tabelle verwiesen wird.
Also, was Sie eigentlich wollen, ist ein Indexdienst. Das ist eine völlig andere Aufgabe und sollte daher von einem anderen Objekt ausgeführt werden.

Um es auf eine sehr vereinfachte Version herunterzubrechen:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Für die Implementierung des Index gibt es drei Möglichkeiten:

  • Die getContraintsReferencingMethode könnte wirklich nur das Ganze Databasefür TableInstanzen crawlen und deren Constraints crawlen , um das Ergebnis zu erhalten. Abhängig davon, wie teuer dies ist und wie oft Sie es benötigen, kann es eine Option sein.
  • Es könnte auch einen Cache verwenden. Wenn Ihr Datenbankmodell können einmal definiert ändern, können Sie den Cache halten durch Signale von der jeweiligen Brennen Tableund ConstraintInstanzen, wenn sie sich ändern. Eine etwas einfachere Lösung wäre, Indexeinen "Snapshot-Index" des Ganzen erstellen zu lassen Database, mit dem Sie arbeiten und den Sie dann verwerfen würden. Dies ist natürlich nur möglich, wenn Ihre Anwendung zwischen "Modellierungszeit" und "Abfragezeit" stark unterscheidet. Wenn es eher wahrscheinlich ist, dass beide gleichzeitig ausgeführt werden, ist dies nicht durchführbar.
  • Eine andere Möglichkeit wäre, mit AOP die gesamten Erstellungsaufrufe abzufangen und den Index entsprechend zu pflegen.
back2dos
quelle
Sehr ausführliche Antwort, mir gefällt Ihre Lösung bisher! Was würden Sie denken, wenn ich DI für die Table-Klasse ausführen und ihr während der Erstellung eine Liste von Einschränkungen geben würde? Ich habe sowieso eine TableParser-Klasse, die in diesem Fall als Factory fungieren oder mit einer Factory zusammenarbeiten kann.
Tim Meyer
@ Tim Meyer: DI ist nicht unbedingt Konstruktor-Injection. DI kann auch von Elementfunktionen durchgeführt werden. Ob die Tabelle alle ihre Teile über den Konstruktor erhalten soll, hängt davon ab, ob diese Teile erst zur Konstruktionszeit hinzugefügt und später nicht geändert werden sollen oder ob Sie schrittweise eine Tabelle erstellen möchten. Das sollte die Grundlage Ihrer Entwurfsentscheidung sein.
Doc Brown
1

Die Heilung für zirkuläre Abhängigkeiten ist das Gelübde, dass Sie sie niemals erschaffen werden. Ich finde, dass Codierungstest zuerst eine starke Abschreckung darstellt.

Auf jeden Fall können zirkuläre Abhängigkeiten immer durch Einführung einer abstrakten Basisklasse aufgehoben werden. Dies ist typisch für Graphendarstellungen. Hier sind die Tabellen Knoten und die Fremdschlüsseleinschränkungen Kanten. Erstellen Sie daher eine abstrakte Table-Klasse, eine abstrakte Constraint-Klasse und möglicherweise eine abstrakte Column-Klasse. Dann können alle Implementierungen von den abstrakten Klassen abhängen. Dies ist möglicherweise nicht die bestmögliche Darstellung, stellt jedoch eine Verbesserung gegenüber miteinander gekoppelten Klassen dar.

Wie Sie jedoch vermuten, erfordert die beste Lösung für dieses Problem möglicherweise keine Nachverfolgung der Objektbeziehungen. Wenn Sie nur XML in SQL übersetzen möchten, benötigen Sie keine speicherinterne Darstellung des Abhängigkeitsgraphen. Das Constraint-Diagramm wäre schön, wenn Sie Diagrammalgorithmen ausführen möchten, aber Sie haben das nicht erwähnt, daher gehe ich davon aus, dass dies keine Voraussetzung ist. Sie benötigen lediglich eine Liste mit Tabellen und Einschränkungen sowie einen Besucher für jeden SQL-Dialekt, den Sie unterstützen möchten. Generieren Sie die Tabellen und generieren Sie dann die Einschränkungen außerhalb der Tabellen. Bis sich die Anforderungen geändert haben, hätte ich kein Problem damit, den SQL-Generator an das XML-DOM zu koppeln. Sparen Sie morgen für morgen.

Kevin Cline
quelle
Hier kommt "(eigentlich viel mehr als das, aber lassen Sie es uns einfach halten)" ins Spiel. In einigen Fällen muss ich beispielsweise eine Tabelle löschen, um zu überprüfen, ob Einschränkungen auf diese Tabelle verweisen.
Tim Meyer