Un de mes amis travaille pour une petite entreprise sur un projet que tout développeur détesterait: il est obligé de publier le plus rapidement possible, il est le seul qui semble se soucier de la dette technique, le client n'a aucune formation technique, etc.
Il m'a raconté une histoire qui m'a fait réfléchir sur la pertinence des modèles de conception dans des projets comme celui-ci. Voici l'histoire.
Nous devions afficher les produits à différents endroits du site Web. Par exemple, les gestionnaires de contenu peuvent visualiser les produits, mais aussi les utilisateurs finaux ou les partenaires via l'API.
Parfois, des informations manquaient dans les produits: par exemple, un certain nombre d'entre eux n'avaient aucun prix lorsque le produit venait d'être créé, mais le prix n'était pas encore spécifié. Certains n'avaient pas de description (la description étant un objet complexe avec des historiques de modification, un contenu localisé, etc.). Certains manquaient d'informations sur l'expédition.
Inspiré par mes lectures récentes sur les modèles de conception, j'ai pensé que c'était une excellente occasion d'utiliser la magie modèle d'objet nul . Alors je l'ai fait, et tout était lisse et propre. Il suffit d'appeler
product.Price.ToString("c")
pour afficher le prix, ouproduct.Description.Current
pour afficher la description; aucun élément conditionnel requis. Jusqu'au jour où la partie prenante a demandé à l'afficher différemment dans l'API, en ayant unnull
en JSON. Et aussi différemment pour les gestionnaires de contenu en affichant "Prix non spécifié [Modifier]". Et j'ai dû tuer mon modèle Null Object bien-aimé, car il n'était plus nécessaire.De la même manière, j'ai dû supprimer quelques usines abstraites et quelques constructeurs, j'ai fini par remplacer mon beau motif de façade par des appels directs et moches, car les interfaces sous-jacentes changeaient deux fois par jour pendant trois mois, et même le Singleton m'a quitté lorsque les exigences indiquaient que l'objet concerné devait être différent selon le contexte.
Plus de trois semaines de travail ont consisté à ajouter des modèles de conception, puis à les déchirer un mois plus tard, et mon code est finalement devenu suffisamment spaghetti pour être impossible à maintenir par quiconque, y compris moi-même. Ne serait-il pas préférable de ne jamais utiliser ces modèles en premier lieu?
En effet, j'ai dû travailler moi-même sur ces types de projets où les exigences changent constamment, et sont dictées par des personnes qui n'ont pas vraiment à l'esprit la cohésion ou la cohérence du produit. Dans ce contexte, peu importe votre agilité, vous arriverez avec une solution élégante à un problème, et lorsque vous l'implémenterez enfin, vous apprendrez que les exigences ont changé si radicalement, que votre solution élégante ne convient pas plus longtemps.
Quelle serait la solution dans ce cas?
Vous n'utilisez aucun modèle de conception, arrêtez de penser et écrivez directement du code?
Il serait intéressant de faire une expérience où une équipe écrit directement du code, tandis qu'une autre réfléchit à deux fois avant de taper, prenant le risque de devoir jeter le design original quelques jours plus tard: qui sait, peut-être que les deux équipes auraient le même dette technique. En l'absence de telles données, je dirais seulement qu'il ne se sent bien de taper du code sans y penser avant de travailler sur 20 mois-homme projet.
Gardez le modèle de conception qui n'a plus de sens et essayez d'ajouter plus de modèles pour la situation nouvellement créée?
Cela ne semble pas juste non plus. Les modèles sont utilisés pour simplifier la compréhension du code; mettre trop de modèles, et le code deviendra un gâchis.
Commencer à penser à un nouveau design qui englobe les nouvelles exigences, puis refaçonner lentement l'ancien design dans le nouveau?
En tant que théoricien et celui qui favorise Agile, je suis totalement dedans. En pratique, quand vous savez que vous devrez revenir au tableau blanc chaque semaine et refaire la grande partie de la conception précédente et que le client n'a tout simplement pas assez de fonds pour vous payer cela, ni assez de temps pour attendre, cela ne fonctionnera probablement pas.
Alors, des suggestions?
Je vois quelques fausses hypothèses dans cette question:
Les modèles de conception ne sont pas une fin en soi, ils devraient vous servir, et non l'inverse. Si un modèle de conception ne rend pas le code plus facile à implémenter, ou au moins mieux évolutif (cela signifie: plus facile à adapter aux exigences changeantes), alors le motif manque son but. N'appliquez pas de modèles lorsqu'ils ne facilitent pas la "vie" de l'équipe. Si le nouveau motif d'objet Null servait votre ami pendant le temps où il l'a utilisé, alors tout allait bien. Si cela devait être éliminé plus tard, cela pourrait aussi être correct. Si le modèle d'objet Null ralentissait l'implémentation (correcte), son utilisation était incorrecte. Notez, à partir de cette partie de l'histoire, on ne peut conclure à aucune cause de "code spaghetti" jusqu'à présent.
Ce n'est ni son travail ni sa faute! Votre travail consiste à vous soucier de la cohésion et de la cohérence. Lorsque les exigences changent deux fois par jour, votre solution ne doit pas être de sacrifier la qualité du code. Dites simplement au client combien de temps cela prend, et si vous pensez que vous avez besoin de plus de temps pour obtenir la conception "correcte", ajoutez une marge de sécurité suffisamment grande à toute estimation. Surtout quand un client essaie de vous mettre la pression, utilisez le "Scotty Principle" . Et lorsque vous vous disputez avec un client non technique au sujet de l'effort, évitez les termes comme "refactoring", "tests unitaires", "modèles de conception" ou "documentation de code" - ce sont des choses qu'il ne comprend pas et considère probablement comme "inutiles". un non-sens "car il n'y voit aucune valeur. Parlez toujours de choses qui sont visibles ou au moins compréhensibles par le client (fonctionnalités, sous-fonctionnalités, changements de comportement, documents utilisateur, corrections d'erreurs, optimisation des performances, etc.).
Honnêtement, si "les interfaces sous-jacentes changent deux fois par jour pendant trois mois", alors la solution ne devrait pas être de réagir en changeant le code deux fois par jour. La vraie solution est de demander pourquoi les exigences changent si souvent et s'il est possible de faire un changement à cette partie du processus. Peut-être que des analyses initiales plus approfondies seront utiles. L'interface est peut-être trop large car la frontière entre les composants est mal choisie. Parfois, il est utile de demander plus d'informations concernant la partie des exigences qui est stable et celles qui sont encore en discussion (et en fait, reporter l'implémentation des éléments en discussion). Et parfois, certaines personnes doivent simplement être "frappées dans le cul" pour ne pas avoir changé d'avis deux fois par jour.
Mon humble avis est que vous ne devez pas éviter ou ne pas éviter d'utiliser des modèles de conception.
Les modèles de conception sont tout simplement des solutions bien connues et fiables aux problèmes généraux, qui ont reçu des noms. Ils ne sont pas différents sur le plan technique de toute autre solution ou conception à laquelle vous pouvez penser.
Je pense que la racine du problème pourrait être que votre ami pense en termes "d'appliquer ou de ne pas appliquer un modèle de conception", au lieu de penser en termes de "quelle est la meilleure solution à laquelle je peux penser, y compris, mais sans s'y limiter, les modèles Je connais".
Peut-être que cette approche le conduit à utiliser des modèles de manière partiellement artificielle ou forcée, dans des endroits où ils n'appartiennent pas. Et c'est ce qui entraîne un gâchis.
Dans votre exemple d'utilisation du modèle Null Object, je pense qu'il a finalement échoué car il répondait aux besoins du programmeur et non aux besoins du client. Le client devait afficher le prix sous une forme adaptée au contexte. Le programmeur devait simplifier une partie du code d'affichage.
Donc, lorsqu'un modèle de conception ne répond pas aux exigences, disons-nous que tous les modèles de conception sont une perte de temps ou disons-nous que nous avons besoin d'un modèle de conception différent?
Il semblerait que l'erreur ait été plus de supprimer les objets du motif que de les utiliser. Dans la conception initiale, l'objet nul semble avoir fourni une solution à un problème. Ce n'était peut-être pas la meilleure solution.
Le fait d'être la seule personne travaillant sur un projet vous donne la chance de découvrir l'ensemble du processus de développement. Le gros inconvénient est de ne pas avoir quelqu'un pour être votre mentor. Prendre le temps d'apprendre et d'appliquer les meilleures ou les meilleures pratiques est susceptible de porter ses fruits rapidement. L'astuce consiste à identifier quelle pratique apprendre quand.
Le chaînage des références sous la forme product.Price.toString ('c') viole la loi de Déméter . J'ai vu toutes sortes de problèmes avec cette pratique, dont beaucoup sont liés à des valeurs nulles. Une méthode comme product.displayPrice ('c') pourrait gérer les prix nuls en interne. De même, product.Description.Current peut être géré par product.displayDescription (), product.displayCurrentDescription (). ou product.diplay ('Actuel').
La gestion de la nouvelle exigence pour les gestionnaires et les fournisseurs de contenu doit être gérée en répondant au contexte. Il existe une variété d'approches qui peuvent être utilisées. Les méthodes d'usine peuvent utiliser différentes classes de produits en fonction de la classe d'utilisateurs à laquelle elles seront affichées. Une autre approche consisterait à ce que les méthodes d'affichage des classes de produits construisent des données différentes pour différents utilisateurs.
La bonne nouvelle est que votre ami se rend compte que les choses deviennent incontrôlables. Espérons qu'il ait le code en contrôle de révision. Cela lui permettra d'annuler les mauvaises décisions, qu'il prendra invariablement. Une partie de l'apprentissage consiste à essayer différentes approches, dont certaines échoueront. S'il peut gérer les prochains mois, il peut trouver des approches qui lui simplifient la vie et nettoient les spaghettis. Il pourrait essayer de réparer une chose chaque semaine.
Arrêtons-nous un instant et examinons le problème fondamental ici - L'architecture d'un système où le modèle d'architecture est trop couplé à des fonctionnalités de bas niveau dans le système, provoquant une interruption fréquente de l'architecture dans le processus de développement.
Je pense que nous devons nous rappeler que l'utilisation de l'architecture et des modèles de conception qui y sont liés doit être posée à un niveau approprié, et que l'analyse de ce qui est le bon niveau n'est pas triviale. D'une part, vous pouvez facilement maintenir l'architecture de votre système à un niveau trop élevé avec seulement des contraintes très basiques comme "MVC" ou similaires, ce qui peut conduire à des opportunités manquées comme dans des directives claires et un levier de code, et où le code spaghetti peut facilement s'épanouir dans tout cet espace libre.
D'un autre côté, vous pourriez tout aussi bien sur-architecturer votre système, que lorsque vous définissez les contraintes à un niveau détaillé, où vous supposez que vous pouvez compter sur des contraintes qui, en réalité, sont plus volatiles que ce à quoi vous vous attendez, brisant constamment vos contraintes et vous forçant à constamment remodeler et reconstruire, jusqu'à ce que vous commenciez à désespérer.
Les changements dans les exigences d'un système seront toujours là, dans une mesure plus ou moins grande. Et les avantages potentiels de l'utilisation de l'architecture et des modèles de conception seront toujours là, il n'est donc pas vraiment question d'utiliser des modèles de conception ou non, mais à quel niveau vous devez les utiliser.
Cela vous oblige non seulement à comprendre les exigences actuelles du système proposé, mais également à identifier quels aspects de celui-ci peuvent être considérés comme des propriétés centrales stables du système, et quelles propriétés pourraient être susceptibles de changer au cours du développement.
Si vous constatez que vous devez constamment vous battre avec du code de spaghetti non organisé, vous ne faites probablement pas assez d'architecture, ou à un niveau élevé. Si vous trouvez que votre architecture se casse fréquemment, vous faites probablement une architecture trop détaillée.
L'utilisation d'architecture et de motifs de conception n'est pas quelque chose dans laquelle vous pouvez simplement "enduire" un système, comme si vous peigniez un bureau. Ce sont des techniques qui doivent être appliquées de manière réfléchie, à un niveau où les contraintes sur lesquelles vous devez compter ont une forte probabilité d'être stables, et où ces techniques valent vraiment la peine de modéliser l'architecture et de mettre en œuvre les contraintes/architecture/modèles réels comme code.
Relatif à la question d'une architecture trop détaillée, vous pouvez aussi bien mettre beaucoup d'efforts en architecture où elle ne donne pas beaucoup de valeur. Voir l'architecture axée sur le risque pour référence, j'aime ce livre - juste assez d'architecture logicielle , peut-être que vous aussi.
Modifier
J'ai clarifié ma réponse depuis que j'ai réalisé que je m'exprimais souvent comme "trop d'architecture", où je voulais vraiment dire "architecture trop détaillée", ce qui n'est pas exactement la même chose. Une architecture trop détaillée peut souvent être considérée comme une architecture "trop", mais même si vous gardez l'architecture à un bon niveau et créez le plus beau système que l'humanité ait jamais vu, cela pourrait toujours être trop d'effort sur l'architecture si les priorités sont sur les fonctionnalités et le délai de commercialisation.
La question semble erronée sur tant de points. Mais les plus flagrants sont:
Beaucoup de gens l'ont dit à juste titre, les modèles de conception concernent essentiellement l'étiquetage et la dénomination, une pratique courante. Pensez donc à une chemise, une chemise a un col, pour une raison quelconque, vous enlevez le col ou une partie du col. Le nom et l'étiquetage changent, mais c'est toujours une chemise par essence. C'est exactement le cas ici, des changements mineurs dans les détails qui ne signifient pas que vous avez "assassiné" ce schéma. (encore une fois l'esprit de la formulation extrême)
D'après mon expérience, lorsque des exigences mineures surviennent, il vous suffit de modifier une petite partie de la base de code. Certains peuvent être un peu hacky, mais rien de trop grave pour affecter substantiellement la maintenabilité ou la lisibilité et souvent quelques lignes de commentaires pour expliquer la partie hacky suffiront. Il s'agit également d'une pratique très courante.
Votre ami semble faire face à de nombreux vents contraires basés sur son anecdote. C'est malheureux et cela peut être un environnement très difficile à travailler. Malgré la difficulté, il était sur la bonne voie en utilisant des modèles pour lui faciliter la vie, et c'est dommage qu'il ait quitté cette voie. Le code spaghetti est le résultat ultime.
Puisqu'il y a deux problèmes différents, techniques et interpersonnels, je vais aborder chacun séparément.
La difficulté de votre ami est de faire face à des exigences qui évoluent rapidement, et comment cela affecte sa capacité à écrire du code maintenable. Je dirais tout d'abord que les exigences qui changent deux fois par jour, chaque jour pendant une si longue période de temps est un problème plus important et a une attente implicite irréaliste. Les exigences changent plus rapidement que le code ne peut changer. Nous ne pouvons pas nous attendre à ce que le code ou le programmeur suivent. Ce rythme de changement rapide est symptomatique d'une conception incomplète du produit souhaité à un niveau supérieur. C'est un problème. S'ils ne savent pas ce qu'ils veulent vraiment, ils perdront beaucoup de temps et d'argent pour ne jamais l'obtenir.
Il pourrait être bon de fixer des limites pour les changements. Regroupez les modifications en ensembles toutes les deux semaines, puis gelez-les pendant les deux semaines pendant leur mise en œuvre. Construisez une nouvelle liste pour les deux prochaines semaines. J'ai le sentiment que certains de ces changements se chevauchent ou se contredisent (par exemple, faire des va-et-vient entre deux options). Lorsque les changements surviennent rapidement et furieusement, ils ont tous la priorité absolue. Si vous les laissez s'accumuler dans une liste, vous pouvez travailler avec eux pour organiser et hiérarchiser ce qui est le plus important pour maximiser les efforts et la productivité. Ils peuvent voir que certains de leurs changements sont stupides ou moins importants, ce qui donne à votre ami un peu de répit.
Ces problèmes ne devraient cependant pas vous empêcher d'écrire du bon code. Un mauvais code entraîne des problèmes plus graves. La refactorisation d'une solution à une autre peut prendre du temps, mais le fait même qu'elle soit possible montre les avantages de bonnes pratiques de codage à travers des modèles et des principes.
Dans un environnement de changements fréquents, la dette technique sera arrivera à échéance à un moment donné. Il est préférable de faire des paiements à ce sujet plutôt que de jeter l'éponge et d'attendre qu'elle devienne trop grosse pour être surmontée. Si un modèle n'est plus utile, refactorisez-le, mais ne revenez pas aux méthodes de codage des cow-boys.
Votre ami semble avoir une bonne compréhension des modèles de conception de base. L'objet nul est une bonne approche du problème auquel il était confronté. En vérité, c'est toujours une bonne approche. Là où il semble avoir des défis, c'est de comprendre les principes derrière les modèles, le pourquoi de ce qu'ils sont. Sinon, je ne pense pas qu'il aurait abandonné son approche.
(Ce qui suit est un ensemble de solutions techniques qui n'étaient pas demandées dans la question d'origine, mais qui montrent comment nous pourrions adhérer à des modèles à des fins d'illustration.)
Le principe de l'objet nul est l'idée d'encapsuler ce qui varie . Nous cachons les changements afin de ne pas avoir à y faire face partout ailleurs. Ici, l'objet nul encapsulait la variance dans le product.Price
instance (je l'appellerai un objet Price
, et un prix d'objet nul sera NullPrice
). Price
est un objet de domaine, un concept d'entreprise. Parfois, dans notre logique métier, nous ne connaissons pas encore le prix. Ça arrive. Cas d'utilisation parfait pour l'objet nul. Price
s ont une méthode ToString
qui affiche le prix, ou une chaîne vide si elle n'est pas connue (ou, NullPrice#ToString
renvoie une chaîne vide). Il s'agit d'un comportement raisonnable. Ensuite, les exigences changent.
Nous devons générer un null
dans la vue API ou une chaîne différente dans la vue des gestionnaires. Comment cela affecte-t-il notre logique métier? Et bien non. Dans la déclaration ci-dessus, j'ai utilisé deux fois la "vue" de Word. Cette Parole n'a probablement pas été prononcée explicitement, mais nous devons nous entraîner à entendre les mots cachés dans les exigences. Alors, pourquoi "voir" est-il si important? Parce qu'il nous indique où le changement doit vraiment se produire: à notre avis.
En plus: que nous utilisions ou non un framework MVC n'a pas d'importance ici. Bien que MVC ait une signification très spécifique pour "View", je l'utilise dans le sens plus général (et peut-être plus applicable) d'un morceau de code de présentation.
Nous devons donc vraiment corriger cela dans la vue. Comment pourrions-nous faire cela? La façon la plus simple de le faire serait une instruction if
. Je sais que l'objet nul était destiné à se débarrasser de tous les ifs, mais nous devons être pragmatiques. Nous pouvons vérifier la chaîne pour voir si elle est vide et basculer:
if(product.Price.ToString("c").Length == 0) { // one way of many
writer.write("Price unspecified [Change]");
} else {
writer.write(product.Price.ToString("c"));
}
Comment cela affecte-t-il l'encapsulation? La partie la plus importante ici est que la logique de vue est encapsulée dans la vue . Nous pouvons ainsi garder notre logique métier/objets de domaine complètement isolés des changements dans la logique de la vue. C'est moche, mais ça marche. Ce n'est pas la seule option, cependant.
Nous pourrions dire que notre logique métier a légèrement changé en ce sens que nous voulons afficher des chaînes par défaut si aucun prix n'est défini. Nous pouvons apporter une petite modification à notre Price#ToString
méthode (crée en fait une méthode surchargée). Nous pouvons accepter une valeur de retour par défaut et renvoyer cela si aucun prix n'est défini:
class Price {
...
// A new ToString method
public string ToString(string c, string default) {
return ToString(c);
}
...
}
class NullPrice {
...
// A new ToString method
public string ToString(string c, string default) {
return default;
}
...
}
Et maintenant, notre code de vue devient:
writer.write(product.Price.ToString("c", "Price unspecified [Change]"));
Le conditionnel a disparu. Faire trop, cependant, pourrait proliférer des méthodes de cas spécial dans vos objets de domaine, donc cela n'a de sens que s'il n'y aura que quelques exemples de cela.
Nous pourrions plutôt créer une méthode IsSet
sur Price
qui retourne un booléen:
class Price {
...
public bool IsSet() {
return return true;
}
...
}
class NullPrice {
...
public bool IsSet() {
return false;
}
...
}
Voir la logique:
if(product.Price.IsSet()) {
writer.write(product.Price.ToString("c"));
} else {
writer.write("Price unspecified [Change]");
}
Nous voyons le retour du conditionnel dans la vue, mais le cas est plus fort pour la logique métier indiquant si le prix est fixé. On peut utiliser Price#IsSet
ailleurs maintenant que nous l'avons.
Enfin, nous pouvons résumer l'idée de présenter un prix entièrement dans une aide à la vue. Cela cacherait le conditionnel, tout en préservant l'objet domaine autant que nous le souhaiterions:
class PriceStringHelper {
public PriceStringHelper() {}
public string PriceToString(Price price, string default) {
if(price.IsSet()) { // or use string length to not change the Price class at all
return price.ToString("c");
} else {
return default;
}
}
}
Voir la logique:
writer.write(new PriceStringHelper().PriceToString(product.Price, "Price unspecified [Change]"));
Il existe de nombreuses autres façons d'effectuer les modifications (nous pourrions généraliser PriceStringHelper
en un objet qui renvoie une valeur par défaut si une chaîne est vide), mais ce sont quelques-unes qui préservent (pour la plupart) à la fois la modèles et les principes, ainsi que l'aspect pragmatique de la fabrication de tels un changement.
La complexité d'un modèle de conception peut vous mordre si le problème qu'il était censé résoudre disparaît soudainement. Malheureusement, en raison de l'enthousiasme et de la popularité des modèles de conception, ce risque est rarement explicité. L'anecdote de votre ami aide beaucoup à montrer comment les modèles ne sont pas payants. Jeff Atwood a quelques mots de choix sur le sujet.
Documenter les points de variation (ce sont des risques) dans les exigences
La plupart des modèles de conception les plus complexes (Null Object pas tellement) contiennent le concept de Protected variations , c'est-à-dire "Identifier les points de variation ou d'instabilité prédits; attribuer des responsabilités pour créer une interface stable autour d'eux." Adaptateur, visiteur, façade, couches, observateur, stratégie, décorateur, etc. exploitent tous ce principe. Ils "portent leurs fruits" lorsque le logiciel doit être étendu dans la dimension de la variabilité attendue, et les hypothèses "stables" restent stables.
Si vos exigences sont si instables que vos "variations prévues" sont toujours fausses, alors les schémas que vous appliquez vous causeront de la douleur ou seront au mieux une complexité inutile.
Craig Larman parle de deux possibilités d'appliquer des variantes protégées:
Les deux sont censés être documentés par les développeurs, mais vous devriez probablement avoir l'engagement des clients sur les points de variation.
Pour gérer les risques, vous pouvez dire que tout modèle de conception appliquant le PV doit être tracé jusqu'à un point de variation dans les exigences approuvées par le client. Si un client modifie un point de variation des exigences, votre conception devra peut-être changer radicalement (car vous avez probablement investi dans la conception [les modèles] pour prendre en charge cette variation). Pas besoin d'expliquer la cohésion, le couplage, etc.
Par exemple, votre client souhaite que le logiciel fonctionne avec trois systèmes d'inventaire hérités différents. C'est un point de variation que vous concevez. Si le client abandonne cette exigence, alors bien sûr, vous avez un tas d'infrastructure de conception inutile. Le client doit savoir que les points de variation coûtent quelque chose.
CONSTANTS
dans le code source sont une forme simple de PV
Une autre analogie avec votre question serait de demander si l'utilisation de CONSTANTS
dans le code source est une bonne idée. En se référant à cet exemple , disons que le client a abandonné le besoin de mots de passe. Ainsi, le MAX_PASSWORD_SIZE
car une diffusion constante dans votre code deviendrait inutile et même un obstacle à la maintenance et à la lisibilité. Souhaitez-vous blâmer l'utilisation de CONSTANTS
comme raison?
Je pense que cela dépend au moins en partie de la nature de votre situation.
Vous avez mentionné des exigences en constante évolution. Si le client dit "Je veux que cette application apicole fonctionne également avec les guêpes", cela semble être le genre de situation dans laquelle une conception soignée aiderait à progresser, pas à la gêner (surtout si vous considérez qu'à l'avenir, elle pourrait vouloir garder les mouches des fruits aussi.)
D'un autre côté, si la nature du changement ressemble plus à "Je veux que cette application apicole gère la paie de mon conglomérat de laverie", aucune quantité de code ne vous sortira de votre trou.
Les modèles de conception n'ont rien de intrinsèquement bon. Ce sont des outils comme les autres - nous ne les utilisons que pour faciliter nos tâches à moyen et long terme. Si un outil différent (tel que communication ou recherche) est plus utile, nous l'utilisons.