Ein Objekt über eine eigene Methode oder über eine andere Klasse speichern?

38

Wenn ich ein Objekt speichern und abrufen möchte, sollte ich eine andere Klasse erstellen, um damit umzugehen, oder wäre es besser, dies in der Klasse selbst zu tun? Oder vielleicht beides mischen?

Was wird nach dem OOD-Paradigma empfohlen?

Beispielsweise

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

Gegen. (Ich weiß, das Folgende wird empfohlen, aber es scheint mir ein bisschen gegen den Zusammenhalt der StudentKlasse)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

Wie wäre es, sie zu mischen?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }
Ahmad
quelle
3
Sie sollten einige der Forschung auf dem Active Record, wie Sie diese Frage oder diese
Eric König
@gnat, ich dachte, zu fragen, was besser ist, ist meinungsbasiert, fügte dann "Für und Wider" hinzu und habe kein Problem, es zu entfernen, aber tatsächlich meine ich in Bezug auf das OOD-Paradigma, das empfohlen wird
Ahmad
3
Es gibt keine richtige Antwort. Es hängt von Ihren Bedürfnissen, Ihren Vorlieben, der Art und Weise ab, wie Ihre Klasse verwendet werden soll und wo Sie Ihr Projekt sehen. Das einzig richtige OO ist, dass Sie als Objekte speichern und abrufen können.
Dunk

Antworten:

34

Grundsatz der einheitlichen Verantwortung , Trennung von Anliegen und funktionaler Zusammenhalt . Wenn Sie sich über diese Konzepte informieren, lautet die Antwort: Trennen Sie sie .

Ein einfacher Grund, die StudentKlasse "DB" zu trennen (oder StudentRepositorygängigeren Konventionen zu folgen), besteht darin, dass Sie Ihre in der StudentKlasse vorhandenen "Geschäftsregeln" ändern können , ohne den für die Persistenz verantwortlichen Code zu beeinflussen. umgekehrt.

Diese Art der Trennung ist sehr wichtig, nicht nur zwischen Geschäftsregeln und Persistenz, sondern auch zwischen den vielen Belangen Ihres Systems, damit Sie Änderungen in nicht verwandten Modulen mit minimaler Auswirkung vornehmen können (minimal, da dies manchmal unvermeidbar ist). Es hilft, robustere Systeme zu bauen, die einfacher zu warten sind und bei ständigen Änderungen zuverlässiger sind.

Indem Sie Geschäftsregeln und Persistenz miteinander vermischen, entweder eine einzelne Klasse wie im ersten Beispiel oder DBeine Abhängigkeit von Student, verbinden Sie zwei sehr unterschiedliche Anliegen. Es mag so aussehen, als ob sie zusammengehören. Sie scheinen zusammenhängend zu sein, weil sie dieselben Daten verwenden. Aber hier ist die Sache: Kohäsion kann nicht nur an den Daten gemessen werden, die zwischen Prozeduren geteilt werden. Sie müssen auch die Abstraktionsebene berücksichtigen, auf der sie existieren. Tatsächlich wird die ideale Art der Kohäsion beschrieben als:

Funktionale Kohäsion ist, wenn Teile eines Moduls gruppiert werden, weil sie alle zu einer einzigen genau definierten Aufgabe des Moduls beitragen.

Und es ist klar, dass das Ausführen von Überprüfungen über einen Studentlängeren Zeitraum hinweg keine "einzelne, genau definierte Aufgabe" darstellt. Auch hier sind Geschäftsregeln und Persistenzmechanismen zwei sehr unterschiedliche Aspekte des Systems, die nach vielen Prinzipien eines guten objektorientierten Designs voneinander getrennt werden sollten.

Ich empfehle, über saubere Architektur zu lesen , diese Gespräche über das Prinzip der Einzelverantwortung zu lesen (wobei ein sehr ähnliches Beispiel verwendet wird) und diese Gespräche auch über saubere Architektur zu lesen . Diese Konzepte umreißen die Gründe für solche Trennungen.

