Principes et structure de code SOLID

150

Lors d'un récent entretien d'embauche, je ne pouvais pas répondre à une question à propos de SOLID - à part fournir le sens de base des divers principes. Cela me gêne vraiment. J'ai passé quelques jours à fouiller et je n'ai pas encore produit de résumé satisfaisant.

La question de l'entrevue était:

Si vous examiniez un projet .Net dont je vous ai dit de suivre rigoureusement les principes SOLID, qu'attendriez-vous de voir en termes de projet et de structure de code?

Je me suis effondré un peu, je n'ai pas vraiment répondu à la question, puis j'ai été bombardé.

Comment aurais-je pu mieux gérer cette question?

Unité S
la source
3
Je me demandais ce qui n'est pas clair dans la page de wiki de SOLID
BЈовић le
Extensible Abstract Building Blocks.
Rwong
En suivant les principes SOLID de la conception orientée objet, vos cours auront naturellement tendance à être petits, bien factorisés et faciles à tester. Source: docs.asp.net/fr/latest/fundamentals/…
WhileTrueSleep

Réponses:

188

S = Principe de responsabilité unique

Donc, je m'attendrais à voir une structure de dossiers / fichiers et une hiérarchie d'objets bien organisée. Chaque classe / élément de fonctionnalité doit être nommé de manière à ce que sa fonctionnalité soit très évidente, et il ne doit contenir que la logique pour effectuer cette tâche.

Si vous voyiez d'énormes classes de gestionnaires avec des milliers de lignes de code, cela signifierait que la responsabilité unique n'est pas suivie.

O = principe ouvert / fermé

C’est essentiellement l’idée que de nouvelles fonctionnalités doivent être ajoutées via de nouvelles classes ayant un impact minimal sur les fonctionnalités existantes ou nécessitant une modification de ces fonctionnalités.

Je m'attendrais à voir beaucoup d'utilisation de l'héritage d'objet, du sous-typage, des interfaces et des classes abstraites pour séparer la conception d'une fonctionnalité de l'implémentation réelle, permettant ainsi à d'autres utilisateurs de venir et d'implémenter d'autres versions sans affecter les performances. original.

L = principe de substitution de Liskov

Cela concerne la possibilité de traiter les sous-types comme leur type parent. Cela sort de la boîte en C # si vous implémentez une hiérarchie d'objets héritée appropriée.

Je m'attendrais à voir le code traiter les objets communs comme leur type de base et appeler des méthodes sur les classes de base / abstraites plutôt que d'instancier et de travailler sur les sous-types eux-mêmes.

I = Principe de séparation des interfaces

Ceci est similaire à SRP. Fondamentalement, vous définissez des sous-ensembles de fonctionnalités plus petits en tant qu’interfaces et travaillez avec celles-ci pour que votre système reste découplé (par exemple, a FileManagerpourrait avoir la seule responsabilité de traiter les E / S sur fichier, mais cela pourrait implémenter un IFileReaderet IFileWritercontenant les définitions de méthode spécifiques pour la lecture. et écriture de fichiers).

D = Principe d'inversion de dépendance.

Là encore, cela concerne le fait de garder un système découplé. Peut-être serez-vous à la recherche d’une bibliothèque .NET Dependency Injection utilisée dans la solution telle que Unityou Ninjectou dans un système ServiceLocator tel que AutoFacServiceLocator.

Eoin Campbell
la source
36
J'ai vu beaucoup de violations de LSP en C #, chaque fois que quelqu'un décide que son sous-type est spécialisé et n'a donc pas besoin d'implémenter une partie de l'interface et lève simplement une exception sur cette pièce à la place ... C'est une approche junior commune pour résoudre le problème de la conception et de la mise en œuvre d'interface incompréhensibles
Jimmy Hoffa
2
@ JimmyHoffa C'est l'une des principales raisons pour lesquelles j'insiste sur l'utilisation des contrats de code. passer par le processus de conception des contrats aide beaucoup à sortir les gens de cette mauvaise habitude.
Andy
12
Je n'aime pas le "LSP qui sort de la boîte en C #" et assimilant DIP à la pratique d'injection de dépendance.
Euphoric
3
+1 mais Inversion de dépendance <> Injection de dépendance. Ils jouent bien ensemble, mais l’inversion de dépendance va bien au-delà de l’injection de dépendance. Référence: DIP à l'état sauvage
Marjan Venema le
3
@Andy: ce qui aide aussi, ce sont les tests unitaires définis sur les interfaces par rapport auxquels tous les implémenteurs (toute classe pouvant / est instanciée) sont testés.
Marjan Venema
17

