web-dev-qa-db-fra.com

Comment une classe peut-elle avoir plusieurs méthodes sans enfreindre le principe de responsabilité unique

Le principe de responsabilité unique est défini sur wikipedia as

Le principe de responsabilité unique est un principe de programmation informatique qui stipule que chaque module, classe ou fonction doit avoir la responsabilité d'une seule partie des fonctionnalités fournies par le logiciel, et que la responsabilité doit être entièrement encapsulée par la classe

Si une classe ne doit avoir qu'une seule responsabilité, comment peut-elle avoir plus d'une méthode? Chaque méthode n'aurait-elle pas une responsabilité différente, ce qui signifierait alors que la classe aurait plus d'une responsabilité.

Chaque exemple que j'ai vu démontrer le principe de responsabilité unique utilise un exemple de classe qui n'a qu'une seule méthode. Il peut être utile de voir un exemple ou d'avoir une explication d'une classe avec plusieurs méthodes qui peuvent toujours être considérées comme ayant une seule responsabilité.

68
Goose

La responsabilité unique n'est peut-être pas quelque chose qu'une seule fonction peut remplir.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Cette classe peut enfreindre le principe de responsabilité unique. Non pas parce qu'il a deux fonctions, mais si le code de getX() et getY() doit satisfaire différentes parties prenantes qui peuvent exiger des changements. Si le vice-président, M. X, envoie une note de service indiquant que tous les nombres doivent être exprimés en nombres à virgule flottante et la directrice de la comptabilité, Mme Y, insiste pour que tous les nombres que son département examine restent des nombres entiers, peu importe ce que M. X pense bien, alors cette classe aurait mieux à avoir. une seule idée de qui il est responsable parce que les choses sont sur le point de devenir confuses.

Si SRP avait été suivi, il serait clair si la classe Location contribue aux choses auxquelles Mr X et son groupe sont exposés. Indiquez clairement les responsabilités de la classe et vous savez quelle directive affecte cette classe. S'ils ont tous deux un impact sur cette classe, elle a été mal conçue pour minimiser l'impact du changement. "Une classe ne devrait avoir qu'une seule raison de changer" ne signifie pas que toute la classe ne peut jamais faire qu'une seule petite chose. Cela signifie que je ne devrais pas être en mesure de regarder la classe et de dire que M. X et Mme Y ont un intérêt pour cette classe.

Autre que des choses comme ça. Non, plusieurs méthodes conviennent. Donnez-lui simplement un nom qui indique clairement les méthodes qui appartiennent à la classe et celles qui ne le sont pas.

Le SRP de l'oncle Bob concerne plus la loi de Conway que la loi de Curly . L'oncle Bob préconise d'appliquer la loi de Curly (faire une chose) aux fonctions et non aux classes. SRP met en garde contre le mélange des raisons de changer ensemble. La loi de Conway stipule que le système suivra la façon dont les informations d'une organisation circulent. Cela conduit à suivre SRP parce que vous ne vous souciez pas de ce dont vous n'avez jamais entendu parler.

"Un module doit être responsable devant un et un seul acteur"

Robert C Martin - Architecture propre

Les gens continuent de vouloir que la SRP soit à peu près toutes les raisons de limiter la portée. Il y a plus de raisons de limiter la portée que SRP. Je limite davantage la portée en insistant sur le fait que la classe doit être une abstraction qui peut prendre un nom qui garantit regarder à l'intérieur ne vous surprendra pas .

Vous pouvez appliquer la loi de Curly aux cours. Vous êtes en dehors de ce dont parle Oncle Bob, mais vous pouvez le faire. Lorsque vous vous trompez, c'est lorsque vous commencez à penser que cela signifie une fonction. C'est comme penser qu'une famille ne devrait avoir qu'un seul enfant. Avoir plus d'un enfant ne l'empêche pas d'être une famille.

Si vous appliquez la loi de Curly à une classe, tout dans la classe doit concerner une seule idée unificatrice. Cette idée peut être large. L'idée pourrait être la persistance. Si certaines fonctions de l'utilitaire de journalisation sont présentes, elles sont clairement hors de propos. Peu importe si M. X est le seul à se soucier de ce code.

Le principe classique à appliquer ici est appelé Séparation des préoccupations . Si vous séparez toutes vos préoccupations, on pourrait faire valoir que ce qui reste à un seul endroit est une préoccupation. C'est ce que nous appelions cette idée avant que le film City Slickers de 1991 ne nous présente le personnage Curly.

