So erstellen Sie besseren OO-Code in einer relationalen datenbankgesteuerten Anwendung, in der die Datenbank schlecht konzipiert ist

19

Ich schreibe eine Java-Webanwendung, die hauptsächlich aus einer Reihe ähnlicher Seiten besteht, in denen jede Seite mehrere Tabellen und einen Filter enthält, der für diese Tabellen gilt. Die Daten in diesen Tabellen stammen aus einer SQL-Datenbank.

Ich verwende myBatis als ORM, was in meinem Fall möglicherweise nicht die beste Wahl ist, da die Datenbank schlecht gestaltet ist und mybatis ein stärker datenbankorientiertes Tool ist.

Ich stelle fest, dass ich viel doppelten Code schreibe, da ich aufgrund des schlechten Designs der Datenbank unterschiedliche Abfragen für ähnliche Dinge schreiben muss, da diese Abfragen sehr unterschiedlich sein können. Das heißt, ich kann die Abfragen nicht einfach parametrisieren. Dies pflanzt sich in meinen Code fort und anstatt Zeilen in Spalten in meiner Tabelle mit einer einfachen Schleife zu füllen, habe ich folgenden Code:

Hole A Daten (p1, ..., pi);

erhalten B Daten (p1, ..., pi);

erhalten C - Daten (p1, ..., pi);

erhalten D - Daten (p1, ..., pi); ...

Und das explodiert bald, wenn wir unterschiedliche Tabellen mit unterschiedlichen Spalten haben.

Es trägt auch zur Komplexität bei, dass ich "Wicket" verwende, also eine Zuordnung von Objekten zu HTML-Elementen auf der Seite. So wird mein Java-Code zu einem Adapter zwischen der Datenbank und dem Front-End, wodurch ich viel Verkabelung, Boilerplate-Code mit einer darin eingemischten Logik erstelle.

Wäre es die richtige Lösung, die ORM-Mapper mit einer Extralayer zu umhüllen, die eine homogenere Schnittstelle zur Datenbank bietet, oder gibt es eine bessere Möglichkeit, mit diesem Spaghetti-Code umzugehen, den ich schreibe?

EDIT: Weitere Informationen zur Datenbank

Die Datenbank enthält hauptsächlich Informationen zu Telefonanrufen. Das schlechte Design besteht aus:

Tabellen mit einer künstlichen ID als Primärschlüssel, die nichts mit dem Domänenwissen zu tun haben.

Keine Eindeutigkeit, Trigger, Schecks oder Fremdschlüssel.

Felder mit einem generischen Namen, die unterschiedlichen Konzepten für unterschiedliche Datensätze entsprechen.

Datensätze, die nur durch Kreuzung mit anderen Tabellen mit anderen Bedingungen kategorisiert werden können.

Spalten, bei denen es sich um Zahlen oder Datumsangaben handeln soll, die als Zeichenfolgen gespeichert werden.

Um es zusammenzufassen, ein chaotisches / faules Design rundum.

DPM
quelle
7
Ist das Korrigieren des Datenbankdesigns eine Option?
RMalke
1
Bitte erläutern Sie, wie schlecht die Datenbank aufgebaut ist.
Tulains Córdova
@Renan Malke Stigliani Leider nicht, da es eine ältere Software gibt, die davon abhängt. Ich habe jedoch einige Tabellen mit einem etwas anderen Design gespiegelt und sie aufgefüllt, was den Code vereinfacht. Ich bin jedoch nicht stolz darauf und möchte Tabellen nicht wahllos duplizieren
DPM
1
In diesem Buch finden Sie möglicherweise einige Ideen, wie Sie beginnen können, das Datenbankproblem zu beheben und den alten Code am Laufen zu halten: amazon.com/…
HLGEM 30.07.13
4
Die meisten Probleme, die Sie auflisten. . . nicht. Die Verwendung von Ersatzschlüsseln anstelle von natürlichen Schlüsseln ist heutzutage eine recht übliche Empfehlung. überhaupt nicht "schlechtes Design". Das Fehlen von Einschränkungen und die Verwendung unangemessener Spaltentypen sind ein besseres Beispiel für "schlechtes Design", aber es sollte sich eigentlich überhaupt nicht auf Ihren Anwendungscode auswirken (es sei denn, Sie planen, diese Probleme zu missbrauchen?).
Ruakh

