web-dev-qa-db-fra.com

Cette conception de classe viole-t-elle le principe de responsabilité unique?

Aujourd'hui, j'ai eu une dispute avec quelqu'un.

J'expliquais les avantages d'avoir un modèle de domaine riche par opposition à un modèle de domaine anémique. Et j'ai démontré mon point avec une classe simple ressemblant à ça:

public class Employee
{
    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastname;
    }

    public string FirstName { get private set; }
    public string LastName { get; private set;}
    public int CountPaidDaysOffGranted { get; private set;}

    public void AddPaidDaysOffGranted(int numberOfdays)
    {
        // Do stuff
    }
}

Alors qu'il défendait son approche de modèle anémique, l'un de ses arguments était: "Je crois en SOLIDE . Vous violez le principe de responsabilité unique (SRP), car vous représentez à la fois des données et exécutez la logique dans la même classe. "

J'ai trouvé cette affirmation vraiment surprenante, car en suivant ce raisonnement, toute classe ayant une propriété et une méthode viole le SRP, et donc OOP en général n'est pas SOLIDE, et programmation fonctionnelle est le seul chemin vers le ciel.

J'ai décidé de ne pas répondre à ses nombreux arguments, mais je suis curieux de savoir ce que la communauté pense de cette question.

Si j'avais répondu, j'aurais commencé par souligner le paradoxe mentionné ci-dessus, puis indiquer que le SRP dépend fortement du niveau de granularité que vous souhaitez prendre en compte et que si vous le prenez assez loin, toute classe contenant plus d'un ou une méthode la viole.

Qu'auriez-vous dit?

Mise à jour: L'exemple a été généreusement mis à jour par guntbert pour rendre la méthode plus réaliste et nous aider à nous concentrer sur la discussion sous-jacente.

63
tobiak777

La responsabilité unique doit être comprise comme une abstraction des tâches logiques dans votre système. Une classe devrait avoir la responsabilité unique (de faire tout ce qui est nécessaire pour) d'exécuter une seule tâche spécifique. Cela peut en fait apporter beaucoup dans une classe bien conçue, selon la responsabilité. La classe qui exécute votre moteur de script, par exemple, peut avoir beaucoup de méthodes et de données impliquées dans le traitement des scripts.

Votre collègue se concentre sur la mauvaise chose. La question n'est pas "quels sont les membres de cette classe?" mais "quelle (s) opération (s) utile (s) cette classe effectue-t-elle dans le programme?" Une fois cela compris, votre modèle de domaine semble très bien.

68
Mason Wheeler

Le principe de responsabilité unique ne concerne que si un morceau de code (dans la POO, nous parlons généralement de classes) a la responsabilité d'un morceau de fonctionnalité . Je pense que votre ami disant que les fonctions et les données ne peuvent pas se mélanger n'a pas vraiment compris cette idée. Si Employee devait également contenir des informations sur son lieu de travail, la vitesse de sa voiture et le type de nourriture que son chien mange, nous aurions un problème.

Étant donné que cette classe ne traite que d'un Employee, je pense qu'il est juste de dire qu'elle ne viole pas SRP de manière flagrante, mais les gens auront toujours leur propre opinion.

