Kampf mit dem Prinzip der Einzelverantwortung

11

Betrachten Sie dieses Beispiel:

Ich habe eine Website. Es ermöglicht Benutzern, Beiträge zu verfassen (kann alles sein) und Tags hinzuzufügen, die den Beitrag beschreiben. Im Code habe ich zwei Klassen, die den Beitrag und die Tags darstellen. Nennen wir diese Klassen Postund Tag.

Postkümmert sich um das Erstellen von Posts, das Löschen von Posts, das Aktualisieren von Posts usw. Tagkümmert sich um das Erstellen von Tags, das Löschen von Tags, das Aktualisieren von Tags usw.

Es fehlt eine Operation. Das Verknüpfen von Tags mit Posts. Ich habe Probleme damit, wer diese Operation durchführen soll. Es könnte in jede Klasse gleich gut passen.

Einerseits Postkönnte die Klasse eine Funktion haben, die a Tagals Parameter verwendet und es dann in einer Liste von Tags speichert. Andererseits Tagkönnte die Klasse eine Funktion haben, die a Postals Parameter verwendet und das Tagmit dem verknüpft Post.

Das Obige ist nur ein Beispiel für mein Problem. Ich stoße tatsächlich auf mehrere Klassen, die alle ähnlich sind. Es könnte in beide gleich gut passen. Welche Konventionen oder Designstile gibt es, um dieses Problem zu lösen, ohne die Funktionalität tatsächlich in beide Klassen einzuteilen? Ich gehe davon aus, dass es etwas anderes geben muss, als nur eines auszuwählen?

Vielleicht ist es die richtige Antwort, es in beide Klassen einzuteilen?

Wütender Vogel
quelle

Antworten:

11

Wie der Piratenkodex ist die SRP eher eine Richtlinie als eine Regel, und sie ist nicht einmal besonders gut formuliert. Die meisten Entwickler haben die Neudefinitionen von Martin Fowler (in Refactoring ) und Robert Martin (in Clean Code ) akzeptiert , was darauf hindeutet, dass eine Klasse nur einen Grund zur Änderung haben sollte (im Gegensatz zu einer Verantwortung).

Es ist eine gute, solide Richtlinie (entschuldigen Sie das Wortspiel), aber es ist fast genauso gefährlich, sich daran zu hängen, wie es zu ignorieren.

Wenn Sie einem Tag einen Beitrag hinzufügen können und umgekehrt, haben Sie das Prinzip der Einzelverantwortung nicht verletzt. Beide haben immer noch nur einen Grund, sich zu ändern - wenn sich die Struktur dieses Objekts ändert. Das Ändern der Struktur eines der beiden ändert nichts an der Art und Weise, wie es dem anderen hinzugefügt wird, sodass Sie ihm keine neue "Verantwortung" hinzufügen.

Ihre endgültige Entscheidung sollte wirklich von der Funktionalität bestimmt werden, die für das Frontend erforderlich ist. Es wird wahrscheinlich notwendig sein, einem Post irgendwann ein Tag hinzuzufügen. Gehen Sie also wie folgt vor:

// C-style-language pseudo-code
class Post {
    string _title;
    string _content;
    Date _date;
    List<Tag> _tags;

    Post(string title, string content) {
        _title = title;
        _content = content;
        _date = Now;
        _tags = new List<Tag>();
    }

    Tag[] getTags() {
        return _tags.toArray();
    }

    void addTag(Tag tag) {
        if (_tags.contains(tag)) {
            throw "Cannot add tag twice";
        }

        _tags.Add(tag);
        tag.referencePost(this);
    }

    // more stuff here, obviously
}

class Tag {
    string _name;
    List<Post> _posts;

    Tag(string name) {
        _name = name;
    }

    Post[] getPosts() {
        return _posts.toArray();
    }

    void referencePost(Post post) {
        if (!post.getTags().contains(this) || _posts.contains(post)) {
            throw "Only reference a post by calling Post.addTag()";
        }

        _posts.Add(post);
    }

    // more stuff here too
}

Wenn Sie später auch Beiträge zu Tags hinzufügen müssen, fügen Sie der Tag-Klasse einfach eine addPost-Methode und der Post-Klasse eine referenceTag-Methode hinzu. Offensichtlich habe ich sie anders benannt, damit Sie nicht versehentlich einen Stapelüberlauf verursachen, indem Sie addTag von addPost und addPost von addTag aufrufen.

pdr
quelle
Ich denke, die Beziehung zwischen Tag und Post ist viele zu viele. In diesem Fall ist es sinnvoll, dass ein Tag Verweise auf mehrere Posts enthält. Wie würden Sie damit umgehen, wenn Sie eine einzige Referenz behalten?
Andres F.
@AndresF.: Ich stimme Ihnen zu, daher habe ich meine Antwort offensichtlich nicht sehr gut geschrieben. Ich habe deutlich bearbeitet. (Entschuldigung an den vorherigen Aufsteiger, wenn dies die Bedeutung ändert, wie Sie es gesehen haben.)
pdr
6

Nein, nicht in beiden! Es sollte an einem Ort sein.

Was ich an Ihrer Frage unangenehm finde, ist die Tatsache, dass Sie sagen " Postkümmert sich um das Erstellen von Posts, das Löschen von Posts, das Aktualisieren von Posts" und dasselbe für Tag. Nun, das ist nicht richtig. Postkann sich nur um die Aktualisierung kümmern, das gleiche gilt für Tag. Das Erstellen und Löschen ist die Arbeit einer anderen Person außerhalb von Postund Tag(nennen wir es einfach Store).

