Ich versuche, ein Programm für die Verwaltung von Mitarbeitern zu erstellen. Ich kann jedoch nicht herausfinden, wie ich die Employee
Klasse gestalten soll. Mein Ziel ist es, Mitarbeiterdaten in der Datenbank mithilfe eines Employee
Objekts erstellen und bearbeiten zu können .
Die grundlegende Implementierung, an die ich dachte, war diese einfache:
class Employee
{
// Employee data (let's say, dozens of properties).
Employee() {}
Create() {}
Update() {}
Delete() {}
}
Bei dieser Implementierung sind mehrere Probleme aufgetreten.
- Die
ID
ein Mitarbeiter von der Datenbank gegeben wird, so dass , wenn ich das Objekt zur Beschreibung einen neuen Mitarbeiters verwende, wird es keine seinID
noch zu speichern, während ein Objekt einen vorhandenen Mitarbeiter darstellen wird eine habenID
. Ich habe also eine Eigenschaft, die das Objekt manchmal beschreibt und manchmal nicht (Was könnte darauf hinweisen, dass wir gegen SRP verstoßen ? Da wir dieselbe Klasse für die Darstellung neuer und bestehender Mitarbeiter verwenden ...). - Die
Create
Methode soll einen Mitarbeiter in der Datenbank erstellen, während dieUpdate
undDelete
auf einen vorhandenen Mitarbeiter einwirken sollen (erneut SRP ...). - Welche Parameter sollte die Methode 'Erstellen' haben? Dutzende von Parametern für alle Mitarbeiterdaten oder vielleicht ein
Employee
Objekt? - Sollte die Klasse unveränderlich sein?
- Wie wird die
Update
Arbeit? Nimmt es die Eigenschaften und aktualisiert die Datenbank? Oder werden möglicherweise zwei Objekte - ein "altes" und ein "neues" - benötigt, um die Datenbank mit den Unterschieden zwischen ihnen zu aktualisieren? (Ich denke, die Antwort hat mit der Antwort über die Veränderlichkeit der Klasse zu tun). - Was wäre die Verantwortung des Konstrukteurs? Was wären die Parameter, die es braucht? Würde es Mitarbeiterdaten mithilfe eines
id
Parameters aus der Datenbank abrufen und diese mit den Eigenschaften füllen?
Wie Sie sehen, habe ich ein bisschen Unordnung im Kopf und bin sehr verwirrt. Könnten Sie mir bitte helfen zu verstehen, wie eine solche Klasse aussehen sollte?
Bitte beachten Sie, dass ich keine Meinungen haben möchte, nur um zu verstehen, wie eine so häufig verwendete Klasse im Allgemeinen gestaltet ist.
Employee
Objekt zur Abstraktion verwendet werden, sind die Fragen 4. und 5. im Allgemeinen nicht zu beantworten, hängen von Ihren Anforderungen ab. Wenn Sie die Struktur- und CRUD-Operationen in zwei Klassen unterteilen, ist es ziemlich klar, dass der Konstruktor vonEmployee
keine Daten abrufen kann von db mehr, so dass Antworten 6.Update
ein Mitarbeiter oder aktualisieren Sie einen Mitarbeiterdatensatz? Hast duEmployee.Delete()
oder macht einBoss.Fire(employee)
?Antworten:
Dies ist eine wohlgeformtere Transkription meines ersten Kommentars unter Ihrer Frage. Die Antworten auf Fragen, die vom OP beantwortet werden, finden Sie am Ende dieser Antwort. Bitte überprüfen Sie auch den wichtigen Hinweis am selben Ort.
Was Sie derzeit beschreiben, Sipo, ist ein Entwurfsmuster namens Active Record . Wie bei allem hat auch dieser seinen Platz unter den Programmierern gefunden, wurde jedoch aus einem einfachen Grund, der Skalierbarkeit, zugunsten des Repositorys und der Data Mapper- Muster verworfen .
Kurz gesagt, ein aktiver Datensatz ist ein Objekt, das:
Sie sprechen mehrere Probleme mit Ihrem aktuellen Design an, und das Hauptproblem Ihres Designs wird im letzten, sechsten Punkt angesprochen (last but not least, denke ich). Wenn Sie eine Klasse haben, für die Sie einen Konstruktor entwerfen, und Sie nicht einmal wissen, was der Konstruktor tun soll, macht die Klasse wahrscheinlich etwas falsch. Das ist in deinem Fall passiert.
Das Korrigieren des Entwurfs ist jedoch ziemlich einfach, indem die Entitätsdarstellung und die CRUD-Logik in zwei (oder mehr) Klassen aufgeteilt werden.
So sieht Ihr Design jetzt aus:
Employee
- enthält Informationen über die Mitarbeiterstruktur (ihre Attribute) und Methoden zum Ändern der Entität (wenn Sie sich für einen veränderlichen Weg entscheiden), enthält CRUD-Logik für dieEmployee
Entität, kann eine Liste vonEmployee
Objekten zurückgeben und akzeptiert einEmployee
Objekt, wenn Sie möchten Aktualisieren Sie einen Mitarbeiter, kann eine einzelneEmployee
über eine Methode wie zurückgebengetSingleById(id : string) : Employee
Wow, die Klasse scheint riesig.
Dies wird die vorgeschlagene Lösung sein:
Employee
- enthält Informationen über die Mitarbeiterstruktur (ihre Attribute) und Methoden zum Ändern der Entität (wenn Sie sich für einen veränderlichen Weg entscheiden)EmployeeRepository
- enthält CRUD-Logik für dieEmployee
Entität, kann eine Liste vonEmployee
Objekten zurückgeben, akzeptiert einEmployee
Objekt, wenn Sie einen Mitarbeiter aktualisieren möchten, kann ein einzelnesEmployee
über eine Methode wie zurückgebengetSingleById(id : string) : Employee
Haben Sie von der Trennung von Bedenken gehört ? Nein, das wirst du jetzt. Es ist die weniger strenge Version des Prinzips der Einzelverantwortung, die besagt, dass eine Klasse eigentlich nur eine Verantwortung haben sollte, oder wie Onkel Bob sagt:
Es ist ziemlich klar, dass, wenn ich Ihre anfängliche Klasse klar in zwei Klassen aufteilen konnte, die immer noch eine gut abgerundete Oberfläche haben, die anfängliche Klasse wahrscheinlich zu viel getan hat, und das war es auch.
Das Besondere am Repository-Muster ist, dass es nicht nur als Abstraktion dient, um eine mittlere Schicht zwischen der Datenbank (die alles sein kann, Datei, NoSQL, SQL, objektorientiert) bereitzustellen, sondern auch nicht konkret sein muss Klasse. In vielen OO-Sprachen können Sie die Schnittstelle als eine tatsächliche
interface
(oder eine Klasse mit einer rein virtuellen Methode, wenn Sie sich in C ++ befinden) definieren und dann mehrere Implementierungen durchführen.Dies hebt die Entscheidung, ob ein Repository eine tatsächliche Implementierung von Ihnen ist, vollständig auf. Sie verlassen sich einfach auf die Schnittstelle, indem Sie sich tatsächlich auf eine Struktur mit dem
interface
Schlüsselwort verlassen. Und Repository ist genau das. Es ist ein ausgefallener Begriff für die Abstraktion von Datenschichten, nämlich das Zuordnen von Daten zu Ihrer Domain und umgekehrt.Eine weitere großartige Sache bei der Aufteilung in (mindestens) zwei Klassen ist, dass die
Employee
Klasse jetzt ihre eigenen Daten klar verwalten und es sehr gut machen kann, da sie sich nicht um andere schwierige Dinge kümmern muss.Frage 6: Was soll der Konstruktor in der neu erstellten
Employee
Klasse tun ? Es ist einfach. Es sollte die Argumente übernehmen, prüfen, ob sie gültig sind (z. B. sollte ein Alter wahrscheinlich nicht negativ sein oder der Name sollte nicht leer sein), einen Fehler auslösen, wenn die Daten ungültig waren und wenn die bestandene Validierung die Argumente privaten Variablen zuweist des Unternehmens. Es kann jetzt nicht mit der Datenbank kommunizieren, da es einfach keine Ahnung hat, wie es geht.Frage 4: Kann überhaupt nicht beantwortet werden, nicht generell, da die Antwort stark davon abhängt, was genau Sie brauchen.
Frage 5: Nun , da Sie die aufgeblähte Klasse in zwei getrennt haben, können Sie auf der mehrere Update - Methoden direkt haben
Employee
Klasse, wiechangeUsername
,markAsDeceased
, die die Daten der ManipulationEmployee
Klasse nur im RAM und dann könnte man ein Verfahren , wie zum Beispiel vorstelltregisterDirty
von der Arbeitseinheitsmuster für die Repository-Klasse, über das Sie dem Repository mitteilen würden, dass dieses Objekt Eigenschaften geändert hat und nach dem Aufrufen dercommit
Methode aktualisiert werden muss .Offensichtlich muss ein Objekt für ein Update eine ID haben und daher bereits gespeichert sein. Es ist die Verantwortung des Repositorys, dies zu erkennen und einen Fehler auszulösen, wenn die Kriterien nicht erfüllt sind.
Frage 3: Wenn Sie sich für das Arbeitseinheitsmuster entscheiden, lautet die
create
Methode jetztregisterNew
. Wenn Sie dies nicht tun, würde ich es wahrscheinlichsave
stattdessen nennen. Das Ziel eines Repositorys ist es, eine Abstraktion zwischen der Domäne und der Datenschicht bereitzustellen. Aus diesem Grund würde ich Ihnen empfehlen, dass diese Methode (sei esregisterNew
odersave
) dasEmployee
Objekt akzeptiert und es den Klassen überlassen bleibt, die die Repository-Schnittstelle implementieren, welche Attribute Sie beschließen, das Unternehmen zu verlassen. Das Übergeben eines gesamten Objekts ist besser, sodass Sie nicht viele optionale Parameter benötigen.Frage 2: Beide Methoden werden nun Teil der Repository-Schnittstelle und verstoßen nicht gegen das Prinzip der Einzelverantwortung. Die Verantwortung des Repositorys besteht darin, CRUD-Operationen für die
Employee
Objekte bereitzustellen. Dies ist die Aufgabe (neben Lesen und Löschen übersetzt CRUD sowohl Erstellen als auch Aktualisieren). Natürlich könnten Sie das Repository noch weiter aufteilen, indem Sie einEmployeeUpdateRepository
und so weiter haben, aber das wird selten benötigt und eine einzelne Implementierung kann normalerweise alle CRUD-Operationen enthalten.Frage 1: Sie haben eine einfache
Employee
Klasse erhalten, die jetzt (unter anderem) die ID hat. Ob die ID gefüllt oder leer (odernull
) ist, hängt davon ab, ob das Objekt bereits gespeichert wurde. Trotzdem ist eine ID immer noch ein Attribut, das die Entität besitzt, und die Verantwortung derEmployee
Entität besteht darin, sich um ihre Attribute und damit um ihre ID zu kümmern.Ob eine Entität eine ID hat oder nicht, spielt normalerweise keine Rolle, bis Sie versuchen, eine Persistenzlogik darauf zu erstellen. Wie in der Antwort auf Frage 5 erwähnt, liegt es in der Verantwortung des Repositorys, festzustellen, dass Sie nicht versuchen, eine bereits gespeicherte Entität zu speichern oder eine Entität ohne ID zu aktualisieren.
Wichtige Notiz
Bitte beachten Sie, dass das Entwerfen einer funktionalen Repository-Schicht zwar eine große Trennung der Bedenken ist, aber eine ziemlich mühsame Arbeit ist und meiner Erfahrung nach etwas schwieriger zu erreichen ist als der Ansatz der aktiven Aufzeichnung. Am Ende erhalten Sie jedoch ein Design, das weitaus flexibler und skalierbarer ist, was eine gute Sache sein kann.
quelle
Erstellen Sie zunächst eine Mitarbeiterstruktur mit den Eigenschaften des konzeptionellen Mitarbeiters.
Erstellen Sie dann eine Datenbank mit einer passenden Tabellenstruktur, z. B. mssql
Erstellen Sie dann ein Mitarbeiter-Repository für diese Datenbank EmployeeRepoMsSql mit den verschiedenen CRUD-Operationen, die Sie benötigen.
Erstellen Sie dann eine IEmployeeRepo-Schnittstelle, die die CRUD-Operationen verfügbar macht
Erweitern Sie dann Ihre Employee-Struktur zu einer Klasse mit dem Konstruktionsparameter IEmployeeRepo. Fügen Sie die verschiedenen erforderlichen Methoden zum Speichern / Löschen usw. hinzu und implementieren Sie sie mit dem injizierten EmployeeRepo.
Wenn es sich um Id handelt, schlage ich vor, dass Sie eine GUID verwenden, die über Code im Konstruktor generiert werden kann.
Um mit vorhandenen Objekten zu arbeiten, kann Ihr Code diese über das Repository aus der Datenbank abrufen, bevor Sie deren Aktualisierungsmethode aufrufen.
Alternativ können Sie sich für das verpönte (aber meiner Ansicht nach überlegene) Anemic Domain Object-Modell entscheiden, bei dem Sie Ihrem Objekt keine CRUD-Methoden hinzufügen und das Objekt einfach an das Repo übergeben, um es zu aktualisieren / speichern / löschen
Unveränderlichkeit ist eine Designentscheidung, die von Ihren Mustern und Ihrem Codierungsstil abhängt. Wenn Sie alle funktionsfähig sind, versuchen Sie auch, unveränderlich zu sein. Wenn Sie sich nicht sicher sind, ist ein veränderliches Objekt wahrscheinlich einfacher zu implementieren.
Anstelle von Create () würde ich mit Save () gehen. Erstellen funktioniert mit dem Unveränderlichkeitskonzept, aber ich finde es immer nützlich, ein Objekt erstellen zu können, das noch nicht "gespeichert" ist, z. B. wenn Sie eine Benutzeroberfläche haben, mit der Sie ein oder mehrere Mitarbeiterobjekte auffüllen und sie dann vor einigen Regeln erneut überprüfen können Speichern in der Datenbank.
***** Beispielcode
quelle
UserUpdate
Service mit einerchangeUsername(User user, string newUsername)
Methode benötigen , wenn ich diechangeUsername
Methode genauso gutUser
direkt zur Klasse hinzufügen kann? Einen Service dafür zu erstellen ist unsinnig.Überprüfung Ihres Designs
Sie
Employee
sind in Wirklichkeit eine Art Proxy für ein Objekt, das dauerhaft in der Datenbank verwaltet wird.Ich schlage daher vor, an die ID zu denken, als wäre sie ein Verweis auf Ihr Datenbankobjekt. Unter Berücksichtigung dieser Logik können Sie Ihr Design wie bei Nicht-Datenbankobjekten fortsetzen. Mit der ID können Sie die traditionelle Kompositionslogik implementieren:
Employee
Möglicherweise muss es noch erstellt werden, oder es wurde einfach gelöscht.Sie müssten auch einen Status für das Objekt verwalten. Beispielsweise:
Vor diesem Hintergrund könnten wir uns entscheiden für:
Um Ihren Objektstatus zuverlässig verwalten zu können, müssen Sie eine bessere Kapselung sicherstellen, indem Sie die Eigenschaften privat machen und den Zugriff nur über Getter und Setter gewähren, die den Status der Setter aktualisieren.
Deine Fragen
Ich denke, die ID-Eigenschaft verletzt nicht die SRP. Die einzige Verantwortung besteht darin, auf ein Datenbankobjekt zu verweisen.
Ihr Mitarbeiter als Ganzes ist nicht mit dem SRP kompatibel, da er für die Verknüpfung mit der Datenbank verantwortlich ist, aber auch für das Speichern temporärer Änderungen und für alle Transaktionen, die mit diesem Objekt ausgeführt werden.
Ein anderes Design könnte darin bestehen, die veränderbaren Felder in einem anderen Objekt zu belassen, das nur geladen wird, wenn auf Felder zugegriffen werden muss.
Sie können die Datenbanktransaktionen auf dem Mitarbeiter mithilfe des Befehlsmusters implementieren . Diese Art von Design würde auch die Entkopplung zwischen Ihren Geschäftsobjekten (Mitarbeiter) und Ihrem zugrunde liegenden Datenbanksystem erleichtern, indem datenbankspezifische Redewendungen und APIs isoliert werden.
Ich würde nicht ein Dutzend Parameter hinzufügen
Create()
, da sich die Geschäftsobjekte weiterentwickeln und die Wartung sehr schwierig machen könnten. Und der Code würde unlesbar werden. Hier haben Sie zwei Möglichkeiten: Entweder übergeben Sie einen minimalistischen Satz von Parametern (nicht mehr als 4), die für die Erstellung eines Mitarbeiters in der Datenbank unbedingt erforderlich sind, und führen die verbleibenden Änderungen per Update durch, oder Sie übergeben ein Objekt. Übrigens verstehe ich in Ihrem Design, dass Sie bereits gewählt haben :my_employee.Create()
.Sollte die Klasse unveränderlich sein? Siehe obige Diskussion: In Ihrem Originalentwurf Nr. Ich würde mich für einen unveränderlichen Ausweis entscheiden, aber nicht für einen unveränderlichen Mitarbeiter. Ein Mitarbeiter entwickelt sich im wirklichen Leben (neue Arbeitsstelle, neue Adresse, neue eheliche Situation, sogar neue Namen ...). Ich denke, es wird einfacher und natürlicher sein, mit dieser Realität zu arbeiten, zumindest in der Ebene der Geschäftslogik.
Wenn Sie einen Befehl zum Aktualisieren und ein bestimmtes Objekt für (GUI?) Verwenden möchten, um die gewünschten Änderungen zu speichern, können Sie sich für einen alten / neuen Ansatz entscheiden. In allen anderen Fällen würde ich ein veränderbares Objekt aktualisieren. Achtung: Das Update kann Datenbankcode auslösen, sodass Sie nach einem Update sicherstellen sollten, dass das Objekt immer noch wirklich mit der Datenbank synchronisiert ist.
Ich denke, dass das Abrufen eines Mitarbeiters aus der Datenbank im Konstruktor keine gute Idee ist, da das Abrufen schief gehen kann und es in vielen Sprachen schwierig ist, mit fehlgeschlagenen Konstruktionen umzugehen. Der Konstruktor sollte das Objekt (insbesondere die ID) und seinen Status initialisieren.
quelle