web-dev-qa-db-fra.com

SOLID Principes et structure du code

Lors d'un récent entretien d'embauche, je n'ai pas pu répondre à une question sur SOLIDE - au-delà de fournir la signification de base des différents principes. Cela me dérange vraiment. J'ai fait quelques jours de fouille et je n'ai pas encore trouvé de résumé satisfaisant.

La question de l'entretien était:

Si vous regardiez un projet .Net dont je vous ai dit que vous suiviez strictement les principes SOLID, que vous attendriez-vous à voir en termes de projet et de structure de code?

J'ai pataugé un peu, je n'ai pas vraiment répondu à la question, puis j'ai bombardé.

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

154
S-Unit

S = principe de responsabilité unique

Je m'attends donc à voir une structure de dossiers/fichiers et une hiérarchie d'objets bien organisées. Chaque classe/élément de fonctionnalité doit être nommé de manière à ce que sa fonctionnalité soit très évidente et 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, ce serait le signe qu'une seule responsabilité n'était pas respectée.

O = principe ouvert/fermé

C'est essentiellement l'idée que de nouvelles fonctionnalités devraient être ajoutées à travers de nouvelles classes qui ont un minimum d'impact sur/nécessitent une modification des fonctionnalités existantes.

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'un élément de fonctionnalité de l'implémentation réelle, permettant aux autres de venir et d'implémenter d'autres versions sans affecter la original.

L = principe de substitution de Liskov

Cela a à voir avec 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és appropriée.