Antworten:

53

Objektorientierung ist besonders deshalb von großem Wert, weil solche Szenarien auftreten und Sie Werkzeuge zum vernünftigen Entwerfen von Abstraktionen erhalten, mit denen Sie die Komplexität zusammenfassen können.

Die eigentliche Frage ist hier, wo Sie diese Komplexität zusammenfassen.

Lassen Sie mich einen Moment zurücktreten und über die Komplexität sprechen, auf die ich mich hier beziehe. Ihr Problem (so wie ich es verstehe; korrigieren Sie mich, wenn ich falsch liege) ist ein Persistenzmodell, das für die Aufgaben, die Sie mit den Daten ausführen müssen, nicht effektiv verwendbar ist. Es kann für andere Aufgaben effektiv und verwendbar sein, aber nicht für Ihre Aufgaben.

Was machen wir also, wenn wir Daten haben, die kein gutes Modell für unsere Mittel darstellen?

Übersetzen. Du bist das Einzige tun kannst . Diese Übersetzung ist die 'Komplexität', auf die ich mich oben beziehe. Da wir nun akzeptieren, dass wir das Modell übersetzen, müssen wir uns für einige Faktoren entscheiden.

Müssen wir beide Richtungen übersetzen? Werden beide Richtungen gleich übersetzt, wie in:

(Tbl A, Tbl B) -> Obj X (lesen)

Obj X -> (Tbl A, Tbl B) (schreiben)

oder stellen Einfüge- / Aktualisierungs- / Löschaktivitäten einen anderen Objekttyp dar, sodass Sie Daten als Obj X lesen, aber Daten aus Obj Y eingefügt / aktualisiert werden? Welche dieser beiden Möglichkeiten Sie nutzen möchten oder ob keine Aktualisierung / Einfügung / Löschung möglich ist, ist ein wichtiger Faktor für den Ort, an dem Sie die Übersetzung einfügen möchten.


Wo übersetzen Sie?

Zurück zu der ersten Aussage, die ich in dieser Antwort gemacht habe; OO ermöglicht es Ihnen , zu verkapseln Komplexität und was ich hier beziehen , ist die Tatsache , dass nicht nur sollten Sie, aber Sie müssen diese Komplexität kapseln , wenn Sie möchten, um sicherzustellen , nicht durchsickern und sickern in den gesamten Code. Gleichzeitig ist es wichtig zu erkennen, dass Sie keine haben können perfekte Abstraktion haben können. Sorgen Sie sich also weniger darum als darum, eine sehr effektive und verwendbare zu haben.

Wieder jetzt; Ihr Problem ist: Wo setzen Sie diese Komplexität ein? Nun, Sie haben die Wahl.

Sie können dies mit gespeicherten Prozeduren in der Datenbank tun . Dies hat den Nachteil, dass es mit ORMs oft nicht sehr gut funktioniert, aber das stimmt nicht immer. Gespeicherte Prozeduren bieten einige Vorteile, darunter häufig die Leistung. Gespeicherte Prozeduren können jedoch eine Menge Wartung erfordern, aber es liegt an Ihnen, Ihr spezielles Szenario zu analysieren und zu sagen, ob die Wartung mehr oder weniger als andere Entscheidungen sein wird. Ich persönlich kenne mich mit gespeicherten Prozeduren sehr gut aus, und als solche reduziert diese Tatsache des verfügbaren Talents den Overhead. nie unterschätzen den Wert von Entscheidungen basierend auf was Sie tun wissen. Manchmal ist die suboptimale Lösung optimaler als die richtige, weil Sie oder Ihr Team wissen, wie man sie besser erstellt und verwaltet als die optimale Lösung.

Eine weitere Option in der Datenbank sind Ansichten. Abhängig von Ihrem Datenbankserver können diese sehr optimal oder suboptimal oder gar nicht effektiv sein. Einer der Nachteile kann die Abfragezeit sein, je nachdem, welche Indizierungsoptionen in Ihrer Datenbank verfügbar sind. Ansichten werden zu einer noch besseren Wahl, wenn Sie keine Datenänderungen vornehmen müssen (Einfügen / Aktualisieren / Löschen).