Un endroit où nous pourrions nous améliorer est de séparer les informations sur les employés (comme le nom, le numéro de téléphone, l'e-mail) de leurs vacances. Dans mon esprit, cela ne signifie pas que les méthodes et les données ne peuvent pas se mélanger, cela signifie simplement que peut-être la fonctionnalité de vacances pourrait être dans un endroit séparé.

41
Frank Bryce

Il existe déjà d'excellentes réponses soulignant que SRP est un concept abstrait sur la fonctionnalité logique, mais il y a des points plus subtils qui, à mon avis, méritent d'être ajoutés.

Les deux premières lettres de SOLID, SRP et OCP, indiquent toutes deux comment votre code change en réponse à une modification des exigences. Ma définition préférée du SRP est: "un module/classe/fonction ne devrait avoir qu'une seule raison de changer." Discuter des raisons probables de la modification de votre code est beaucoup plus productif que de se demander si votre code est SOLID ou non.

Combien de raisons votre classe d'employés doit-elle changer? Je ne sais pas, parce que je ne connais pas le contexte dans lequel vous l'utilisez, et je ne vois pas non plus l'avenir. Ce que je peux faire, c'est réfléchir à des changements possibles en fonction de ce que j'ai vu dans le passé, et vous pouvez évaluer subjectivement leur probabilité. Si plusieurs scores entre "raisonnablement probable" et "mon code a déjà changé pour cette raison exacte", alors vous violez SRP contre ce type de changement. En voici une: quelqu'un avec plus de deux noms rejoint votre entreprise (ou un architecte lit cet excellent article du W3C ). En voici une autre: votre entreprise modifie la façon dont elle attribue les jours fériés.

Notez que ces raisons sont également valables même si vous supprimez la méthode AddHolidays. De nombreux modèles de domaines anémiques violent le SRP. Beaucoup d'entre elles ne sont que des tables de base de données en code, et il est très courant que les tables de base de données aient plus de 20 raisons de changer.

Voici quelque chose à mâcher: votre classe d'employé changerait-elle si votre système devait suivre les salaires des employés? Adresses? Informations de contact d'urgence? Si vous avez dit "oui" (et "susceptible de se produire") à deux d'entre eux, alors votre classe violerait SRP même si elle n'avait pas encore de code! SOLID concerne autant les processus et l'architecture que le code.

20
Carl Leth

À mon avis, cette classe pourrait potentiellement violer SRP si elle continuait à représenter à la fois un Employee et EmployeeHolidays.

Telle qu'elle est, et si elle me venait pour examen par les pairs, je la laisserais probablement passer. Si plus de propriétés et de méthodes spécifiques aux employés étaient ajoutées, et plus de propriétés spécifiques aux vacances étaient ajoutées, je conseillerais probablement une scission, citant à la fois SRP et ISP.

19
NikolaiDante

Que la classe représente des données n'est pas la responsabilité de la classe, c'est un détail d'implémentation privé.

La classe a une responsabilité, celle de représenter un employé. Dans ce contexte, cela signifie qu'il présente une API publique qui vous fournit les fonctionnalités dont vous avez besoin pour traiter avec les employés (si AddHolidays est un bon exemple est discutable).

La mise en œuvre est interne; il se trouve que cela nécessite des variables privées et une logique. Cela ne signifie pas que la classe a désormais plusieurs responsabilités.

9
RemcoGerlich

L'idée que mélanger la logique et les données de quelque manière que ce soit est toujours fausse, est tellement ridicule qu'elle ne mérite même pas d'être discutée. Cependant, il y a bien une violation claire de la responsabilité unique dans l'exemple, mais ce n'est pas parce qu'il y a une propriété DaysOfHolidays et une fonction AddHolidays(int).

C'est parce que l'identité de l'employé est mêlée à la gestion des vacances, ce qui est mauvais. L'identité de l'employé est une chose complexe qui est nécessaire pour suivre les vacances, le salaire, les heures supplémentaires, pour représenter qui fait partie de l'équipe, pour établir un lien avec les rapports sur le rendement, etc. Un employé peut également changer le prénom, le nom ou les deux, et rester les mêmes employé. Les employés peuvent même avoir plusieurs orthographes de leur nom, comme un ASCII et une orthographe unicode. Les gens peuvent avoir 0 à n prénom et/ou noms de famille. Ils peuvent avoir des noms différents dans différentes juridictions. - Le suivi de l'identité d'un employé est une responsabilité suffisante pour que la gestion des vacances ou des vacances ne puisse pas être ajoutée sans l'appeler une deuxième responsabilité.

5
Peter

"Je suis partisan de SOLID. Vous violez le principe de responsabilité unique (SRP) car vous représentez des données et exécutez de la logique dans la même classe."

Comme les autres, je ne suis pas d'accord avec cela.

Je dirais que le SRP est violé si vous effectuez plus d'un morceau de logique dans la classe. La quantité de données qui doit être stockée dans la classe pour atteindre cet élément logique n'est pas pertinente.

Je ne trouve pas utile de nos jours de débattre de ce qui constitue et ne constitue pas une seule responsabilité ou une seule raison de changer. Je proposerais un principe de peine minimum à sa place:

Principe du deuil minimum: le code doit chercher à minimiser sa probabilité d'exiger des changements ou à maximiser la facilité de changement.

Comment ça? Ne devrait pas prendre un spécialiste des fusées pour comprendre pourquoi cela peut aider à réduire les coûts de maintenance et, espérons-le, cela ne devrait pas être un sujet de débat sans fin, mais comme avec SOLID en général, ce n'est pas quelque chose à appliquer aveuglément partout. C'est quelque chose à considérer tout en équilibrant les compromis.

Quant à la probabilité de nécessiter des changements, elle diminue avec:

  1. Bon test (fiabilité améliorée).
  2. N'impliquer que le strict minimum requis pour faire quelque chose de spécifique (cela peut inclure la réduction des couplages afférents).
  3. Simplement rendre le code badass à ce qu'il fait (voir Principe Make Badass).

Quant à la difficulté de faire des changements, elle monte avec des couplages efférents. Les tests introduisent des couplages efférents mais améliorent la fiabilité. Bien fait, il fait généralement plus de bien que de mal et est totalement acceptable et promu par le principe du chagrin minimum.

Make Badass Principle: les classes qui sont utilisées dans de nombreux endroits devraient être géniales. Ils doivent être fiables, efficaces si cela est lié à leur qualité, etc.

Et le principe Make Badass est lié au principe Minimum Grief, car les choses badass trouveront une probabilité plus faible d'exiger des changements que les choses qui aspirent à ce qu'elles font.

J'aurais commencé par souligner le paradoxe mentionné ci-dessus, puis indiquer que le SRP dépend fortement du niveau de granularité que vous souhaitez prendre en compte et que si vous le prenez assez loin, toute classe contenant plus d'une propriété ou d'une méthode viole il.

D'un point de vue SRP, une classe qui ne fait pratiquement rien n'aurait certainement qu'une (parfois zéro) raison de changer:

class Float
{
public:
    explicit Float(float val);
    float get() const;
    void set(float new_val);
};

Cela n'a pratiquement aucune raison de changer! C'est mieux que SRP. C'est ZRP!

Sauf que je dirais que c'est en violation flagrante du principe Make Badass. C'est aussi absolument sans valeur. Quelque chose qui fait si peu ne peut pas être un dur à cuire. Il contient trop peu d'informations (TLI). Et naturellement, lorsque vous avez quelque chose qui est TLI, il ne peut rien faire de vraiment significatif, pas même avec les informations qu'il encapsule, donc il doit le divulguer au monde extérieur dans l'espoir que quelqu'un d'autre fasse réellement quelque chose de significatif et de dur à cuire. Et cette fuite est acceptable pour quelque chose qui est juste destiné à agréger des données et rien de plus, mais ce seuil est la différence que je vois entre "données" et "objets".

Bien sûr, quelque chose qui est TMI est également mauvais. Nous pourrions mettre tout notre logiciel dans une seule classe. Il ne peut même avoir qu'une seule méthode run. Et quelqu'un pourrait même affirmer qu'il a maintenant une raison très large de changer: "Cette classe ne devra être modifiée que si le logiciel a besoin d'être amélioré." Je suis stupide, mais bien sûr, nous pouvons imaginer tous les problèmes de maintenance avec cela.

Il y a donc un équilibre entre la granularité ou la grossièreté des objets que vous concevez. Je le juge souvent en fonction de la quantité d'informations que vous devez divulguer au monde extérieur et de la quantité de fonctionnalités utiles qu'il peut effectuer. Je trouve souvent le principe Make Badass utile pour trouver l'équilibre tout en le combinant avec le principe du chagrin minimum.

2
user204677

Au contraire, pour moi, le modèle de domaine anémique rompt certains OOP concepts principaux (qui relient les attributs et le comportement), mais peut être inévitable en fonction des choix architecturaux. Les domaines anémiques sont plus faciles à penser, moins organiques et plus séquentiel.

De nombreux systèmes ont tendance à le faire lorsque plusieurs couches doivent jouer avec les mêmes données (couche service, couche web, couche client, agents ...).

Il est plus facile de définir la structure des données à un endroit et le comportement dans les autres classes de service. Si la même classe a été utilisée sur plusieurs couches, celle-ci peut grossir et elle pose la question de savoir quelle couche est responsable de définir le comportement dont elle a besoin et qui est capable d'appeler les méthodes.

Par exemple, ce n'est peut-être pas une bonne idée qu'un processus d'agent qui calcule des statistiques sur tous vos employés puisse appeler un calcul pour les jours payés. Et l'interface graphique de la liste des employés n'a certainement pas besoin du tout du nouveau calcul d'ID agrégé utilisé dans cet agent statistique (et des données techniques qui l'accompagnent). Lorsque vous séparez les méthodes de cette façon, vous vous retrouvez généralement avec une classe avec uniquement des structures de données.

Vous pouvez trop facilement sérialiser/désérialiser les données "objet", ou juste certaines d'entre elles, ou vers un autre format (json) ... sans vous soucier du concept/de la responsabilité de l'objet. Ce ne sont que des données qui passent. Vous pouvez toujours faire un mappage de données entre deux classes (Employee, EmployeeVO, EmployeeStatistic…), mais que signifie réellement Employee ici?

Alors oui, il sépare complètement les données dans les classes de domaine et le traitement des données dans les classes de service, mais il est ici nécessaire. Un tel système est à la fois fonctionnel pour apporter de la valeur commerciale et technique pour propager les données partout où elles sont nécessaires tout en gardant une portée de responsabilité appropriée (et l'objet distribué ne résout pas cela non plus).

Si vous n'avez pas besoin de séparer les portées de comportement, vous êtes plus libre de placer les méthodes dans les classes de service ou dans les classes de domaine, selon la façon dont vous voyez votre objet. J'ai tendance à voir un objet comme le "vrai" concept, cela aide naturellement à garder SRP. Donc, dans votre exemple, il est plus réaliste que le patron de l'employé d'ajouter le jour de paie accordé à son compte PayDayAccount. Un employé est embauché par l'entreprise, Works, peut être malade ou demander un conseil, et il a un compte Payday (le patron peut le récupérer directement auprès de lui ou d'un registre PayDayAccount ...) Mais vous pouvez créer un raccourci agrégé ici pour plus de simplicité si vous ne voulez pas trop de complexité pour un logiciel simple.

1
Vince

Vous violez le principe de responsabilité unique (SRP) car vous représentez à la fois des données et exécutez la logique dans la même classe.

Cela me semble très raisonnable. Le modèle peut ne pas avoir de propriétés publiques s'il expose des actions. Il s'agit essentiellement d'une idée de séparation commande-requête. Veuillez noter que la commande aura un état privé à coup sûr.

0
Dmitry Nogin

Vous ne pouvez pas violer le principe de responsabilité unique parce que c'est juste un critère esthétique, pas une règle de la nature. Ne vous laissez pas induire en erreur par le nom à consonance scientifique et les lettres majuscules.

0
ZunTzu