Lutter avec le principe de responsabilité unique

11

Considérez cet exemple:

J'ai un site Web. Il permet aux utilisateurs de créer des messages (peut être n'importe quoi) et d'ajouter des balises qui décrivent le message. Dans le code, j'ai deux classes qui représentent le message et les balises. Appelons ces classes Postet Tag.

Posts'occupe de créer des posts, de supprimer des posts, de mettre à jour des posts, etc. Tags'occupe de créer des tags, de supprimer des tags, de mettre à jour des tags, etc.

Il manque une opération. La liaison des balises aux messages. Je me bats avec qui devrait faire cette opération. Il pourrait tout aussi bien s'intégrer dans l'une ou l'autre classe.

D'une part, la Postclasse pourrait avoir une fonction qui prend a Tagcomme paramètre, puis la stocke dans une liste de balises. D'un autre côté, la Tagclasse pourrait avoir une fonction qui prend un Postcomme paramètre et lie le Tagau Post.

Ce qui précède n'est qu'un exemple de mon problème. En fait, je rencontre cela avec plusieurs classes qui sont toutes similaires. Il pourrait tout aussi bien convenir aux deux. À moins de mettre la fonctionnalité dans les deux classes, quelles conventions ou quels styles de conception existent pour m'aider à résoudre ce problème. Je suppose qu'il doit y avoir autre chose que d'en choisir un?

Peut-être que le mettre dans les deux classes est la bonne réponse?

Oiseau en colère
la source

Réponses:

11

Comme le Code Pirate, le SRP est plus une directive qu'une règle, et ce n'est même pas particulièrement bien formulé. La plupart des développeurs ont accepté les redéfinitions de Martin Fowler (dans Refactoring ) et Robert Martin (dans Clean Code ), suggérant qu'une classe ne devrait avoir qu'une seule raison de changer (par opposition à une responsabilité).

C'est une bonne directive solide (excusez le jeu de mots), mais il est presque aussi dangereux de s'y accrocher que de l'ignorer.

Si vous pouvez ajouter une publication à un tag et vice versa, vous n'avez pas enfreint le principe de la responsabilité unique. Les deux n'ont toujours qu'une seule raison de changer - si la structure de cet objet change. Changer la structure de l'un ou de l'autre ne change pas la façon dont il est ajouté à l'autre, donc vous n'y ajoutez pas une nouvelle "responsabilité".

Votre décision finale devrait vraiment être dictée par les fonctionnalités requises sur le front-end. Il sera probablement nécessaire d'ajouter un tag à un message à un moment donné, alors faites quelque chose comme ceci:

// 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
}

Si, plus tard, vous trouvez également la nécessité d'ajouter des publications aux balises, ajoutez simplement une méthode addPost à la classe Tag et une méthode referenceTag à la classe Post. Évidemment, je les ai nommés différemment afin de ne pas provoquer accidentellement un débordement de pile en appelant addTag depuis addPost et addPost depuis addTag.

pdr
la source
Je pense que la relation entre Tag et Post est plusieurs-à-plusieurs, auquel cas il est logique qu'un Tag conserve des références à plusieurs Posts. Comment géreriez-vous cela si vous gardiez une seule référence?
Andres F.
@AndresF .: Je suis d'accord avec vous, donc je n'ai clairement pas très bien écrit ma réponse. J'ai édité de manière significative. (Toutes mes excuses à l'électeur précédent si cela change le sens tel que vous l'avez vu.)
pdr
6

Non, pas dans les deux! Il devrait être au même endroit.

Ce que je trouve inquiétant dans votre question, c'est le fait que vous dites " Posts'occupe de créer des messages, de supprimer des messages, de mettre à jour des messages" et de même pour Tag. Eh bien, ce n'est pas vrai. Postne peut que s'occuper de la mise à jour, de même pour Tag. La création et la suppression sont le travail de quelqu'un d'autre, extérieur à Postet Tag(appelons-le simplement Store).

Une bonne responsabilité pour Post"connaît son auteur, son contenu et la date de la dernière mise à jour". Une bonne responsabilité Tagest "connaît son nom et son but (lire: description)". Une bonne responsabilité pour Store"connaît tous les messages et toutes les balises et peut les ajouter, les supprimer et les rechercher".

Si vous regardez ces trois participants, qui est le plus naturellement celui qui devrait avoir la connaissance de la relation Post-Tag?