C'est bon. C'est juste que ce que l'oncle Bob appelle une responsabilité n'est pas un problème. Une responsabilité envers lui n'est pas quelque chose sur laquelle vous vous concentrez. C'est quelque chose qui peut vous forcer à changer. Vous pouvez vous concentrer sur une seule préoccupation tout en créant du code responsable pour différents groupes de personnes avec des programmes différents.

Peut-être que vous ne vous en souciez pas. Bien. Penser que le fait de "faire une chose" résoudra tous vos problèmes de conception montre un manque d'imagination de ce qu'une "chose" peut finir par être. Une autre raison de limiter la portée est l'organisation. Vous pouvez imbriquer plusieurs "une chose" à l'intérieur d'autres "une seule chose" jusqu'à ce que vous ayez un tiroir à ordures plein de tout. J'en ai parlé avant

Bien sûr, la raison classique OOP pour limiter la portée est que la classe contient des champs privés et plutôt que d'utiliser des getters pour partager ces données, nous plaçons chaque méthode qui a besoin de ces données dans la classe où ils peuvent utiliser les données en privé. Beaucoup trouvent cela trop restrictif pour être utilisé comme limiteur de portée car toutes les méthodes qui appartiennent ensemble n'utilisent pas exactement les mêmes champs. J'aime m'assurer que toute idée qui a rassemblé les données soit la même que celle qui a amené les méthodes ensemble.

La façon fonctionnelle de voir cela est que a.f(x) et a.g(x) sont simplement fune(x) et gune(X). Pas deux fonctions mais un continuum de paires de fonctions qui varient ensemble. Le a n'a même pas besoin de contenir de données. Cela pourrait simplement être la façon dont vous savez quelle implémentation f et g vous allez utiliser. Les fonctions qui changent ensemble vont de pair. C'est du bon vieux polymorphisme.

SRP n'est qu'une des nombreuses raisons de limiter la portée. C'est une bonne chose. Mais pas le seul.

30
candied_orange

La clé ici est la portée , ou, si vous préférez, la granularité . Une partie de la fonctionnalité représentée par une classe peut être encore séparée en parties de fonctionnalité, chaque partie étant une méthode.

Voici un exemple. Imaginez que vous devez créer un CSV à partir d'une séquence. Si vous voulez être conforme à la RFC 4180, il faudrait un certain temps pour implémenter l'algorithme et gérer tous les cas Edge.

Le faire dans une seule méthode entraînerait un code qui ne serait pas particulièrement lisible, et surtout, la méthode ferait plusieurs choses à la fois. Par conséquent, vous le diviserez en plusieurs méthodes; par exemple, l'un d'eux peut être en charge de générer l'en-tête, c'est-à-dire la toute première ligne du CSV, tandis qu'une autre méthode convertirait une valeur de n'importe quel type en sa représentation sous forme de chaîne adaptée au format CSV, tandis qu'une autre déterminerait si un la valeur doit être placée entre guillemets.

Ces méthodes ont leur propre responsabilité. La méthode qui vérifie s'il est nécessaire d'ajouter des guillemets doubles ou non a la sienne, et la méthode qui génère l'en-tête en a une. Il s'agit du SRP appliqué aux méthodes .

Maintenant, toutes ces méthodes ont un objectif en commun, c'est-à-dire prendre une séquence et générer le CSV. C'est la seule responsabilité de la classe .


Pablo H a commenté:

Bel exemple, mais je pense qu'il ne répond toujours pas pourquoi SRP autorise une classe à avoir plus d'une méthode publique.

En effet. L'exemple CSV que j'ai donné a idéalement une méthode publique et toutes les autres méthodes sont privées. Un meilleur exemple serait celui d'une file d'attente, implémentée par une classe Queue. Cette classe contiendrait essentiellement deux méthodes: Push (également appelée enqueue) et pop (également appelée dequeue).

  • La responsabilité de Queue.Push consiste à ajouter un objet à la queue de la file d'attente.

  • La responsabilité de Queue.pop consiste à supprimer un objet de la tête de la file d'attente et à gérer le cas où la file d'attente est vide.

  • La responsabilité de la classe Queue est de fournir une logique de file d'attente.

51
Arseni Mourzenko

Une fonction est une fonction.

Une responsabilité est une responsabilité.

Un mécanicien a la responsabilité de réparer les voitures, ce qui impliquera des diagnostics, des tâches de maintenance simples, des travaux de réparation réels, une délégation de tâches à d'autres, etc.

Une classe de conteneur (liste, tableau, dictionnaire, carte, etc.) a la responsabilité de stocker des objets, ce qui implique de les stocker, de permettre l'insertion, de fournir un accès, une sorte de classement, etc.