Je m'attendrais à voir du 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 d'interface

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 garder votre système découplé (par exemple, un FileManager pourrait avoir la responsabilité unique de traiter les E/S de fichiers, mais cela pourrait implémenter un IFileReader et IFileWriter qui contenaient les définitions de méthodes spécifiques pour la lecture et l'écriture des fichiers).

D = principe d'inversion de dépendance.

Encore une fois, cela concerne le maintien d'un système découplé. Peut-être seriez-vous à la recherche de l'utilisation d'une bibliothèque d'injection de dépendances .NET, utilisée dans la solution telle que Unity ou Ninject ou d'un système ServiceLocator tel que AutoFacServiceLocator.

191
Eoin Campbell

Beaucoup de petites classes et interfaces avec injection de dépendance partout. Probablement dans un grand projet, vous utiliseriez également un cadre 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 STRICTEMENT suit SOLID ne signifient pas nécessairement une bonne base de code pour travailler avec tout le monde. Selon qui était l'intervieweur, il/elle aurait peut-être voulu que vous montriez que vous comprenez ce que SOLID signifie et/ou vérifiez dans quelle mesure vous suivez dogmatiquement les principes de conception.

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

[~ # ~] s [~ # ~] principe de responsabilité ingle, vous aurez donc de nombreuses petites classes chacune d'elles ne faisant qu'une seule chose

[~ # ~] o [~ # ~] principe de fermeture du stylet, qui dans .NET est généralement implémenté avec une injection de dépendance, qui nécessite également le I et le D ci-dessous ...

[~ # ~] l [~ # ~] le principe de substitution iskov est probablement impossible à expliquer en c # avec une ligne. Heureusement, il existe d'autres questions à ce sujet, par exemple https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

[~ # ~] i [~ # ~] Le principe de ségrégation d'interface fonctionne en tandem avec le principe ouvert-fermé. S'il est suivi littéralement, cela signifierait préférer un grand nombre de très petites interfaces plutôt que quelques "grandes" interfaces

[~ # ~] d [~ # ~] principe d'inversion d'épendence les classes de haut niveau ne doivent pas dépendre de classes de bas niveau, les deux doivent dépendre d'abstractions .

17
Paolo Falabella

Certaines choses de base que je m'attendrais à voir dans la base de code d'une boutique qui épousait SOLID dans leur travail quotidien:

  • De nombreux 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.
  • Beaucoup de modèles d'adaptateurs et composites - je m'attendrais à ce que beaucoup de modèles d'adaptateurs (une classe implémentant une interface en "passant" à la fonctionnalité d'une interface différente) rationalisent le branchement d'une dépendance développée dans un seul but en légèrement différents endroits où sa fonctionnalité est également nécessaire. Les mises à jour aussi simples que le remplacement d'un enregistreur de console par un enregistreur de fichiers violeront LSP/ISP/DIP si l'interface est mise à jour pour exposer un moyen de spécifier le nom de fichier à utiliser; à la place, la classe de l'enregistreur de fichiers exposera les membres supplémentaires, puis un adaptateur fera ressembler l'enregistreur de fichiers à un enregistreur de console en masquant les nouveaux éléments, de sorte que seul l'objet qui capture tout cela ensemble doit connaître la différence.

    De même, lorsqu'une classe doit ajouter une dépendance d'une interface similaire à une interface existante, pour éviter de changer l'objet (OCP), la réponse habituelle est d'implémenter un modèle Composite/Strategy (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 via une, certaines ou la totalité des implémentations).

  • Beaucoup d'interfaces et ABC - DIP nécessite nécessairement que les abstractions existent, et le FAI encourage celles-ci à une portée étroite. Par conséquent, les interfaces et les classes de base abstraites sont la règle, et vous en aurez besoin de beaucoup pour couvrir la fonctionnalité de dépendance partagée de votre base de code. Bien que strict SOLID nécessiterait l'injection tout, il est évident que vous devez créer quelque part, et donc si un formulaire GUI n'est créé que comme enfant d'un formulaire parent par effectuer une action sur ledit parent, je n'ai aucun scrupule à créer le formulaire enfant à partir du code directement dans le parent. Je fais généralement ce code sa propre méthode, donc si deux actions du même formulaire ouvrent la fenêtre, j'appelle simplement le méthode.
  • De nombreux projets - Le but de tout cela est de limiter la portée du changement. Le changement inclut la nécessité de recompiler (un exercice relativement trivial plus, mais toujours important dans de nombreuses opérations critiques du processeur et de la bande passante, comme le déploiement de mises à jour dans un environnement mobile). Si un fichier d'un projet doit être reconstruit, tous les fichiers le font. 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 devrez également recompiler la définition de l'interface elle-même, ce qui obligera les utilisations à pointer vers un nouvel emplacement dans le binaire résultant. Par conséquent, garder les interfaces distinctes des implémentations et, tout en les séparant par domaine d'utilisation général, est une bonne pratique typique.
  • Beaucoup d'attention accordée à la terminologie "Gang of Four" - Les modèles de conception identifiés dans le livre de 1994 Design Patterns mettent l'accent sur la conception de code modulaire de la taille d'une bouchée qui SOLID cherche à créer. Le principe d'inversion de dépendance et le principe ouvert/fermé, par exemple, sont au cœur de la plupart des modèles identifiés dans ce livre. En tant que tel, je m'attendrais à une boutique qui adhère fortement à SOLID principes pour englober également la terminologie dans le livre du Gang des Quatre, et nommer les classes selon leur fonction le long de ces lignes, telles que "AbcFactory", "XyzRepository", "DefToXyzAdapter", "A1Command" etc.
  • Un référentiel générique - Conformément aux FAI, DIP et SRP, comme généralement compris, le référentiel est presque omniprésent dans la conception SOLID, car il permet à du code de demander des classes de données de manière abstraite sans avoir besoin de détails spécifiques). connaissance du mécanisme de récupération/persistance, et il met 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 une InvoiceDAO qui produirait hydraté objets de ce type, et ainsi de suite pour 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 framework IoC pour faire la majorité de mon injection de dépendance. Il devient rapidement un anti-modèle God Object de tout jeter dans le récipient, de le secouer et de déverser la dépendance à l'hydratation verticale dont vous avez besoin grâce à une méthode d'usine injectée. Cela semble génial, 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 dans votre solution. C'est un beaucoup de raisons de changer. S'il n'est pas fluide (enregistrements tardifs à l'aide de fichiers de configuration), alors un élément clé de votre programme repose sur des "chaînes magiques", un tout autre type de vers.
13
KeithS

Distrayez-les avec discussion de Jon Skeet sur la façon dont le 'O' dans SOLID est "inutile et mal compris" et faites-les parler de la "variation protégée" d'Alistair Cockburn et Josh Bloch "conçoit l'héritage, ou l'interdit".

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

  • La plupart des gens ne savent pas ce que signifient "ouvert" et "fermé" dans le "principe ouvert-fermé", même s'ils pensent qu'ils le font.
  • Les interprétations courantes incluent:
    • que les modules doivent toujours être étendus par héritage d'implémentation, ou
    • que le code source du module d'origine ne peut jamais être modifié.
  • L'intention sous-jacente de l'OCP, et la formulation originale de Bertrand Meyer, est très bien:
    • que 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é" confondent juste la question, même s'ils font un acronyme prononçable de Nice.

Le PO a demandé: "Comment aurais-je pu mieux traiter cette question?" En tant qu'ingénieur principal qui mène une entrevue, je serais infiniment plus intéressé par un candidat qui peut parler intelligemment des avantages et des inconvénients des différents styles de conception de code que par quelqu'un qui peut créer une liste de points.

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

10
David Moles

Il y a probablement un certain nombre de façons de répondre à cette question dans des délais variables. Cependant, je pense que c'est plus dans le sens de "Savez-vous ce que SOLID signifie?") Donc, répondre à cette question se résume probablement à frapper les points et à l'expliquer en termes de projet.

Donc, vous vous attendez à voir ce qui suit:

  • Les classes ont une responsabilité unique (par exemple, une classe d'accès aux données pour les clients obtiendra uniquement les données des clients à partir de la base de données clients).
  • Les classes sont facilement étendues sans affecter le comportement existant. Je n'ai pas à modifier les propriétés ou 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 qui utilisent ces classes de base n'ont pas à déballer la classe de base au type plus spécifique pour les gérer.
  • Les interfaces sont petites et faciles à comprendre. Si une classe utilise une interface, elle n'a pas besoin de dépendre de plusieurs méthodes pour accomplir une tâche.
  • Le code est suffisamment abstrait pour que l'implémentation de haut niveau ne dépende pas concrètement d'une implémentation spécifique de bas niveau. Je devrais pouvoir commuter l'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 un service Web sans affecter le reste de mon application.
6
villecoder

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

Les principes SOLID régissent vraiment les classes et les interfaces et la façon dont ils se sont reliés.

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

Une brève observation ou réponse que je donnerais est que généralement vous verrez des fichiers qui ne contiennent qu'une interface, et souvent la convention est qu'ils commencent par un I 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 le code serait soigneusement partagé à travers certaines limites entre les modules, les applications ou les bibliothèques.

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

4
J. Polfer