web-dev-qa-db-fra.com

Principe de responsabilité unique - Comment éviter la fragmentation du code?

Je travaille dans une équipe où le chef d'équipe est un ardent défenseur des principes de développement de SOLID. Cependant, il n'a pas beaucoup d'expérience dans la mise au point de logiciels complexes.

Nous avons une situation où il a appliqué SRP à ce qui était déjà une base de code assez complexe, qui est maintenant devenue très fragmentée et difficile à comprendre et à déboguer.

Nous avons maintenant un problème non seulement avec la fragmentation du code, mais aussi avec l'encapsulation, car les méthodes au sein d'une classe qui peuvent avoir été privées ou protégées ont été jugées comme représentant une "raison de changer" et ont été extraites vers des classes et interfaces publiques ou internes qui n'est pas conforme aux objectifs d'encapsulation de l'application.

Nous avons certains constructeurs de classes qui prennent en charge plus de 20 paramètres d'interface, donc notre enregistrement et résolution IoC devient un monstre à part entière.

Je veux savoir s'il existe une approche "refactor away from SRP" que nous pourrions utiliser pour résoudre certains de ces problèmes. J'ai lu que cela ne viole pas SOLID si je crée un certain nombre de classes vides à grain grossier qui "encapsulent" un certain nombre de classes étroitement liées pour fournir un point d'accès unique au somme de leurs fonctionnalités (c'est-à-dire imitant une implémentation de classe moins exagérément SRP).

En dehors de cela, je ne pense pas à une solution qui nous permette de poursuivre de manière pragmatique nos efforts de développement, tout en satisfaisant tout le monde.

Aucune suggestion ?

59
Dean Chalk

Si votre classe a 20 paramètres dans le constructeur, il ne semble pas que votre équipe sache exactement ce qu'est SRP. Si vous avez une classe qui ne fait qu'une chose, comment a-t-elle 20 dépendances? C'est comme partir en voyage de pêche et apporter une canne à pêche, une boîte à pêche, des fournitures de courtepointe, une boule de bowling, des nunchucks, un lance-flammes, etc.

Cela dit, la SRP, comme la plupart des principes, peut être sur-appliquée. Si vous créez une nouvelle classe pour incrémenter des entiers, alors oui, cela peut être une seule responsabilité, mais allez. C'est ridicule. Nous avons tendance à oublier que des choses comme les principes SOLID sont là dans un but. SOLID est un moyen pour une fin, pas une fin en soi. La fin est maintenabilité. Si vous voulez obtenir cette granularité avec le principe de responsabilité unique, c'est un indicateur que le zèle pour SOLID a aveuglé l'équipe à l'objectif de SOLID.

Donc, je suppose que ce que je dis est ... Le SRP n'est pas votre problème. C'est soit une mauvaise compréhension du SRP, soit une application incroyablement granulaire de celui-ci. Essayez de faire en sorte que votre équipe garde l'essentiel. Et l'essentiel est la maintenabilité.

ÉDITER

Amener les gens à concevoir des modules d'une manière qui encourage la facilité d'utilisation. Considérez chaque classe comme une mini API. Pensez d'abord, "Comment voudrais-je utiliser cette classe", puis implémentez-la. Ne pensez pas seulement "Que doit faire cette classe?" Le SRP a une grande tendance à rendre les classes plus difficiles à utiliser, si vous ne pensez pas beaucoup à la convivialité.

EDIT 2

Si vous cherchez des conseils sur la refactorisation, vous pouvez commencer à faire ce que vous avez suggéré - créer des classes plus grossières pour en regrouper plusieurs autres. Assurez-vous que la classe à grain grossier adhère toujours au SRP , mais à un niveau supérieur. Ensuite, vous avez deux alternatives:

  1. Si les classes à grains plus fins ne sont plus utilisées ailleurs dans le système, vous pouvez progressivement extraire leur implémentation dans la classe à grains plus fins et les supprimer.
  2. Laissez les classes plus fines tranquilles. Peut-être qu'ils étaient bien conçus et que vous aviez juste besoin de l'emballage pour les rendre plus faciles à utiliser. Je soupçonne que c'est le cas pour une grande partie de votre projet.