Gute Verantwortung für Postist "kennt den Autor, den Inhalt und das Datum der letzten Aktualisierung". Gute Verantwortung für Tagist "kennt seinen Namen und Zweck (lesen: Beschreibung)". Gute Verantwortung für Storeist "kennt alle Beiträge und alle Tags und kann sie hinzufügen, entfernen und durchsuchen".

Wenn Sie sich diese drei Teilnehmer ansehen, wer ist am natürlichsten derjenige, der das Wissen über die Post-Tag-Beziehung haben sollte?

(Für mich ist es der Beitrag, es scheint natürlich, dass er "seine Tags kennt"; die umgekehrte Suche (alle Beiträge für einen Tag) scheint die Aufgabe des Geschäfts zu sein; obwohl ich mich möglicherweise irre)

herby
quelle
Wenn jeder Beitrag eine Liste von Tags enthält und / oder jeder Tag eine Liste von Beiträgen enthält, zu denen er gehört, kann die Frage "Enthält der Beitrag x das Tag y?" Leicht beantwortet werden. Gibt es eine Möglichkeit, eine solche Frage effizient zu beantworten, ohne dass eine der Klassen Verantwortung übernimmt, außer durch die Verwendung von so etwas wie a ConditionalWeakTable(vorausgesetzt, man hat das Glück, einen Rahmen zu haben, in dem man existiert)?
Supercat
3

In der Gleichung fehlt ein wichtiges Detail. Warum enthält Tag Post und umgekehrt? Die Antwort auf diese Frage bestimmt die Lösung für jeden gegebenen Satz.

Im Allgemeinen kann ich mir eine ähnliche Situation vorstellen. Eine Box und Inhalt. Eine Box hat Inhalt, daher ist eine Beziehung angemessen. Kann der Inhalt eine Box haben? Klar, eine Box mit einer Box. Eine Box ist Inhalt. IS-A ist jedoch nicht für alle Boxen geeignet. In einem solchen Fall würde ich ein Dekorationsmuster in Betracht ziehen. Auf diese Weise wird eine Box bei Bedarf zur Laufzeit mit Inhalten dekoriert.

Tags können auch Posts haben, aber für mich ist dies keine statische Beziehung. Es könnte sich vielmehr um einen Bericht aller Beiträge handeln, die diesen Tag haben. In diesem Fall handelt es sich um eine neue Entität, nicht um eine.

P.Brian.Mackey
quelle
2

Während theoretisch solche Dinge in beide Richtungen gehen können, passt in der Praxis, wenn Sie zur Implementierung kommen, eine Richtung fast immer besser als die andere. Meine Vermutung ist, dass es besser in die PostKlasse passt, da die Zuordnung während der Erstellung oder Bearbeitung von Posts erstellt wird, wenn sich gleichzeitig andere Dinge an der Post ändern.

Wenn Sie mehrere Tags zuordnen und dies in einem Datenbankupdate tun möchten, müssen Sie vor dem Update eine Liste aller Tags erstellen, die demselben Beitrag zugeordnet sind. Diese Liste passt viel besser in die PostKlasse.

Karl Bielefeldt
quelle
1

Persönlich würde ich diese Funktionalität keinem von beiden hinzufügen.

Für mich sind beide Postund TagDatenobjekte, sollten also nicht mit Datenbankfunktionen umgehen. Sie sollten einfach existieren. Sie sollen Daten speichern und von anderen Teilen Ihrer Anwendung verwendet werden.

Stattdessen hätte ich eine andere Klasse, die für Ihre Geschäftslogik und Daten zu Ihrer Webseite verantwortlich ist. Wenn Ihre Seite einen Beitrag anzuzeigen und damit die Benutzer Tags hinzuzufügen, dann würde die Klasse ein hat PostObjekt und enthält Funktionen hinzufügen Tagszu , dass Post. Wenn Ihre Seite Tags anzeigen und damit die Benutzer Beiträge zu diesen Tags hinzuzufügen, wäre es ein enthaltenes TagObjekt und Funktionalität hinzufügen müssen Postszu , dass Tag.

Das bin aber nur ich. Wenn Sie der Meinung sind, dass Sie die Datenbankfunktionalität in Ihren Datenobjekten verwalten müssen, würde ich die Antwort von pdr empfehlen

Rachel
quelle
0

Als ich diese Frage las, fiel mir als erstes eine Many-To-Many- Datenbankbeziehung ein. Posts können viele Tags haben ... Tags können viele Posts haben ... Es scheint mir, dass beide Klassen die Fähigkeit benötigen, diese Beziehung bis zu einem gewissen Grad zu verwalten.

Aus der Sicht des Beitrags ...
Wenn Sie einen Beitrag bearbeiten oder erstellen, wird eine sekundäre Aktivität zum Verwalten von Tag- Beziehungen.

  1. Vorhandenes Tag zum Post hinzufügen
  2. Tag aus Post entfernen

IMO, die Schaffung eines völlig neuen TAG gehört nicht hierher.

Aus der Sicht des Tags ...
Sie können ein Tag erstellen, ohne es einem Beitrag zuweisen zu müssen. Die einzige Aktivität, die ich sehe, die die Interaktion mit einem Beitrag beinhaltet, ist eine Funktion zum Löschen von Tags. Diese Funktion sollte jedoch eine eigenständige Funktion sein.

Dies funktioniert nur, wenn eine Datenbankverknüpfungstabelle vorhanden ist, die die Beziehung Viele zu Viele auflöst

Michael Riley - AKA Gunny
quelle