Wenn Sie über die Datenbank hinausgehen, haben Sie die alte Bereitschaft, das Repository-Muster zu verwenden. Dies ist ein bewährter Ansatz, der sehr effektiv sein kann. Zu den Nachteilen gehört in der Regel die Kesselplatte, aber gut faktorisierte Repositorys können eine gewisse Menge davon vermeiden. Selbst wenn dies zu unglücklichen Mengen an Kesselplatte führt, handelt es sich bei den Repositorys in der Regel um einfachen Code, der einfach zu verstehen und zu warten ist sowie eine gute API darstellt /Abstraktion. Repositorys können auch für ihre Unit-Testbarkeit gut sein, die Sie durch datenbankinterne Optionen verlieren.

Es gibt Tools wie Auto-Mapper , die die Verwendung eines ORM plausibel machen, um die Übersetzung zwischen Datenbankmodellen von Orm in verwendbare Modelle zu ermöglichen. Einige dieser Tools können jedoch schwierig sein, magisches Verhalten beizubehalten / zu verstehen. Sie erstellen jedoch ein Minimum an Overhead-Code, was zu weniger Wartungsaufwand führt, wenn sie gut verstanden werden.

Als nächstes entfernen Sie sich immer weiter von der Datenbank , was bedeutet, dass es größere Mengen an Code geben wird, die sich mit dem nicht übersetzten Persistenzmodell befassen werden, was wirklich unangenehm sein wird. In diesen Szenarien geht es darum, die Übersetzungsebene in Ihre Benutzeroberfläche einzufügen, wie es sich anhört, als ob Sie dies jetzt tun. Dies ist im Allgemeinen eine sehr schlechte Idee und verfällt mit der Zeit schrecklich.


Jetzt fangen wir an, verrückt zu reden .

Das Objectist nicht die einzige Abstraktion, die existiert. Im Laufe der vielen Jahre, in denen Informatik studiert wurde, und noch vor dieser Zeit nach dem Studium der Mathematik, hat sich eine Fülle von Abstraktionen entwickelt. Wenn wir kreativ werden wollen, lassen Sie uns über bekannte verfügbare Abstraktionen sprechen, die untersucht wurden.

Da ist das Schauspielermodell.Dies ist ein interessanter Ansatz, da Sie lediglich Nachrichten an anderen Code senden müssen, um die gesamte Arbeit effektiv an diesen anderen Code zu delegieren. Dadurch wird die Komplexität sehr effektiv von Ihrem gesamten Code getrennt. Dies könnte insofern funktionieren, als Sie eine Nachricht an einen Schauspieler mit der Aufschrift "Ich muss Obj X an Y senden" senden und an Position Y ein Behälter auf eine Antwort wartet, der dann Obj X verarbeitet. Sie können sogar eine Nachricht senden, die anweist "Ich muss Obj X und Berechnung Y, Z erledigen" und dann müssen Sie nicht einmal warten; Die Übersetzung erfolgt auf der anderen Seite des Nachrichtendurchlaufs und Sie können einfach weitermachen, wenn Sie das Ergebnis nicht lesen müssen. Dies kann ein geringfügiger Missbrauch des Darstellermodells für Ihre Zwecke sein, aber alles hängt davon ab.