Lorsque vous avez terminé la refactorisation (mais avant de vous engager dans le référentiel), passez en revue votre travail et demandez-vous si votre refactoring était en fait une amélioration de la maintenabilité et de la facilité d'utilisation.

87
Phil

Je pense que c'est dans le refactoring de Martin Fowler que j'ai lu une fois une contre-règle à SRP, définissant où cela va trop loin. Il y a une deuxième question, aussi importante que "chaque classe a-t-elle une seule raison de changer?" et c'est "chaque changement affecte-t-il seulement une classe?"

Si la réponse à la première question est, dans tous les cas, "oui" mais que la deuxième question n'est "même pas proche", alors vous devez réexaminer la façon dont vous implémentez SRP.

Par exemple, si l'ajout d'un champ à une table signifie que vous devez modifier un DTO et une classe de validateur et une classe de persistance et un objet de modèle de vue, etc., alors vous avez créé un problème. Vous devriez peut-être repenser la façon dont vous avez implémenté SRP.

Vous avez peut-être dit que l'ajout d'un champ est la raison de changer l'objet Client, mais le changement de la couche de persistance (par exemple d'un fichier XML à une base de données) est une autre raison de changer l'objet Client. Vous décidez donc également de créer un objet CustomerPersistence. Mais si vous le faites de telle sorte que l'ajout d'un champ ENCORE nécessite une modification de l'objet CustomerPersisitence, alors quel était le point? Vous avez toujours un objet avec deux raisons de changer - il n'est tout simplement plus client.

Cependant, si vous introduisez un ORM, il est fort possible que vous puissiez faire fonctionner les classes de telle sorte que si vous ajoutez un champ au DTO, il changera automatiquement le SQL utilisé pour lire ces données. Vous avez alors de bonnes raisons de séparer les deux préoccupations.

En résumé, voici ce que j'ai tendance à faire: s'il y a un équilibre approximatif entre le nombre de fois que je dis "non, il y a plus d'une raison de changer cet objet" et le nombre de fois que je dis "non, ce changement affecter plus d'un objet ", alors je pense que j'ai le juste équilibre entre SRP et la fragmentation. Mais si les deux sont encore élevés, je commence à me demander s'il existe une manière différente de séparer les préoccupations.

33
pdr

Tout simplement parce qu'un système est complexe ne signifie pas que vous devez le compliquer . Si vous avez une classe qui a trop de dépendances (ou de collaborateurs) comme celle-ci:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... alors c'est devenu trop compliqué et vous ne suivez pas vraiment SRP , n'est-ce pas? Je parierais que si vous notiez ce que MyAwesomeClass fait sur une carte CRC elle ne tiendrait pas sur une fiche ou si vous devez écrire de minuscules lettres illisibles.

Ce que vous avez ici, c'est que vos gars ont uniquement suivi le Principe de ségrégation d'interface et l'ont peut-être poussé à l'extrême, mais c'est une toute autre histoire. Vous pourriez faire valoir que les dépendances sont des objets de domaine (ce qui arrive), mais avoir une classe qui gère 20 objets de domaine en même temps l'étire un peu trop.

TDD vous fournira un bon indicateur de combien une classe fait. En termes simples; si une méthode de test a un code d'installation qui prend une éternité à écrire (même si vous refactorisez les tests), votre MyAwesomeClass a probablement trop de choses à faire.

Alors, comment résolvez-vous cette énigme? Vous déplacez les responsabilités vers d'autres classes. Vous pouvez suivre certaines étapes pour une classe qui a ce problème:

  1. Identifiez toutes les actions (ou responsabilités) que votre classe fait avec ses dépendances.
  2. Regroupez les actions en fonction de dépendances étroitement liées.
  3. Redéléguer! C'est-à-dire refactoriser chacune des actions identifiées vers de nouvelles classes ou (plus important encore) vers d'autres classes.