MichelHenrich
quelle
11
Ah, aber die StudentKlasse sollte richtig gekapselt sein, ja? Wie kann dann eine externe Klasse die Persistenz des Privatstaates handhaben? Die Verkapselung muss gegen SoC und SRP abgewogen werden, aber es ist wahrscheinlich falsch, eine der beiden Methoden zu wählen, ohne die Kompromisse sorgfältig abzuwägen. Eine mögliche Lösung für dieses Problem ist die Verwendung von paketprivaten Zugriffsmethoden für den zu verwendenden Persistenzcode.
amon
1
@amon Ich stimme zu, dass es nicht so einfach ist, aber ich glaube, es ist eine Frage der Kopplung, nicht der Verkapselung. Beispielsweise können Sie die Student-Klasse mit abstrakten Methoden für alle vom Unternehmen benötigten Daten abstrahieren, die dann vom Repository implementiert werden. Auf diese Weise ist die Datenstruktur der Datenbank vollständig hinter der Repository-Implementierung verborgen, sodass sie sogar von der Student-Klasse eingekapselt ist. Gleichzeitig ist das Repository jedoch stark an die Student-Klasse gekoppelt. Wenn abstrakte Methoden geändert werden, muss sich auch die Implementierung ändern. Es geht nur um Kompromisse. :)
MichelHenrich
4
Die Behauptung, dass SRP die Wartung von Programmen erleichtert, ist bestenfalls zweifelhaft. Das Problem, das SRP verursacht, besteht darin, dass statt einer Sammlung gut benannter Klassen der Name alles angibt, was die Klasse kann und was nicht (mit anderen Worten, leicht zu verstehen, was zu einer einfachen Wartung führt), am Ende Hunderte / Tausende von Klassen, in denen die Leute einen Thesaurus benutzen mussten, um einen eindeutigen Namen zu finden. Viele Namen sind ähnlich, aber nicht ganz. IOW, überhaupt nicht wartbar, IMO.
Dunk
3
@Dunk Sie sprechen, als ob Klassen die einzige verfügbare Modularisierungseinheit wären. Ich habe viele Projekte nach SRP gesehen und geschrieben und bin damit einverstanden, dass Ihr Beispiel tatsächlich auftritt, aber nur, wenn Sie Pakete und Bibliotheken nicht korrekt verwenden. Wenn Sie Verantwortlichkeiten in Pakete aufteilen, da der Kontext durch sie impliziert wird, können Sie die Klassen wieder frei benennen. Um ein System wie dieses zu warten, müssen Sie lediglich das richtige Paket auswählen, bevor Sie nach der zu ändernden Klasse suchen. :)
MichelHenrich
5
@Dunk OOD-Prinzipien können wie folgt ausgedrückt werden: "Im Prinzip ist dies aufgrund von X, Y und Z besser." Es bedeutet nicht, dass es immer angewendet werden sollte; Die Leute müssen pragmatisch sein und die Kompromisse abwägen. In großen Projekten überwiegen die Vorteile in der Regel die Lernkurve, von der Sie sprechen. Dies ist jedoch für die meisten Menschen nicht die Realität und sollte daher nicht so häufig angewendet werden. Selbst in den leichteren SRP-Anwendungen können wir uns jedoch beide einig sein, dass die Trennung von Persistenz und Geschäftsregeln immer Vorteile bringt, die über die Kosten hinausgehen (mit Ausnahme von möglicherweise wegwerfbarem Code).
MichelHenrich
10

Beide Ansätze verstoßen gegen das Prinzip der einheitlichen Verantwortung. Ihre erste Version weist der StudentKlasse viele Verantwortlichkeiten zu und bindet sie an eine bestimmte DB-Zugriffstechnologie. Die zweite führt zu einer riesigen DBKlasse, die nicht nur für Schüler, sondern für alle anderen Arten von Datenobjekten in Ihrem Programm verantwortlich ist. BEARBEITEN: Ihr dritter Ansatz ist der schlechteste, da er eine zyklische Abhängigkeit zwischen der DB-Klasse und der StudentKlasse erzeugt.

Wenn Sie also kein Spielzeugprogramm schreiben möchten, verwenden Sie keines davon. Verwenden Sie stattdessen eine andere Klasse wie a, StudentRepositoryum eine API zum Laden und Speichern bereitzustellen, vorausgesetzt, Sie implementieren den CRUD-Code selbst. Sie können auch die Verwendung eines ORM- Frameworks in Betracht ziehen , das die harte Arbeit für Sie erledigen kann (und das Framework erzwingt normalerweise einige Entscheidungen, bei denen die Lade- und Speichervorgänge platziert werden müssen).

Doc Brown
quelle
Vielen Dank, ich habe meine Antwort geändert. Was ist mit dem dritten Ansatz? Trotzdem denke ich, hier geht es darum, verschiedene Ebenen
Ahmad
Etwas schlägt mir auch vor, StudentRepository anstelle von DB zu verwenden (weiß nicht warum! Vielleicht noch einmal einzelne Verantwortung), aber ich kann immer noch nicht herausfinden, warum für jede Klasse ein anderes Repository vorhanden ist. Wenn es sich nur um eine Datenzugriffsschicht handelt, gibt es mehr statische Dienstprogrammfunktionen, die zusammen verwendet werden können, nicht wahr? Vielleicht würde es dann so dreckig und voll werden (Entschuldigung für mein Englisch)
Ahmad
1
@ Ahmad: es gibt ein paar Gründe und ein paar Vor- und Nachteile zu bedenken. Dies könnte ein ganzes Buch füllen. Die Gründe dafür sind Testbarkeit, Wartbarkeit und langfristige Entwicklungsfähigkeit Ihrer Programme, insbesondere wenn sie einen langen Lebenszyklus haben.
Doc Brown
Hat jemand an einem Projekt mitgearbeitet, bei dem sich die DB-Technologie grundlegend geändert hat? (ohne ein massives umschreiben?) habe ich nicht. Wenn ja, hat das Befolgen von SRP, wie hier vorgeschlagen, um eine Bindung an diese Datenbank zu vermeiden, erheblich geholfen?
user949300
@ user949300: Ich glaube, ich habe mich nicht klar ausgedrückt. Die Studentenklasse ist nicht an eine bestimmte DB-Technologie gebunden, sondern an eine bestimmte DB-Zugriffstechnologie. Sie erwartet daher eine Art SQL-DB für die Persistenz. Wenn Sie die Klasse Student nicht über den Persistenzmechanismus auf dem Laufenden halten, können Sie die Klasse in verschiedenen Kontexten viel einfacher wiederverwenden. Zum Beispiel in einem Testkontext oder in einem Kontext, in dem die Objekte in einer Datei gespeichert sind. Und das habe ich in der Vergangenheit sehr oft getan. Sie müssen nicht den gesamten DB-Stack Ihres Systems ändern, um davon zu profitieren.
Doc Brown
3