Beaucoup de petites classes et interfaces avec injection de dépendance partout. Dans un grand projet, vous utiliseriez probablement également un framework IoC pour vous aider à construire et gérer la durée de vie de tous ces petits objets. Voir https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Notez qu'un grand projet .NET qui suit STRICTEMENT les principes de SOLID ne signifie pas nécessairement une bonne base de code à utiliser pour tout le monde. En fonction de l'identité de l'intervieweur, il / elle aurait peut-être souhaité que vous montriez que vous comprenez ce que signifie SOLID et / ou que vous vérifiez dogmatiquement que vous respectez les principes de conception.

Vous voyez, pour être solide, vous devez suivre:

S principe de responsabilité Ingle, de sorte que vous aurez beaucoup de petites classes chacun d'entre eux font une seule chose

Principe O Pen-Closed, qui, dans .NET, est généralement implémenté avec l'injection de dépendance, qui requiert également le I et le D ci-dessous ...

Le principe de substitution de L iskov est probablement impossible à expliquer en c # avec une ligne. Heureusement, d'autres questions s'y posent, par exemple https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

Je NTERFACE Ségrégation Principe fonctionne en tandem avec le principe ouvert-fermé. Si cela était suivi à la lettre, cela signifierait préférer un grand nombre de très petites interfaces plutôt que quelques "grandes" interfaces

D ependency principe d'inversion des classes de haut niveau ne devraient pas dépendre des classes de bas niveau, les deux devraient dépendre des abstractions.

Paolo Falabella
la source
SRP ne signifie pas "faire une seule chose".
Robert Harvey
13