Un exemple abstrait sur la refactorisation des responsabilités

Soit C une classe qui a plusieurs dépendances D1, D2, D3, D4 Que vous devez refactoriser pour utiliser moins. Lorsque nous identifions les méthodes que C appelle sur les dépendances, nous pouvons en faire une simple liste:

  • D1 - performA(D2), performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

En regardant la liste, nous pouvons voir que D1 Et D2 Sont liés l'un à l'autre car la classe en a besoin ensemble. Nous pouvons également voir que D4 A besoin de D3. Nous avons donc deux regroupements:

  • Group 1 - D1 <-> D2
  • Group 2 - D4 -> D3

Les regroupements indiquent que la classe a maintenant deux responsabilités.

  1. Group 1 - Un pour gérer les deux objets appelants qui ont besoin l'un de l'autre. Vous pouvez peut-être laisser votre classe C éliminer la nécessité de gérer les deux dépendances et laisser l'une d'entre elles gérer ces appels à la place. Dans ce regroupement, il est évident que D1 Pourrait avoir une référence à D2.
  2. Group 2 - L'autre responsabilité a besoin d'un objet pour en appeler un autre. D4 Ne peut-il pas gérer D3 À la place de votre classe? Ensuite, nous pouvons probablement éliminer D3 De la classe C en laissant D4 Faire les appels à la place.

Ne prenez pas ma réponse comme gravée dans la pierre car l'exemple est très abstrait et fait beaucoup d'hypothèses. Je suis à peu près sûr qu'il existe d'autres façons de refactoriser cela, mais au moins les étapes pourraient vous aider à obtenir une sorte de processus pour déplacer les responsabilités au lieu de diviser les classes.


Éditer:

Parmi les commentaires @ Emmad Karem dit:

"Si votre classe a 20 paramètres dans le constructeur, il ne semble pas que votre équipe sache exactement ce qu'est SRP. Si vous avez une classe qui ne fait qu'une chose, comment a-t-elle 20 dépendances?" - Je pense que si vous avoir une classe Customer, il n'est pas étrange d'avoir 20 paramètres dans le constructeur.

Il est vrai que les objets DAO ont tendance à avoir beaucoup de paramètres, que vous devez définir dans votre constructeur, et les paramètres sont généralement des types simples tels que chaîne. Cependant, dans l'exemple d'une classe Customer, vous pouvez toujours regrouper ses propriétés dans d'autres classes pour simplifier les choses. Comme avoir une classe Address avec des rues et une classe Zipcode qui contient le code postal et gérera également la logique métier telle que la validation des données:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

Cette chose est discutée plus loin dans le billet de blog "Jamais, jamais, jamais utiliser String dans Java (ou au moins souvent)") . Comme alternative d'utiliser des constructeurs ou des statiques Pour rendre les sous-objets plus faciles à créer, vous pouvez utiliser un modèle de générateur de fluide .

24
Spoike

La réponse est la maintenabilité et la clarté du code avant tout. Pour moi, cela signifie écrire moins de code, pas plus. Moins d'abstractions, moins d'interfaces, moins d'options, moins de paramètres.

Chaque fois que j'évalue une restructuration de code ou que j'ajoute une nouvelle fonctionnalité, je pense à la quantité de passe-partout qui sera nécessaire par rapport à la logique réelle. Si la réponse est supérieure à 50%, cela signifie probablement que j'y réfléchis trop.

En plus de SRP, il existe de nombreux autres styles de développement. Dans votre cas, ses sons comme YAGNI font définitivement défaut.

3
cmcginty

Beaucoup de réponses ici sont vraiment bonnes mais se concentrent sur le côté technique de ce problème. J'ajouterai simplement que cela ressemble aux tentatives du développeur de suivre le son SRP comme si elles violaient réellement le SRP.

