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

64

Le principe de responsabilité unique est défini sur wikipedia comme suit :

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 partie des fonctionnalités fournies par le logiciel, responsabilité qui doit être entièrement encapsulée par la classe.

Si une classe ne devrait 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 voudrait dire que la classe aurait plus d'une responsabilité.

Tous les exemples que j'ai vus démontrant le principe de responsabilité unique utilisent un exemple de classe qui ne comporte qu'une 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é.

OIE
la source
11
Pourquoi un vote négatif? Cela semble être une question idéale pour SE.SE; la personne a fait des recherches sur le sujet et s'est efforcée de rendre la question très claire. Il mérite des votes positifs à la place.
Arseni Mourzenko le
19
Le vote négatif était probablement dû au fait qu'il s'agissait d'une question qui avait déjà été posée et à laquelle il avait déjà été répondu à plusieurs reprises, par exemple voir softwareengineering.stackexchange.com/questions/345018/… . À mon avis, cela n’ajoute pas de nouveaux aspects substantiels.
Hans-Martin Mosner
11
Double
Bart van Ingen Schenau
9
Ceci est tout simplement reductio ad absurdum. Si chaque classe ne disposait littéralement que d’une seule méthode, il n’y aurait aucune possibilité pour un programme de faire plus d’une chose.
Darrel Hoffman
6
@ DarelHoffman Ce n'est pas vrai. Si chaque classe était un foncteur avec seulement une méthode "call ()", alors vous venez d'émuler une programmation procédurale simple avec une programmation orientée objet. Vous pouvez toujours faire ce que vous auriez pu faire autrement, car une méthode "call ()" de la classe peut appeler beaucoup d'autres méthodes "call ()" de la classe.
Vaelus

Réponses:

29

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

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

Cette classe peut casser le principe de responsabilité unique. Non pas parce qu'il a deux fonctions, mais si le code doit getX()et getY()doit satisfaire différentes parties prenantes qui peuvent exiger un changement. Si le vice-président, M. X envoie un mémo indiquant que tous les nombres doivent être exprimés en nombres à virgule flottante et que la directrice de la comptabilité, Mme Y, insiste sur le fait que tous les chiffres examinés par son service doivent rester des nombres entiers, indépendamment de ce que M. X pense bien, cette classe aurait intérêt à avoir une seule idée de qui est responsable, car les choses sont sur le point de devenir confuses.

Si SRP avait été suivi, il serait clair si la classe Location contribue aux éléments exposés par M. X et son groupe. Expliquez clairement à quoi la classe est responsable et vous saurez quelle directive aura une incidence sur cette classe. S'ils ont tous les 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 la classe entière ne peut faire qu'une seule petite chose. Cela signifie que je ne devrais pas être capable 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 précise quelles méthodes appartiennent à la classe et lesquelles ne le sont pas.

Le PÉR d'Oncle Bob concerne plus la loi de Conway que la loi de Curly . Oncle Bob préconise d'appliquer la loi de Curly (faire une chose) à des fonctions et non à des 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 manière dont les informations d'une organisation circulent. Cela conduit à suivre le PÉR parce que vous ne vous souciez pas de ce dont vous n'avez jamais entendu parler.

"Un module devrait être responsable devant un, et un seul acteur"

Robert C Martin - Architecture propre

Les gens continuent de vouloir que SRP traite de toutes les raisons pour limiter la portée. Il y a plus de raisons de limiter la portée que SRP. Je limite encore la portée en insistant pour que la classe soit une abstraction pouvant prendre un nom qui garantisse que 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. Vous vous trompez 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 devrait concerner une seule idée unificatrice. Cette idée peut être large. L'idée pourrait être la persistance. Si certaines fonctions utilitaires de journalisation sont présentes, elles sont clairement déplacées. Peu importe si M. X est le seul à s'intéresser à ce code.

Le principe classique à appliquer ici s'appelle Séparation des préoccupations . Si vous séparez toutes vos préoccupations, on pourrait faire valoir que ce qui reste à un endroit donné 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 simplement que ce que l'Oncle Bob appelle une responsabilité n'est pas une préoccupation. Une responsabilité envers lui n'est pas une chose sur laquelle vous vous concentrez. C'est quelque chose qui peut vous forcer à changer. Vous pouvez vous concentrer sur une préoccupation tout en créant un code responsable pour différents groupes de personnes ayant différents programmes.

Peut-être que vous ne vous souciez pas de ça. 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" dans d'autres "une chose" jusqu'à ce que vous ayez un tiroir pour ordures plein de tout. J'ai parlé à ce sujet avant

Bien entendu, la raison classique pour limiter la portée de la programmation orientée objet est que la classe contient des champs privés et que, plutôt que d'utiliser des accesseurs pour partager ces données, nous plaçons toutes les méthodes nécessitant ces données dans la classe où elles peuvent utiliser les données en privé. Nombreux sont ceux qui trouvent cela trop restrictif à utiliser comme limiteur de portée, car toutes les méthodes qui appartiennent à la même classe n'utilisent pas exactement les mêmes champs. J'aime m'assurer que toute idée qui a réuni les données soit la même que celle qui a rassemblé les méthodes.