Eine weitere Einkapselungsgrenze sind Prozessgrenzen. Diese können zur effektiven Trennung der Komplexität verwendet werden. Sie können den Übersetzungscode als Webdienst mit einfacher HTTP-Kommunikation mithilfe von SOAP, REST oder wenn Sie wirklich ein eigenes Protokoll möchten (nicht empfohlen) erstellen. STOMP ist insgesamt kein schlechtes neueres Protokoll. Oder verwenden Sie einen normalen Daemon-Dienst mit einer systemlokalen öffentlich zugänglichen Speicher-Pipe, um mit einem beliebigen Protokoll sehr schnell wieder zu kommunizieren. Dies hat tatsächlich einige ziemlich gute Vorteile:

  • Es können mehrere Prozesse ausgeführt werden, die die Übersetzung für ältere und neuere Versionen gleichzeitig ausführen. Auf diese Weise können Sie den Übersetzungsdienst aktualisieren, um ein Objektmodell V2 zu veröffentlichen, und den verbrauchenden Code zu einem späteren Zeitpunkt separat aktualisieren, um mit dem neuen Objekt zu arbeiten Modell.
  • Sie können interessante Dinge tun, z. B. das Fixieren des Prozesses auf einen Kern für die Leistung. Sie erhalten bei diesem Ansatz auch ein gewisses Maß an Sicherheit, indem Sie diesen Prozess zum einzigen Prozess machen, der mit den Sicherheitsberechtigungen zum Berühren dieser Daten ausgeführt wird.
  • Sie werden eine sehr starke Grenze erhalten, wenn Sie über Prozessgrenzen sprechen, die fest bleiben und für eine lange Zeit ein Minimum an Abstraktionslecks gewährleisten, da das Schreiben von Code im Übersetzungsraum nicht außerhalb des Übersetzungsraums aufgerufen werden kann, da diese nicht vorhanden sind Der Prozessumfang wird nicht gemeinsam genutzt, wodurch vertraglich festgelegte Nutzungsszenarien sichergestellt werden.
  • Die Möglichkeit für asynchrone / nicht blockierende Updates ist einfacher.

Die Nachteile sind offensichtlich mehr Wartung als gewöhnlich erforderlich, und der Kommunikationsaufwand beeinträchtigt die Leistung und die Wartung.


Es gibt eine Vielzahl von Möglichkeiten, die Komplexität zusammenzufassen, damit diese Komplexität an immer seltsameren und merkwürdigeren Stellen in Ihrem System abgelegt werden kann. Mit Formen von Funktionen höherer Ordnung (oft mit Hilfe von Strategiemustern oder verschiedenen anderen ungeraden Formen von Objektmustern gefälscht) können Sie einige sehr interessante Dinge tun.

Das ist richtig, lassen Sie uns über eine Monade sprechen.Sie könnten diese Übersetzungsschicht in einer sehr unabhängigen Weise aus kleinen spezifischen Funktionen erstellen, die die erforderlichen unabhängigen Übersetzungen ausführen, aber alle nicht sichtbaren Übersetzungsfunktionen ausblenden, damit sie für externen Code kaum zugänglich sind. Dies hat den Vorteil, dass die Abhängigkeit von ihnen verringert wird und sie sich leicht ändern können, ohne großen Einfluss auf externen Code zu haben. Anschließend erstellen Sie eine Klasse, die Funktionen höherer Ordnung (anonyme Funktionen, Lambda-Funktionen, Strategieobjekte, die Sie jedoch strukturieren müssen) akzeptiert, die auf allen Objekten vom Typ OO-Modell funktionieren. Sie lassen dann den zugrunde liegenden Code, der diese Funktionen akzeptiert, die wörtliche Ausführung mit den entsprechenden Übersetzungsmethoden durchführen.

Dadurch wird eine Grenze erstellt, an der die gesamte Übersetzung nicht nur auf der anderen Seite der Grenze vorhanden ist, sondern nicht in Ihrem gesamten Code. Es wird nur auf dieser Seite verwendet, sodass der Rest Ihres Codes nur weiß, wo sich der Einstiegspunkt für diese Grenze befindet.

Ok, ja, das ist wirklich verrückt, aber wer weiß? Sie könnten einfach so verrückt sein (im Ernst, unternehmen Sie keine Monaden mit einer Verrücktheitsrate von unter 88%, es besteht die reale Gefahr von Körperverletzungen).

Jimmy Hoffa
quelle
4
Wow, was für eine außergewöhnlich umfassende Antwort. Ich würde dies mehr als einmal befürworten, wenn nur SE es mir erlauben würde.
Marjan Venema
11
Wann kommt die Filmversion heraus?
Yannis
3
@ JimmyHoffa Bravo Sir !!! Ich werde diese Antwort mit einem Lesezeichen versehen und meiner Tochter zeigen, wenn sie älter wird.
Tombatron
4

Mein Vorschlag:

Erstellen Sie Datenbankansichten, die:

  1. Geben Sie Spalten aussagekräftige Namen
  2. Machen Sie die "Kreuzung mit anderen Tischen mit anderen Bedingungen", damit Sie diese Komplexität verbergen können.
  3. Konvertieren Sie Zahlen oder Datumsangaben, die als Zeichenfolgen gespeichert sind, in Zahlen bzw. Datumsangaben.
  4. Erstellen Sie nach bestimmten Kriterien eine Eindeutigkeit, bei der es keine gibt.

Die Idee ist, eine Fassade zu schaffen, die ein besseres Design über dem schlechten emuliert.

Stellen Sie dann sicher, dass sich das ORM auf diese Fassade bezieht und nicht auf die realen Tabellen.

Dies vereinfacht das Einfügen jedoch nicht.

Tulains Córdova
quelle
Die Verwendung von Datenbankansichten scheint eine großartige Idee zu sein, und die eleganteste Vorgehensweise abstrahiert die Hässlichkeit auf der untersten Ebene. Aus irgendeinem Grund hatte ich darüber nicht nachgedacht. Vielen Dank.
DPM
3

Ich kann sehen, wie Ihr vorhandenes Datenbankschema Sie veranlasst, spezifischeren Code und Abfragen für Aufgaben zu schreiben, die andernfalls mit einem besser gestalteten Schema abstrahiert würden, aber es sollte Ihre Fähigkeit, guten objektorientierten Code zu schreiben, nicht behindern.

  • Denken Sie an die SOLID-Prinzipien .
  • Schreiben Sie Code, der sich leicht in einem Unit-Test testen lässt (was häufig durch das Befolgen der SOLID-Prinzipien erfolgt).
  • Halten Sie Ihre Geschäftslogik von Ihrer Anzeigelogik getrennt.
  • Lesen Sie die Dokumentation und Beispiele zu Apache Wicket. Mit diesem Framework können Sie wahrscheinlich mehr Code sparen, als Sie denken. Erfahren Sie also, wie Sie ihn effektiv einsetzen.
  • Bewahren Sie die Logik, die mit der Datenbank umgehen muss, in einer separaten Ebene auf, die eine saubere Schnittstelle bietet, mit der Ihre Geschäftslogik arbeiten kann. Auf diese Weise können Sie (oder ein zukünftiger Betreuer) das Schema verbessern, ohne zu viele Änderungen an der Geschäftslogik vornehmen zu müssen.

Wenn Sie feststellen, dass Sie mit einem Datenbankschema arbeiten, das nicht perfekt ist, können Sie leicht herausfinden, auf welche Weise es Ihre Arbeit erschwert, aber irgendwann müssen Sie diese Beschwerden beiseite legen und das Beste daraus machen.

Stellen Sie sich das als Gelegenheit vor, Ihre Kreativität zu nutzen, um sauberen, wiederverwendbaren und leicht zu wartenden Code trotz des unvollkommenen Schemas zu schreiben.

Mike Partridge
quelle
1

Ich würde vorschlagen, SQL-sprechende Objekte zu verwenden, um Ihre ursprüngliche Frage nach besserem objektorientiertem Code zu beantworten . ORM widerspricht grundsätzlich objektorientierten Prinzipien, da es auf ein Objekt einwirkt und das Objekt in OOP eine autarke Einheit ist, die über alle Ressourcen verfügt, um sein Ziel zu erreichen. Ich bin sicher, dass dieser Ansatz Ihren Code einfacher machen könnte.

Wenn ich über den Problembereich, dh Ihre Domain, spreche, würde ich versuchen, aggregierte Wurzeln zu identifizieren . Dies sind Konsistenzgrenzen Ihrer Domain. Grenzen, die immer Bestand haben müssen. Aggregate kommunizieren über Domain-Ereignisse. Wenn Sie ein System haben, das groß genug ist, sollten Sie es wahrscheinlich auf Subsysteme aufteilen (nennen Sie es SOA, Microservice, in sich geschlossene Systeme usw.).

Ich würde auch die Verwendung von CQRS in Betracht ziehen - dies kann sowohl die Schreib- als auch die Leseseite erheblich vereinfachen. Lesen Sie unbedingt den Artikel von Udi Dahan zu diesem Thema.

Zapadlo
quelle