Vous pouvez voir le blog de Bob ici sur cette situation, mais il fait valoir que si une responsabilité est étalée sur plusieurs classes, alors la responsabilité SRP est violée car ces classes changent en parallèle. Je soupçonne que votre développeur aimerait vraiment le design en haut du blog de Bob, et pourrait être un peu déçu de le voir déchiré. En particulier parce qu'elle viole le "principe de fermeture commun" - les choses qui changent ensemble restent ensemble.

N'oubliez pas que le SRP fait référence à la "raison du changement" et non à "faire une chose", et que vous n'avez pas besoin de vous préoccuper de cette raison du changement jusqu'à ce qu'un changement se produise réellement. Le deuxième type paie pour l'abstraction.

Maintenant, il y a le deuxième problème - le "défenseur virulent du développement SOLID." Il ne semble pas que vous ayez une excellente relation avec ce développeur, donc toute tentative de le convaincre de la les problèmes dans la base de code sont perplexes. Vous devrez réparer la relation afin que vous puissiez avoir une vraie discussion sur les problèmes. Ce que je recommanderais, c'est la bière.

Non sérieusement - si vous ne buvez pas, rendez-vous dans un café. Sortez du bureau et détendez-vous, où vous pourrez parler de ces choses de manière informelle. Plutôt que d'essayer de gagner un argument lors d'une réunion, ce que vous n'aurez pas, ayez une discussion quelque part amusante. Essayez de reconnaître que ce développeur, qui vous rend fou, est un véritable humain qui essaie de faire sortir le logiciel "et ne veut pas expédier de conneries". Étant donné que vous partagez probablement ce terrain d'entente, vous pouvez commencer à discuter de la façon d'améliorer la conception tout en restant conforme au SRP.

Si vous pouvez tous les deux reconnaître que le SRP est une bonne chose, que vous interprétez simplement les aspects différemment, vous pouvez probablement commencer à avoir des conversations productives.

3
Eric Smith

Je suis d'accord avec toutes les réponses sur SRP et sur la façon dont on peut aller trop loin. Dans votre message, vous mentionnez qu'en raison d'une "refacturation excessive" pour adhérer à SRP, vous avez trouvé l'encapsulation cassée ou en cours de modification. La seule chose qui a fonctionné pour moi est de toujours respecter les bases et de faire exactement ce qui est nécessaire pour atteindre une fin.

Lorsque vous travaillez avec des systèmes hérités, "l'enthousiasme" de tout réparer pour l'améliorer est généralement assez élevé dans les chefs d'équipe, en particulier ceux qui sont nouveaux dans ce rôle. SOLID, n'a tout simplement pas SRP - C'est juste le S. Assurez-vous que si vous suivez SOLID, vous n'oublierez pas l'OLID également.

Je travaille actuellement sur un système Legacy et nous avons commencé à suivre une voie similaire au début. Ce qui a fonctionné pour nous était une décision collective de l'équipe pour tirer le meilleur parti des deux mondes - SOLID et KISS (Keep It Simple Stupid). Nous avons discuté collectivement des changements majeurs du code structurer et appliquer le bon sens dans l'application de divers principes de développement. Ils sont parfaits comme lignes directrices et non "Lois du S/W développement". L'équipe n'est pas seulement à propos du chef d'équipe - c'est à propos de tous les développeurs au sein de l'équipe. Ce qui a toujours fonctionné pour moi, c'est de mettre tout le monde dans une pièce et de proposer un ensemble de directives communes que toute votre équipe accepte de suivre.

En ce qui concerne la façon de corriger votre situation actuelle, si vous utilisez un VCS et n'avez pas ajouté trop de nouvelles fonctionnalités à votre application, vous pouvez toujours revenir à une version de code que toute l'équipe pense être compréhensible, lisible et maintenable. Oui! Je vous demande de jeter le travail et de repartir de zéro. C'est mieux que d'essayer de "réparer" quelque chose qui était cassé et de le ramener à quelque chose qui existait déjà.

3
Sharath Satish