La manière fonctionnelle de regarder ceci est que a.f(x)et a.g(x)sont simplement f a (x) et g a (x). Pas deux fonctions mais un continuum de paires de fonctions qui varient ensemble. Le an'a même pas besoin d'y avoir de données. Il pourrait tout simplement savoir comment vous savez qui fet la gmise en œuvre que vous allez utiliser. Les fonctions qui changent ensemble vont de pair. C'est bon vieux polymorphisme.

SRP n'est qu'une des nombreuses raisons pour limiter la portée. C'est un bon. Mais pas le seul.

confits_orange
la source
25
Je pense que cette réponse est déconcertante pour quelqu'un qui essaie de comprendre le PÉR. La bataille entre Monsieur le Président et Madame la Directrice n’est pas résolue par des moyens techniques et son utilisation pour justifier une décision technique n’est pas raisonnable. La loi de Conway en action.
Whatsisname
8
@whatsisname Au contraire. Le PÉR était explicitement destiné à s'appliquer aux parties prenantes. Cela n'a rien à voir avec la conception technique. Vous pouvez être en désaccord avec cette approche, mais c’est ainsi que l’oncle Bob a défini le SRP à l’origine, et il a dû le répéter encore et encore car, pour une raison quelconque, les gens ne semblent pas en mesure de comprendre cette simple notion (esprit, c'est en fait utile, c'est une question complètement orthogonale).
Luaan
La loi de Curly, telle que décrite par Tim Ottinger, souligne qu'une variable doit toujours avoir une signification . Pour moi, SRP est un peu plus fort que cela; une classe peut conceptuellement représenter «une chose», mais enfreindre le SRP si deux facteurs externes de changement traitent un aspect de cette «une chose» de manière différente ou s’inquiètent de deux aspects différents. Le problème est celui de la modélisation. vous avez choisi de modéliser quelque chose comme une classe unique, mais il y a quelque chose dans le domaine qui le rend problématique (les choses commencent à vous gêner à mesure que la base de code évolue).
Filip Milovanović
2
@ FilipMilovanović La similitude que je vois entre la loi de Conway et le SRP, comme l'a expliqué Oncle Bob, dans son livre sur l'architecture sans compromis, s'appuie sur l'hypothèse selon laquelle l'organisation possède un organigramme acyclique propre. C'est une vieille idée. Même la Bible cite ici: "Aucun homme ne peut servir deux maîtres".
candied_orange
1
@TKK im le mettant en relation (sans l'assimiler) à la loi de Conways et non à la loi de Curly. Je réfute l'idée selon laquelle SRP est la loi de Curly, principalement parce que l'oncle Bob l'a dit lui-même dans son livre Clean Architecture.
candied_orange
48

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 divisée en parties de fonctionnalité, chaque partie étant une méthode.

Voici un exemple. Imaginez que vous deviez créer un fichier CSV à partir d'une séquence. Si vous souhaitez vous conformer à la norme RFC 4180, l'implémentation de l'algorithme et le traitement de tous les cas extrêmes prendraient un certain temps.

Le faire en une seule méthode donnerait un code qui ne serait pas particulièrement lisible, et surtout, la méthode ferait plusieurs choses à la fois. Par conséquent, vous allez le scinder en plusieurs méthodes. par exemple, l’un d’eux peut être chargé de générer l’en-tête, c’est-à-dire la toute première ligne du fichier CSV, alors qu’une autre méthode convertirait une valeur de tout type en une représentation sous forme de chaîne adaptée au format CSV, tandis qu’une autre déterminerait si 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 ou non des guillemets a la sienne et la méthode qui génère l’en-tête en a une. Ceci est SRP appliqué aux méthodes .

Désormais, toutes ces méthodes ont un objectif en commun: prendre une séquence et générer le fichier CSV. C'est la seule responsabilité de la classe .


Pablo H a commenté:

Bel exemple, mais j’ai le sentiment que cela ne dit toujours pas pourquoi SRP permet à une classe d’avoir plusieurs méthodes publiques.

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 une file d'attente, implémentée par une Queueclasse. Cette classe contiendrait, en gros, deux méthodes: push(également appelée enqueue) et pop(aussi appelée dequeue).

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

  • La responsabilité de Queue.popconsiste à retirer un objet de la tête de la file et à gérer le cas où la file est vide.

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

Arseni Mourzenko
la source
1
Bel exemple, mais j’ai le sentiment que cela ne dit toujours pas pourquoi SRP permet à une classe d’avoir plus d’une méthode publique .
Pablo H
1
@PabloH: juste. J'ai ajouté un autre exemple dans lequel une classe a deux méthodes.
Arseni Mourzenko
30

Une fonction est une fonction.

Une responsabilité est une responsabilité.

Un mécanicien a la responsabilité de réparer les voitures, ce qui implique des diagnostics, des tâches de maintenance simples, des travaux de réparation, 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 leur insertion, de fournir un accès, une sorte de commande, etc.

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

Peter
la source
2
D'accord. @Aulis Ronkainen - pour lier les deux réponses. Et pour les responsabilités imbriquées, en utilisant votre analogie mécanique, un garage a la responsabilité de la maintenance des véhicules. différents mécaniciens du garage
assument
2
@wolfsshield, d'accord. Mécanique qui ne fait qu'une chose est inutile, mais mécanique qui n'a qu'une seule responsabilité ne l'est pas (du moins nécessairement). Bien que les analogies de la vie réelle ne soient pas toujours les meilleures pour décrire des concepts abstraits de POO, il est important de distinguer ces différences. Je crois que ne pas comprendre la différence est ce qui crée la confusion en premier lieu.
Aulis Ronkainen
3
@AulisRonkainen Même si cela ressemble, sent et ressemble à une analogie, j’ai eu l’intention d’utiliser ce mécanisme pour mettre en évidence le sens spécifique du terme responsabilité dans SRP. Je suis complètement d'accord avec votre réponse.
Peter
20

La responsabilité unique ne signifie pas nécessairement qu'il 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 fournit un accès aux enregistrements d'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ésif, ce à quoi SRP essaie vraiment de vous faire réfléchir.

SRP ne doit pas être confondu avec le "principe de séparation des interfaces" (voir SOLID ). Selon le principe de séparation des interfaces (ISP), des interfaces plus petites et plus légères sont préférables à des interfaces plus grandes et plus généralisées. Go utilise beaucoup le fournisseur de services Internet dans 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 certes liés, mais l’un n’implique pas l’autre. ISP est au niveau de l'interface et SRP au niveau de la classe. Si une classe implémente plusieurs interfaces simples, elle ne peut plus avoir qu'une seule responsabilité.

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

Jesse
la source
3
En fait, vous décrivez le principe de séparation des interfaces (le "I" dans SOLID). SRP est une bête assez différente.
Luaan
En passant, quelle convention de codage utilisez-vous ici? J'attendre les objets UserService et Userà être UpperCamelCase, mais les méthodes Create , Existset Updateje l' aurais fait lowerCamelCase.
KlaymenDK
1
@KlaymenDK Vous avez raison, la majuscule n'est qu'une habitude d'utiliser Go (majuscule = exporté / public, minuscule = privé)
Jesse
@Luaan Merci de l'avoir signalé, je vais clarifier ma réponse
Jesse
1
@KlaymenDK De nombreux langages utilisent PascalCase pour les méthodes et les classes. C # par exemple.
Omegastick
15

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 cent autres choses. Souhaitez-vous embaucher un chef par plat sur votre menu? Ou un chef pour chaque composant de chaque plat? Ou un chef qui peut assumer sa seule responsabilité: cuisiner?

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

gnasher729
la source
4

Contre-exemple: stocker l'état mutable.

Supposons que vous ayez la classe la plus simple de tous les temps, dont le seul travail est de stocker un fichier 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 un setState(), ou un getState(), sauf si vous cassez l'encapsulation et rendez ipublic.

  • Un passeur est inutile sans un getter (vous ne pourriez jamais lire l'information)
  • Un getter est inutile sans un setter (vous ne pouvez jamais modifier l'information).

Il est donc clair que cette responsabilité unique nécessite d’ avoir au moins 2 méthodes sur cette classe. QED.

Alexandre
la source
4

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

La responsabilité unique n'équivaut pas à une seule méthode. Ils signifient différentes choses. En développement logiciel, on parle de cohésion . Les fonctions (méthodes) qui ont une grande cohésion "appartiennent" ensemble et peuvent être considérées comme une seule responsabilité.

Il appartient au développeur de concevoir le système de manière à respecter le principe de responsabilité unique. On peut voir cela comme une technique d'abstraction et est donc parfois une question d'opinion. La mise en œuvre du principe de responsabilité unique facilite principalement le test et la compréhension de son architecture et de sa conception.

Aulis Ronkainen
la source
2

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

Ainsi, considérez que la responsabilité d’une classe est de maintenir l’intégrité des données et d’aider à les utiliser correctement. Clairement, cela est plus facile à faire si tout le code est dans une classe, plutôt que sur plusieurs classes. L'ajout de deux points est fait de manière plus fiable et le code est plus facilement maintenu, avec une Point add(Point p)méthode dans la Pointclasse 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 Pointdoit se situer dans un plan (0,0) à (127,127), le constructeur et toutes les méthodes qui modifient ou produisent une nouvelle Pointont la responsabilité de vérifier les valeurs qui leur sont données et de rejeter toute modification qui violerait ce principe. exigence. (Souvent, quelque chose comme un Pointserait immuable, et s'assurer qu'il n'y avait aucun moyen de modifier un Pointaprès sa construction serait alors aussi une responsabilité de la classe)

Notez que la superposition ici est parfaitement acceptable. Vous pourriez avoir une Pointclasse pour traiter des points individuels et une Polygonclasse pour traiter un ensemble de Points; ceux-ci ont toujours des responsabilités distinctes, car les Polygondélégués assument toute la responsabilité de s'occuper de tout ce qui a un rapport avec Point(par exemple, s'assurer qu'un point a à la fois une xet une yvaleur) pour la Pointclasse.

Curt J. Sampson
la source