À quel moment YAGNI devrait- il avoir préséance sur les bonnes pratiques de codage et vice versa? Je travaille sur un projet au travail et je souhaite introduire progressivement de bonnes normes de code à mes collègues (actuellement, il n'y en a pas et tout est un peu bidouillé sans rime ni raison), mais après avoir créé une série de cours (nous ne faites pas de TDD ou, malheureusement, aucun type de test unitaire). J'ai pris du recul et je pensais que cela violait YAGNI car je sais à peu près avec certitude que nous n'exigeons pas la nécessité d'étendre certaines de ces classes.
Voici un exemple concret de ce que je veux dire: J'ai une couche d'accès aux données qui enveloppe un ensemble de procédures stockées, qui utilise un modèle rudimentaire de style Repository avec des fonctions de base CRUD. Comme il existe une poignée de méthodes dont toutes les classes de référentiels ont besoin, j'ai créé une interface générique pour mes référentiels, appelée IRepository
. Cependant, j'ai ensuite créé une interface "marqueur" (c'est-à-dire une interface qui n'ajoute aucune nouvelle fonctionnalité) pour chaque type de référentiel (par exemple ICustomerRepository
) et la classe concrète l'implémente. J'ai fait la même chose avec une implémentation Factory pour construire les objets métier à partir de DataReaders / DataSets renvoyés par la procédure stockée; la signature de ma classe de référentiel a tendance à ressembler à ceci:
public class CustomerRepository : ICustomerRepository
{
ICustomerFactory factory = null;
public CustomerRepository() : this(new CustomerFactory() { }
public CustomerRepository(ICustomerFactory factory) {
this.factory = factory;
}
public Customer Find(int customerID)
{
// data access stuff here
return factory.Build(ds.Tables[0].Rows[0]);
}
}
Ce qui me préoccupe ici, c'est que je viole YAGNI parce que je sais avec une certitude à 99% qu'il n'y aura jamais de raison de donner autre chose que du concret CustomerFactory
à ce référentiel; étant donné que nous n'avons pas de tests unitaires, je n'ai pas besoin de quelque MockCustomerFactory
chose de similaire, et le nombre d'interfaces risque de semer la confusion chez mes collègues. D'autre part, l'utilisation d'une implémentation concrète de l'usine semble être une odeur de design.
Existe-t-il un bon moyen de parvenir à un compromis entre une conception logicielle appropriée et une architecture non globale? Je me demande si je dois avoir toutes les "interfaces à implémentation simple" ou si je peux sacrifier un peu de bonne conception et ne disposer que, par exemple, de l'interface de base, puis de celle concrète, sans me soucier de la programmation. interface si la mise en œuvre est que sera jamais utilisé.
la source
Réponses:
YAGNI.
Fausse hypothèse.
Ce n'est pas un "sacrifice". C'est une bonne conception.
la source
Dans la plupart des cas, éviter le code dont vous n’avez pas besoin vous aidera à mieux concevoir. La conception la plus facile à gérer et la plus durable est celle qui utilise la plus petite quantité de code simple, bien nommé, qui satisfait aux exigences.
Les conceptions les plus simples sont les plus faciles à faire évoluer. Rien ne tue la facilité de maintenance comme des couches d’abstraction inutiles et trop sophistiquées.
la source
YAGNI et SOLID (ou toute autre méthode de conception) ne s’excluent pas mutuellement. Cependant, ils sont des opposés presque polaires. Vous n'êtes pas obligé d'adhérer à 100% à l'un ou l'autre, mais il y aura des concessions mutuelles; plus vous regardez un motif hautement abstrait utilisé par une classe à un endroit donné, et dites YAGNI et le simplifiez, moins le dessin devient SOLID. L'inverse peut aussi être vrai; plusieurs fois dans le développement, un design est implémenté SOLIDEMENT "sur la foi"; vous ne voyez pas comment vous en aurez besoin, mais vous avez un pressentiment. Cela pourrait être vrai (et cela est de plus en plus vraisemblable à mesure que vous gagnez de l'expérience), mais cela pourrait également vous mettre dans autant de dettes techniques qu'une approche de type "faites-le sans effort"; au lieu d’une base de code "spaghetti code" DIL, vous pouvez vous retrouver avec "code lasagne", avoir autant de couches que simplement ajouter une méthode ou un nouveau champ de données se transforme en un processus de plusieurs jours qui consiste à patauger dans des proxys de service et des dépendances faiblement couplées avec une seule implémentation. Ou vous pourriez vous retrouver avec le "code ravioli", qui se présente sous la forme de petits morceaux qui bougent vers le haut, le bas, la gauche ou la droite dans l’architecture vous conduisent à travers 50 méthodes avec 3 lignes chacune.
Je l'ai dit dans d'autres réponses, mais le voici: lors du premier passage, faites-le fonctionner. Au deuxième passage, rendez-le élégant. Au troisième passage, rendez-le solide.
Décomposer cela:
Lorsque vous écrivez une ligne de code pour la première fois, cela doit simplement fonctionner. À ce stade, pour autant que vous sachiez, il s’agit d’un événement ponctuel. Donc, vous n'avez aucun point de style pour construire une architecture "tour d'ivoire" à ajouter 2 et 2. Faites ce que vous devez faire et supposez que vous ne le verrez plus jamais.
La prochaine fois que votre curseur s'inscrira dans cette ligne de code, vous avez maintenant réfuté votre hypothèse à partir de la première écriture. Vous revisitez ce code, probablement pour l'étendre ou pour l'utiliser ailleurs, donc ce n'est pas un cas isolé. Maintenant, certains principes de base tels que DRY (ne vous répétez pas) et d'autres règles simples pour la conception de code devraient être mis en œuvre; extraire des méthodes et / ou des boucles de formulaire pour le code répété, extraire des variables pour des expressions ou des littéraux courants, peut-être ajouter des commentaires, mais dans l’ensemble, votre code devrait être auto-documenté. Maintenant, votre code est bien organisé, même s’il est peut-être encore étroitement couplé, et quiconque le regarde peut facilement apprendre ce que vous faites en le lisant, au lieu de le suivre ligne par ligne.
La troisième fois que votre curseur entre ce code, c'est probablement une grosse affaire; soit vous l'étendez encore, soit il devient utile dans au moins trois autres endroits différents de la base de code. À ce stade, il s'agit d'un élément clé, voire essentiel, de votre système et doit être conçu comme tel. À ce stade, vous avez également généralement la connaissance de la façon dont elle a été utilisée jusqu'à présent, ce qui vous permettra de prendre de bonnes décisions de conception concernant la manière de concevoir la conception afin de rationaliser ces utilisations, ainsi que les nouvelles. Maintenant, les règles SOLID doivent entrer dans l’équation; extraire des classes contenant du code avec des objectifs spécifiques, définir des interfaces communes pour toutes les classes ayant des objectifs ou des fonctionnalités similaires, configurer des dépendances faiblement couplées entre les classes et concevoir ces dépendances de manière à pouvoir les ajouter, les supprimer ou les échanger facilement.
À partir de ce moment, si vous avez besoin d'étendre, de réimplémenter ou de réutiliser davantage ce code, tout est joliment présenté et résumé dans le format "boîte noire" que nous connaissons et aimons tous; branchez-le partout où vous en avez besoin, ou ajoutez une nouvelle variante sur le thème en tant que nouvelle implémentation de l'interface sans devoir modifier l'utilisation de cette dernière.
la source
Au lieu de l'un ou l'autre, je préfère WTSTWCDTUAWCROT?
(Quelle est la chose la plus simple que nous puissions faire qui soit utile et que nous puissions publier jeudi?)
Les acronymes plus simples sont sur ma liste de choses à faire, mais ils ne sont pas une priorité.
la source
YAGNI et un bon design ne sont pas contradictoires. YAGNI consiste à (ne pas) répondre aux besoins futurs. Une bonne conception consiste à rendre transparent ce que votre logiciel fait actuellement et comment il le fait.
L'introduction d'une usine simplifiera-t-elle votre code existant? Sinon, ne l'ajoutez pas. Si c'est le cas, par exemple lorsque vous ajoutez des tests (ce que vous devriez faire!), Ajoutez-le.
YAGNI consiste à ne pas ajouter de complexité pour prendre en charge les futures fonctions.
Une bonne conception consiste à éliminer la complexité tout en prenant en charge toutes les fonctions actuelles.
la source
Ils ne sont pas en conflit, vos objectifs sont faux.
Qu'essayez-vous d'accomplir?
Vous voulez écrire un logiciel de qualité, et pour ce faire, vous voulez garder votre base de code petite et ne pas avoir de problèmes.
Maintenant que nous sommes en conflit, comment couvrir tous les cas si nous n'écrivons pas de cas que nous n'allons pas utiliser?
Voici à quoi ressemble votre problème.
(quiconque est intéressé, cela s'appelle l' évaporation de nuages )
Alors, qu'est-ce qui motive ça?
Lequel de ces problèmes pouvons-nous résoudre? Eh bien, il semble que ne pas vouloir perdre de temps et le code de ballonnement est un objectif génial et logique. Qu'en est-il de ce premier? Pouvons-nous savoir ce que nous allons devoir coder?
Reformulons tout cela
Vous n'avez pas besoin d'un compromis, vous avez besoin de quelqu'un pour gérer l'équipe qui est compétente et a une vision de l'ensemble du projet. Vous avez besoin de quelqu'un qui peut planifier ce dont vous allez avoir besoin, au lieu que chacun d'entre vous jette des choses dont vous N'AVEZ PAS besoin parce que vous êtes si incertain du futur parce que ... pourquoi? Je vais vous dire pourquoi, c'est parce que personne n'a un plan pour vous tous. Vous essayez d'introduire des normes de code pour résoudre un problème totalement distinct. Votre problème PRIMAIRE que vous devez résoudre est une feuille de route et un projet clairs. Une fois que vous avez cela, vous pouvez dire "les normes de code nous aident à atteindre cet objectif plus efficacement en tant qu'équipe", ce qui est la vérité absolue mais dépasse le cadre de cette question.
Obtenez un chef de projet / équipe capable de faire ces choses. Si vous en avez un, vous devez leur demander une carte et expliquer le problème de YAGNI que ne présente pas de carte. S'ils sont manifestement incompétents, écrivez le plan vous-même et dites: "voici mon rapport sur les choses dont nous avons besoin, veuillez le consulter et laissez-nous savoir votre décision."
la source
Permettre à votre code d'être étendu avec des tests unitaires ne sera jamais couvert par YAGNI, car vous en aurez besoin. Cependant, je ne suis pas convaincu que vos modifications de conception dans une interface à implémentation unique augmentent réellement la testabilité du code car CustomerFactory hérite déjà d'une interface et peut être échangé contre un MockCustomerFactory à tout moment.
la source
La question présente un faux dilemme. Une application correcte du principe YAGNI n’est pas une chose sans rapport. C'est un aspect d'un bon design. Chacun des principes SOLID sont également des aspects d’un bon design. Vous ne pouvez pas toujours appliquer pleinement tous les principes dans n'importe quelle discipline. Les problèmes du monde réel exercent beaucoup de pression sur votre code, et certains d'entre eux vont dans des directions opposées. Les principes de conception doivent en tenir compte, mais aucune poignée de principes ne peut s’adapter à toutes les situations.
Examinons maintenant chaque principe en sachant que, même s’ils peuvent parfois tirer des directions différentes, ils ne sont pas fondamentalement en conflit.
YAGNI a été conçu pour aider les développeurs à éviter un type de travail particulier: celui qui consiste à construire la mauvaise chose. Pour ce faire, il nous aide à éviter de prendre des décisions erronées trop tôt, sur la base d’hypothèses ou de prévisions concernant ce qui, à notre avis, changera ou sera nécessaire à l’avenir. L'expérience collective nous dit que lorsque nous faisons cela, nous avons généralement tort. Par exemple, YAGNI vous dirait de ne pas créer d'interface dans un but de réutilisation , à moins que vous ne sachiez maintenant que vous avez besoin de plusieurs implémenteurs. De même, YAGNI dirait de ne pas créer de "ScreenManager" pour gérer le formulaire unique dans une application, à moins que vous ne sachiez maintenant que vous allez avoir plusieurs écrans.
Contrairement à ce que beaucoup de gens pensent, SOLID n’est pas une question de réutilisabilité, de généricité ou même d’abstraction. SOLID est conçu pour vous aider à écrire du code préparé pour le changement , sans rien dire sur ce que ce changement pourrait être. Les cinq principes de SOLID créent une stratégie de code du bâtiment flexible sans être trop générique, ni simple sans naïf. Une application correcte du code SOLID génère de petites classes ciblées avec des rôles et des limites bien définis. Le résultat pratique est que, quel que soit le besoin, un minimum de classes doit être touché. Et de même, pour tout changement de code, il y a une quantité minimisée de "répercussions" sur les autres classes.
En regardant votre exemple de situation, voyons ce que YAGNI et SOLID pourraient avoir à dire. Vous envisagez une interface de référentiel commune car tous les référentiels ont la même apparence de l'extérieur. Mais la valeur d'une interface générique commune réside dans la possibilité d'utiliser n'importe lequel des implémenteurs sans avoir besoin de savoir laquelle il s'agit en particulier. À moins que cela ne soit nécessaire ou utile dans votre application, YAGNI vous recommande de ne pas le faire.
Il y a 5 principes SOLID à examiner. S est la responsabilité unique. Cela ne dit rien sur l'interface, mais pourrait en dire autant sur vos classes concrètes. On pourrait faire valoir que la gestion de l’accès aux données proprement dit pourrait être confiée à une ou plusieurs autres classes, tandis que la responsabilité des référentiels est de traduire un contexte implicite (CustomerRepository est un référentiel implicite pour les entités du client) en appels explicites aux utilisateurs. API d'accès aux données généralisées spécifiant le type d'entité client.
O est ouvert-fermé. Cela concerne principalement l'héritage. Cela s'appliquerait si vous essayiez de dériver vos référentiels à partir d'une base commune implémentant des fonctionnalités communes, ou si vous espériez dériver plus loin des différents référentiels. Mais vous ne l'êtes pas, alors ce n'est pas le cas.
L est la substituabilité de Liskov. Ceci s'applique si vous avez l'intention d'utiliser les référentiels via l'interface de référentiel commun. Il impose des restrictions sur l'interface et les implémentations pour assurer la cohérence et éviter une manipulation spéciale pour les différents développeurs. La raison en est que ce traitement spécial compromet le but d'une interface. Il peut être utile d’examiner ce principe car il peut vous empêcher d’utiliser l’interface commune du référentiel. Cela coïncide avec les conseils de YAGNI.
I est la ségrégation d'interface. Cela peut s’appliquer si vous commencez à ajouter différentes opérations de requête à vos référentiels. La ségrégation d'interface s'applique lorsque vous pouvez diviser les membres d'une classe en deux sous-ensembles, l'un étant utilisé par certains consommateurs et l'autre par d'autres, mais aucun consommateur n'utilisera probablement les deux sous-ensembles. Il est conseillé de créer deux interfaces distinctes plutôt qu'une interface commune. Dans votre cas, il est peu probable que l'extraction et la sauvegarde d'instances individuelles soient consommées par le même code que celui utilisé pour les requêtes générales. Il peut donc être utile de les séparer en deux interfaces.
D est l'injection de dépendance. Nous revenons ici au même point que le S. Si vous avez séparé votre consommation de l'API d'accès aux données dans un objet séparé, ce principe dit que plutôt que de simplement créer une instance de cet objet, vous devez la transmettre lorsque vous créez. un référentiel. Cela facilite le contrôle de la durée de vie du composant d'accès aux données, en offrant la possibilité de partager des références à ce dernier entre vos référentiels, sans qu'il soit nécessaire de le transformer en singleton.
Il est important de noter que la plupart des principes SOLID ne s'appliquent pas nécessairement à ce stade particulier du développement de votre application. Par exemple, si vous devez diviser l'accès aux données en fonction de la complexité de celui-ci et si vous souhaitez tester la logique de votre référentiel sans toucher à la base de données. Cela semble peu probable (malheureusement, à mon avis), ce n'est donc probablement pas nécessaire.
Après toutes ces considérations, nous constatons que YAGNI et SOLID fournissent en réalité un conseil commun solide et immédiatement pertinent: il n’est probablement pas nécessaire de créer une interface de référentiel générique commune.
Toute cette réflexion est extrêmement utile comme exercice d'apprentissage. Vous prenez beaucoup de temps à apprendre, mais avec le temps, vous développez votre intuition et devenez très rapide. Vous saurez ce qu'il faut faire, mais vous n'avez pas besoin de penser à tous ces mots à moins que quelqu'un vous demande d'expliquer pourquoi.
la source
Vous semblez croire que «bon design» signifie suivre une sorte d'idéologie et un ensemble de règles formelles qui doivent toujours être appliquées, même lorsqu'elles sont inutiles.
OMI c'est mauvais design. YAGNI est un composant d'un bon design, jamais une contradiction.
la source
Dans votre exemple, je dirais que YAGNI devrait prévaloir. Cela ne vous coûtera pas très cher si vous devez ajouter des interfaces ultérieurement. À propos, est-ce vraiment bien de concevoir une interface par classe si elle ne sert aucun objectif?
Une dernière pensée, peut-être que parfois, ce dont vous avez besoin n’est pas une bonne conception mais une conception suffisante. Voici une séquence très intéressante d'articles sur le sujet:
la source
Certaines personnes soutiennent que les noms d'interface ne devraient pas commencer par I. Plus précisément, l'une des raisons est que vous perdez en fait la dépendance selon que le type donné est une classe ou une interface.
Qu'est-ce qui vous interdit d'
CustomerFactory
être une classe au début et ensuite de la changer en une interface, qui sera implémentée parDefaultCustormerFactory
ouUberMegaHappyCustomerPowerFactory3000
? La seule chose que vous devriez avoir à changer est l'endroit où l'implémentation est instanciée. Et si vous avez un design moins bon, il ne reste que quelques endroits au maximum.La refactorisation fait partie du développement. Mieux vaut avoir peu de code, facile à refactoriser, que d’avoir une interface et une classe déclarées pour chaque classe, ce qui vous oblige à modifier chaque nom de méthode à au moins deux endroits à la fois.
Le principal intérêt des interfaces est la modularité, qui est peut-être le pilier le plus important d’une bonne conception. Notez cependant qu'un module n'est pas seulement défini par son découplage du monde extérieur (même si c'est ainsi que nous le percevons de la perspective extérieure), mais également par son fonctionnement interne.
Ce que je veux dire, c’est que découpler des choses, qui appartiennent intrinsèquement ensemble, n’a pas beaucoup de sens. D'une certaine manière, c'est comme si vous disposiez d'une armoire avec une étagère individuelle par tasse.
L'important est de concevoir un gros problème complexe en sous-problèmes plus petits et plus simples. Et vous devez vous arrêter au point où ils deviennent assez simples sans subdivisions supplémentaires, sinon ils deviendront en réalité plus compliqués. Cela pourrait être considéré comme un corollaire de YAGNI. Et cela signifie certainement un bon design.
L'objectif n'est pas de résoudre en quelque sorte un problème local avec un seul référentiel et une seule usine. Le but est que cette décision n’affecte pas le reste de votre demande. C'est ce que la modularité est à propos.
Vous voulez que vos collègues regardent votre module, voient une façade avec une poignée d'appels explicites et se sentent confiants de pouvoir les utiliser, sans avoir à se soucier de tous les branchements intérieurs potentiellement sophistiqués.
la source
Vous créez
interface
plusieurs implémentations anticipant dans le futur. Vous pourriez aussi bien avoir unI<class>
pour chaque classe dans votre base de code. Ne pasUtilisez simplement la classe concrète unique selon YAGNI. Si vous constatez que vous devez disposer d'un objet "fictif" à des fins de test, transformez la classe d'origine en une classe abstraite avec deux implémentations, une avec la classe concrète d'origine et l'autre avec l'implémentation fictive.
Vous devrez évidemment mettre à jour toutes les instanciations de la classe d'origine pour instancier la nouvelle classe concrète. Vous pouvez contourner ce problème en utilisant un constructeur statique dès le départ.
YAGNI dit de ne pas écrire de code avant qu'il ne soit écrit.
Un bon design dit d'utiliser l'abstraction.
Vous pouvez avoir les deux. Une classe est une abstraction.
la source
Pourquoi les interfaces de marqueur? Il me semble que cela ne fait rien d’autre que le "marquage". Avec un "tag" différent pour chaque type d'usine, à quoi ça sert?
Le but d'une interface est de donner à une classe "agit comme" un comportement, de leur donner une "capacité de référentiel" pour ainsi dire. Donc, si tous vos types de référentiels concrets se comportent comme le même IRepository (ils implémentent tous IRepository), ils peuvent tous être traités de la même manière par un autre code, avec le même code exact. À ce stade, votre conception est extensible. Ajout de types de référentiels plus concrets, tous traités comme des référentiels IR génériques - le même code traite tous les types concrets en tant que référentiels "génériques".
Les interfaces servent à manipuler des objets en se basant sur des points communs. Mais les interfaces de marqueur personnalisé a) n’ajoutent aucun comportement. et b) vous obliger à gérer leur unicité.
Dans la mesure où vous concevez des interfaces utiles, vous obtenez l'avantage de ne pas avoir à écrire de code spécialisé pour gérer des classes, des types ou des interfaces de marqueur personnalisées ayant une corrélation 1: 1 avec des classes concrètes. C'est une redondance inutile.
Je peux voir une interface de marqueur si, par exemple, vous aviez besoin d'une collection fortement typée de beaucoup de classes différentes. Dans la collection, ils sont tous des "ImarkerInterface", mais lorsque vous les sortez, vous devez les attribuer à leur type.
la source
Pouvez-vous, maintenant, écrire un ICustomerRepository vaguement raisonnable? Par exemple, (atteignant vraiment ici, probablement un mauvais exemple), dans le passé, vos clients utilisaient-ils toujours PayPal? Ou tout le monde dans l'entreprise est brouillé au sujet de la connexion avec Alibaba? Si c'est le cas, vous voudrez peut-être utiliser la conception plus complexe maintenant et paraître clairvoyant pour vos patrons. :-)
Sinon, attendez. Deviner à une interface avant d'avoir une ou deux implémentations réelles échoue généralement. En d’autres termes, ne généralisez pas / abstenez-vous / utilisez un modèle de design fantaisie avant d’avoir quelques exemples à généraliser.
la source