Quelques éléments de base que je m'attendrais à voir dans le code d'un magasin qui a épousé SOLID dans son travail quotidien:

  • Beaucoup de petits fichiers de code - avec une classe par fichier comme meilleure pratique dans .NET et le principe de responsabilité unique encourageant les petites structures de classe modulaires, je m'attendrais à voir beaucoup de fichiers contenant chacun une petite classe ciblée.
  • Un grand nombre de modèles adaptateur et composite - je m'attendrais à l'utilisation de nombreux modèles d'adaptateur (une classe implémentant une interface en "passant par la fonctionnalité d'une interface différente) pour simplifier l'insertion d'une dépendance développée légèrement différents endroits où sa fonctionnalité est également nécessaire. Des mises à jour aussi simples que le remplacement d'un consignateur de la console par un enregistreur de fichiers violeront le LSP / ISP / DIP si l'interface est mise à jour pour exposer un moyen de spécifier le nom de fichier à utiliser. au lieu de cela, la classe de consignateur de fichiers exposera les membres supplémentaires, puis un adaptateur fera ressembler le consignateur de fichiers à un consignateur de console en masquant le nouveau contenu, de sorte que seul l'objet qui capture tout cela doit connaître la différence.

    De même, lorsqu'une classe doit ajouter une dépendance à une interface similaire à une interface existante pour éviter de changer d'objet (OCP), la réponse habituelle consiste à implémenter un motif Composite / Stratégie (une classe implémentant l'interface de dépendance et consommant plusieurs autres implémentations de cette interface, avec des quantités variables de logique permettant à la classe de passer un appel à une, à toutes ou à toutes les implémentations).

  • Beaucoup d'interfaces et d'ABC - Le DIP nécessite nécessairement l'existence d'abstractions, et le fournisseur de services Internet les encourage à avoir une portée étroite. Par conséquent, les interfaces et les classes de base abstraites constituent la règle et vous en aurez besoin pour couvrir la fonctionnalité de dépendance partagée de votre base de code. Alors que SOLID strict nécessiterait de tout injecter , il est évident que vous devez créer quelque part. Par conséquent, si un formulaire GUI est uniquement créé en tant qu'enfant d'un seul parent en effectuant une action sur ledit parent, je n'ai aucun scrupule à modifier le formulaire enfant. à partir du code directement dans le parent. En règle générale, je fais de ce code sa propre méthode. Par conséquent, si deux actions de la même forme ouvrent la fenêtre, j'appelle simplement la méthode.
  • De nombreux projets - Le but de tout cela est de limiter la portée du changement. Le changement inclut le besoin de recompiler (un exercice relativement trivial, mais toujours important dans de nombreuses opérations critiques pour la bande passante et le processeur, comme le déploiement de mises à jour dans un environnement mobile). Si un fichier d'un projet doit être reconstruit, tous les fichiers le sont. Cela signifie que si vous placez des interfaces dans les mêmes bibliothèques que leurs implémentations, vous manquez le point; vous devrez recompiler toutes les utilisations si vous modifiez une implémentation de l'interface, car vous recompilerez également la définition de l'interface elle-même, ce qui obligera les utilisateurs à pointer vers un nouvel emplacement dans le fichier binaire obtenu. Par conséquent, en séparant les interfaces des usages et Les implémentations, tout en les séparant par domaine d'utilisation, constituent une pratique exemplaire typique.
  • Beaucoup d'attention a été accordée à la terminologie "Gang of Four" - Les modèles de conception identifiés dans l'ouvrage de 1994 Design Designs mettent l'accent sur la conception de code modulaire et compacte que SOLID cherche à créer. Le principe d'inversion de dépendance et le principe d'ouverture / fermeture, par exemple, sont au cœur de la plupart des modèles identifiés dans ce livre. En tant que tel, je m'attendrais à ce qu'un magasin qui adhère fermement aux principes SOLID adopte également la terminologie du livre de Gang of Four, et nomme les classes en fonction de leur fonction, comme "AbcFactory", "XyzRepository", "DefToXyzAdapter". "," A1Command "etc.
  • Un référentiel générique - En accord avec ISP, DIP et SRP comme on l'entend généralement, le référentiel est presque omniprésent dans la conception SOLID, car il permet au code consommateur de demander des classes de données de manière abstraite sans avoir besoin d'une connaissance spécifique du mécanisme de récupération / persistance, et il place le code qui le fait à un endroit par opposition au modèle DAO (dans lequel, si vous aviez par exemple une classe de données Invoice, vous auriez également un InvoiceDAO produisant des objets hydratés de ce type, et ainsi de suite. tous les objets / tables de données dans la base de code / schéma).
  • Un conteneur IoC - j'hésite à ajouter celui-ci, car je n'utilise pas de cadre IoC pour effectuer la majorité de mes injections de dépendance. Cela devient rapidement un anti-modèle de Dieu-objet de tout jeter dans le conteneur, le secouant et versant la dépendance verticalement hydratée dont vous avez besoin par le biais d'une méthode d'usine injectée. Cela semble bien, jusqu'à ce que vous réalisiez que la structure devient assez monolithique et que le projet avec les informations d'enregistrement, s'il est "fluide", doit maintenant tout savoir sur tout ce qui se trouve dans votre solution. C'est beaucoup de raisons de changer. S'il ne parle pas couramment (enregistrements tardifs utilisant des fichiers de configuration), un élément clé de votre programme repose sur des "chaînes magiques", un tout autre concept.
KeithS
la source
1
pourquoi les votes négatifs?
KeithS
Je pense que c'est une bonne réponse. Au lieu de ressembler aux nombreux billets de blogs sur ces termes, vous avez répertorié des exemples et des explications qui illustrent leur utilisation et leur valeur
Crowie
10

Distrayez-les en discutant avec Jon Skeet de la façon dont le "O" dans SOLID est "inutile et mal compris" et faites-leur parler de la "variation protégée" d'Alistair Cockburn et du "projet de succession ou d'interdiction de Josh Bloch".

Bref résumé de l'article de Skeet (bien que je ne recommanderais pas de laisser son nom sans lire le billet de blog original!):

  • La plupart des gens ne savent pas ce que signifie «ouvert» et «fermé» dans «principe ouvert-fermé», même s'ils pensent le savoir.
  • Les interprétations courantes comprennent:
    • que les modules doivent toujours être étendus grâce à l'héritage d'implémentation, ou
    • que le code source du module d'origine ne peut jamais être modifié.
  • L’intention sous-jacente d’OCP, et la formulation originale de Bertrand Meyer, est correcte:
    • les modules doivent avoir des interfaces bien définies (pas nécessairement au sens technique d'interface) sur lesquelles leurs clients peuvent compter, mais
    • il devrait être possible d'étendre ce qu'ils peuvent faire sans casser ces interfaces.
  • Mais les mots "ouvert" et "fermé" ne font que brouiller les cartes, même s’ils constituent un acronyme agréable à prononcer.

Le PO a demandé: "Comment aurais-je pu mieux gérer cette question?" En tant qu’ingénieur principal menant une entrevue, je serais infiniment plus intéressé par un candidat capable de parler intelligemment des avantages et des inconvénients de différents styles de conception de code que par une personne capable de produire une liste de points.

Une autre bonne réponse serait: «Cela dépend de la façon dont ils l'ont compris. Si tout ce qu'ils savent, ce sont les mots à la mode SOLID, je m'attendrais à un abus d'héritage, à une utilisation excessive des infrastructures d'injection de dépendance, un million de petites interfaces reflète le vocabulaire de domaine utilisé pour communiquer avec la gestion de produit .... "

David Moles
la source
6

Il y a probablement plusieurs façons de résoudre ce problème avec des durées variables. Cependant, je pense que cela va plus dans le sens de "Savez-vous ce que SOLID signifie?" Donc, répondre à cette question revient probablement à cogner sur les points et à les expliquer en termes de projet.

Donc, vous vous attendez à voir ce qui suit:

  • Les classes ont une seule responsabilité (par exemple, une classe d'accès aux données pour les clients obtiendra uniquement les données client de la base de données clients).
  • Les classes sont facilement étendues sans affecter le comportement existant. Je n'ai pas besoin de modifier les propriétés ou d'autres méthodes pour ajouter des fonctionnalités supplémentaires.
  • Les classes dérivées peuvent être substituées aux classes de base et les fonctions utilisant ces classes de base n'ont pas à décompresser la classe de base dans un type plus spécifique pour pouvoir les gérer.
  • Les interfaces sont petites et faciles à comprendre. Si une classe utilise une interface, il n'est pas nécessaire qu'elle dépende de plusieurs méthodes pour accomplir une tâche.
  • Le code est suffisamment abstrait pour que la mise en œuvre de haut niveau ne dépende pas concrètement de la mise en œuvre spécifique de bas niveau. Je devrais être en mesure de changer d'implémentation de bas niveau sans affecter le code de haut niveau. Par exemple, je peux changer ma couche d'accès aux données SQL pour une couche basée sur le service Web sans affecter le reste de mon application.
villecoder
la source
4

C'est une excellente question, même si je pense que c'est une question d'entretien difficile.

Les principes SOLID régissent réellement les classes et les interfaces, ainsi que leurs relations.

Cette question est vraiment une question qui a plus à voir avec les fichiers et pas nécessairement les classes.

Une brève observation ou une réponse que je voudrais donner est qu’en général, vous verrez des fichiers ne contenant qu’une interface, et souvent, la convention est qu’ils commencent par un «majuscule». Au-delà de cela, je mentionnerais que les fichiers n'auraient pas de code dupliqué (en particulier dans un module, une application ou une bibliothèque) et que ce code serait partagé avec précaution au-delà de certaines limites entre modules, applications ou bibliothèques.

Robert Martin aborde ce sujet dans le domaine du C ++ dans la conception d'applications C ++ orientées objectifs à l'aide de la méthode Booch (voir les sections Cohésion, Fermeture et Réutilisation) et dans le Code épuré .

J. Polfer
la source
Les codeurs .NET IME suivent généralement une règle "1 classe par fichier", ainsi que des structures miroir de dossiers / espaces de noms; l'IDE de Visual Studio encourage les deux pratiques, et divers plugins tels que ReSharper peuvent les appliquer. Donc, je m'attendrais à voir une structure de projet / fichier refléter la structure de classe / interface.
KeithS