(pour moi, c'est le Post, il semble naturel qu'il "connaisse ses tags"; la recherche inversée (tous les posts pour un tag) semble être le travail du Store; même si je me trompe)

herby
la source
Si chaque publication a une liste de balises, et / ou chaque balise a une liste de publications auxquelles l'inclure, on peut facilement répondre à la question "la publication x inclut-elle la balise y". Existe-t-il un moyen de répondre efficacement à une telle question sans qu'aucune des classes ne prenne la responsabilité, autrement qu'en utilisant quelque chose comme un ConditionalWeakTable(en supposant que l'on a la chance d'avoir un cadre là où il en existe un)?
supercat
3

Il y a un détail important manquant dans l'équation. Pourquoi le tag contient-il du courrier et vice-versa? La réponse à cette question détermine la solution pour chaque ensemble donné.

En général, je peux penser à une situation similaire. Une boîte et son contenu. Une boîte a du contenu, donc une relation has-a est appropriée. Le contenu peut-il avoir une boîte? Bien sûr, une boîte avec une boîte. Une boîte est le contenu. Mais IS-A n'est pas une bonne conception pour toutes les boîtes. Dans un tel cas, je considérerais le motif de décorateur. De cette façon, une boîte est décorée avec du contenu lors de l'exécution si nécessaire.

Les tags peuvent également avoir des messages, mais pour moi, ce n'est pas une relation statique. Il pourrait plutôt s'agir d'un rapport de tous les messages ayant ladite balise. Dans ce cas, c'est une nouvelle entité, pas has-a.

P.Brian.Mackey
la source
2

Alors qu'en théorie, des choses comme ça peuvent aller dans les deux sens, dans la pratique, lorsque vous passez à la mise en œuvre, une façon est presque toujours mieux adaptée que l'autre. Mon intuition est qu'elle s'intégrera mieux dans la Postclasse car l'association sera créée lors de la création ou de la modification du post, lorsque d'autres choses sur le post changent en même temps.

En outre, si vous associez plusieurs balises et souhaitez le faire dans une mise à jour de base de données, vous devez créer une sorte de liste de toutes les balises associées à la même publication avant de procéder à la mise à jour. Cette liste s'intègre beaucoup mieux dans la Postclasse.

Karl Bielefeldt
la source
1

Personnellement, je n'ajouterais cette fonctionnalité à aucun d'eux.

Pour moi, les deux Postet Tagsont des objets de données, donc ne devraient pas gérer la fonctionnalité de base de données. Ils devraient simplement exister. Ils sont destinés à contenir des données et à être utilisés par d'autres parties de votre application.

Au lieu de cela, j'aurais une autre classe qui est responsable de votre logique métier et des données relatives à votre page Web. Si votre page affiche une publication et autorise les utilisateurs à ajouter des balises, la classe aurait un Postobjet et contiendrait des fonctionnalités à ajouter Tagsà cela Post. Si votre page affiche des balises et permet aux utilisateurs d'ajouter des publications à ces balises, elle contiendrait un Tagobjet et aurait des fonctionnalités à ajouter Postsà cela Tag.

C'est juste moi cependant. Si vous pensez que vous devez gérer la fonctionnalité de base de données dans vos objets de données, alors je recommanderais la réponse de pdr

Rachel
la source
0

En lisant cette question, la première chose qui m'est venue à l'esprit était une relation de base de données plusieurs à plusieurs . Les publications peuvent avoir plusieurs balises ... Les balises peuvent avoir plusieurs publications .... Il me semble que les deux classes doivent avoir la capacité de gérer cette relation dans une certaine mesure.

Du point de vue de
la publication ... Si vous modifiez ou créez une publication, une activité secondaire devient la gestion des relations avec les balises .

  1. Ajouter une balise existante à la publication
  2. Supprimer la balise de la publication

OMI, la création d'un tout nouveau TAG n'a pas sa place ici.

Du point de vue du Tag ...
Vous pouvez créer un Tag sans avoir à l'assigner à un Post. La seule activité que je vois qui implique d'interagir avec un message est une fonction Supprimer le tag. Cependant, cette fonction devrait être une fonction autonome indépendante.

Cela ne fonctionnera que s'il existe une table de liaison de base de données qui résout la relation plusieurs à plusieurs

Michael Riley - AKA Gunny
la source