Une seule responsabilité ne signifie pas qu'il y a très peu de code/fonctionnalité, cela signifie que toute fonctionnalité qui existe "appartient ensemble" sous la même responsabilité.

31
Peter

La responsabilité unique ne signifie pas nécessairement qu'elle ne fait qu'une chose.

Prenons par exemple une classe de service utilisateur:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

Cette classe a plusieurs méthodes mais sa responsabilité est claire. Il permet d'accéder aux enregistrements utilisateur dans le magasin de données. Ses seules dépendances sont le modèle utilisateur et le magasin de données. Il est faiblement couplé et très cohérent, ce qui est vraiment ce à quoi SRP essaie de vous faire réfléchir.

SRP ne doit pas être confondu avec le "principe de ségrégation d'interface" (voir SOLID ). Le principe de ségrégation d'interface (ISP) dit que les interfaces plus petites et légères sont préférables aux interfaces plus généralisées et plus généralisées. Go fait un usage intensif du FAI dans toute sa bibliothèque standard:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP et ISP sont certainement liés, mais l'un n'implique pas l'autre. ISP est au niveau de l'interface et SRP est au niveau de la classe. Si une classe implémente plusieurs interfaces simples, elle peut ne plus avoir qu'une seule responsabilité.

Merci à Luaan d'avoir souligné la différence entre ISP et SRP.

20
Jesse

Il y a un chef dans un restaurant. Sa seule responsabilité est de cuisiner. Pourtant, il peut cuisiner des steaks, des pommes de terre, du brocoli et des centaines d'autres choses. Embaucheriez-vous un chef par plat dans votre menu? Ou un chef pour chaque composant de chaque plat? Ou un chef qui peut assumer sa seule responsabilité: cuisiner?

Si vous demandez également à ce chef de faire la paie, c'est lorsque vous violez le PÉR.

15
gnasher729

Vous interprétez mal le principe de la responsabilité unique.

La responsabilité unique n'équivaut pas à une méthode unique. Ils signifient des choses différentes. Dans le développement de logiciels, nous parlons de cohésion . Les fonctions (méthodes) à forte cohésion "appartiennent" ensemble et peuvent être considérées comme exécutant une seule responsabilité.

Il appartient au développeur de concevoir le système afin que le principe de responsabilité unique soit respecté. On peut y voir une technique d'abstraction et c'est donc parfois une question d'opinion. La mise en œuvre du principe de responsabilité unique rend le code principalement plus facile à tester et à comprendre son architecture et sa conception.

4
Aulis Ronkainen

Contre-exemple: stockage de l'état mutable.

Supposons que vous disposiez de la classe la plus simple de tous les temps, dont le seul travail consiste à stocker un int.

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Si vous étiez limité à une seule méthode, vous pourriez avoir soit une setState(), soit une getState(), sauf si vous interrompez l'encapsulation et rendez i public.

  • Un setter est inutile sans getter (on ne pourrait jamais lire les informations)
  • Un getter est inutile sans setter (vous ne pouvez jamais muter les informations).

Donc, clairement, cette seule responsabilité nécessite avoir au moins 2 méthodes sur cette classe. QED.

Il est souvent utile (dans n'importe quelle langue, mais surtout dans OO langues) de regarder les choses et de les organiser du point de vue des données plutôt que des fonctions.

Considérez donc que la responsabilité d'une classe consiste à maintenir l'intégrité et à fournir une aide pour utiliser correctement les données qu'elle possède. De toute évidence, cela est plus facile à faire si tout le code est dans une classe, plutôt que réparti sur plusieurs classes. L'ajout de deux points est plus fiable et le code est plus facile à gérer, avec une méthode Point add(Point p) dans la classe Point que d'avoir cela ailleurs.

Et en particulier, la classe ne doit rien exposer qui pourrait entraîner des données incohérentes ou incorrectes. Par exemple, si un Point doit se trouver dans un plan (0,0) à (127,127), le constructeur et toutes les méthodes qui modifient ou produisent un nouveau Point ont la responsabilité de vérifier les valeurs ils sont donnés et refusent tout changement qui violerait cette exigence. (Souvent, quelque chose comme un Point serait immuable, et s'assurer qu'il n'y a aucun moyen de modifier un Point après sa construction serait alors également une responsabilité de la classe)

Notez que la superposition ici est parfaitement acceptable. Vous pourriez avoir une classe Point pour traiter des points individuels et une classe Polygon pour traiter un ensemble de Points; ceux-ci ont toujours des responsabilités distinctes parce que Polygon délègue toute la responsabilité de traiter quoi que ce soit uniquement avec un Point (comme s'assurer qu'un point a à la fois un x et un y value) à la classe Point.

2
cjs