Es gibt einige Muster, die für die Datenpersistenz verwendet werden können. Es gibt das Muster " Arbeitseinheit", das Muster " Repository" , einige zusätzliche Muster, die wie die Remote-Fassade verwendet werden können, und so weiter.

Die meisten davon haben ihre Fans und ihre Kritiker. Oft kommt es darauf an, herauszufinden, was am besten zur Anwendung passt, und dabei zu bleiben (mit all seinen Vor- und Nachteilen, dh nicht beide Muster gleichzeitig zu verwenden ... es sei denn, Sie sind sich wirklich sicher).

Als Randnotiz: In Ihrem Beispiel sollte RetrieveStudent AddStudent statische Methoden sein (da sie nicht instanzabhängig sind).

Eine andere Möglichkeit, in der Klasse gespeicherte / geladene Methoden zu verwenden, ist:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

Persönlich würde ich einen solchen Ansatz nur in relativ kleinen Anwendungen verwenden, möglicherweise für den persönlichen Gebrauch, oder wenn ich verlässlich vorhersagen kann, dass ich keine komplizierteren Anwendungsfälle haben werde, als nur Objekte zu speichern oder zu laden.

Siehe auch persönlich das Muster der Arbeitseinheit. Wenn Sie es kennenlernen, ist es in kleinen und großen Fällen wirklich gut. Und es wird von vielen Frameworks / APIs unterstützt, zum Beispiel EntityFramework oder RavenDB.

Gerino
quelle
2
Zu Ihrer Randnotiz: Obwohl konzeptionell nur eine Datenbank vorhanden ist, während die Anwendung ausgeführt wird, bedeutet dies nicht, dass Sie die Methoden RetriveStudent und AddStudent statisch machen sollten. Repositorys werden normalerweise mit Schnittstellen erstellt, damit die Entwickler die Implementierung wechseln können, ohne den Rest der Anwendung zu beeinträchtigen. Auf diese Weise können Sie Ihre Anwendung beispielsweise mit Unterstützung für verschiedene Datenbanken verteilen. Dieselbe Logik gilt natürlich auch für viele andere Bereiche außer der Persistenz, wie zum Beispiel die Benutzeroberfläche.
MichelHenrich
Sie haben definitiv recht, ich habe viele Annahmen getroffen, die auf der ursprünglichen Frage basieren.
Gerino
Vielen Dank, ich denke, der beste Ansatz ist die Verwendung von Ebenen, wie im Repository-Muster erwähnt
Ahmad
1

Wenn es sich um eine sehr einfache App handelt, bei der das Objekt mehr oder weniger an den Datenspeicher gebunden ist und umgekehrt (dh als eine Eigenschaft des Datenspeichers betrachtet werden kann), ist eine .save () -Methode für diese Klasse möglicherweise sinnvoll.

Aber ich denke, das wäre ziemlich außergewöhnlich.

Es ist in der Regel besser, die Klasse ihre Daten und ihre Funktionalität verwalten zu lassen (wie ein guter OO-Bürger) und den Persistenzmechanismus für eine andere Klasse oder eine Gruppe von Klassen zu externalisieren.

Die andere Option ist die Verwendung eines Persistenz-Frameworks, das die Persistenz deklarativ definiert (wie bei Annotationen), die Persistenz jedoch weiterhin externalisiert.

rauben
quelle
-1
  • Definieren Sie eine Methode für diese Klasse, die das Objekt serialisiert und eine Folge von Bytes zurückgibt, die Sie in eine Datenbank oder Datei oder was auch immer einfügen können
  • Verfügen Sie über einen Konstruktor für dieses Objekt, der eine solche Folge von Bytes als Eingabe verwendet

In Bezug auf die RetrieveAllStudents()Methode ist Ihr Gefühl richtig, es ist in der Tat wahrscheinlich falsch platziert, weil Sie möglicherweise mehrere unterschiedliche Listen von Studenten haben. Warum nicht einfach die Liste (n) außerhalb der StudentKlasse lassen?

philfr
quelle
Sie wissen, ich habe einen solchen Trend in einigen PHP-Frameworks gesehen, zum Beispiel Laravel, wenn Sie es wissen. Das Modell ist an eine Datenbank gebunden und kann sogar eine Reihe von Objekten zurückgeben.
Ahmad