Bonjour, Habr! Je vous présente l'article "Quatre meilleures règles pour la conception de logiciels" de David Bryant Copeland. David Bryant Copeland est architecte logiciel et CTO pour Stitch Fix. Il tient un blog et est l' auteur de plusieurs livres .
Martin Fowler a récemment tweeté avec un lien vers son article de blog sur quatre règles de conception simples de Kent Beck, qui je pense peuvent être encore améliorées (et qui peuvent parfois envoyer le programmeur dans le mauvais sens):
Explication des règles de Kent tirées de la programmation extrême :
- Kent dit: "Exécutez tous les tests."
- Ne dupliquez pas la logique. Essayez d'éviter les doublons cachés, tels que les hiérarchies de classes parallèles.
- Toutes les intentions importantes pour le programmeur doivent être clairement visibles.
- Le code doit avoir le plus petit nombre possible de classes et de méthodes.
D'après mon expérience, ces règles ne répondent pas tout à fait aux besoins de la conception de logiciels. Mes quatre règles pour un système bien conçu pourraient être:
- il est bien couvert par les tests et les réussit.
- il n'a pas d'abstractions dont le programme n'a pas besoin directement.
- elle a un comportement sans ambiguïté.
- il nécessite le moins de concepts.
Pour moi, ces règles découlent de ce que nous faisons avec nos logiciels.
Alors, que faisons-nous avec notre logiciel?
Nous ne pouvons pas parler de conception de logiciels sans d'abord parler de ce que nous avons l'intention d'en faire.
Le logiciel est écrit pour résoudre le problème. Le programme s'exécute et a un comportement. Ce comportement est étudié pour assurer un fonctionnement correct ou pour détecter des erreurs. Le logiciel change également souvent pour lui donner un comportement nouveau ou modifié.
Par conséquent, toute approche de la conception de logiciels doit se concentrer sur la prévision, l'étude et la compréhension de son comportement afin de rendre le changement de ce comportement aussi simple que possible.
Nous vérifions le comportement correct en testant, et donc je suis d'accord avec Kent que la première chose et la plus importante est qu'un logiciel bien conçu doit passer les tests. J'irai même plus loin et insisterai pour que le logiciel ait des tests (c'est-à-dire qu'il soit bien couvert par des tests).
Une fois le comportement vérifié, les trois points suivants sur les deux listes concernent la compréhension de notre logiciel (et donc de son comportement). Sa liste commence par la duplication de code, qui est vraiment en place. Cependant, dans mon expérience personnelle, trop se concentrer sur la réduction de la duplication de code coûte cher. Pour l'éliminer, il faut créer des abstractions qui le cachent, et ce sont ces abstractions qui rendent le logiciel difficile à comprendre et à modifier.
L'élimination de la duplication de code nécessite des abstractions, et les abstractions conduisent à la complexité
Don't Repeat Yourself ou DRY est utilisé pour justifier des décisions de conception controversées. Avez-vous déjà vu un code similaire?
ZERO = BigDecimal.new(0)
De plus, vous avez probablement vu quelque chose comme ceci:
public void call(Map payload, boolean async, int errorStrategy) {
Si vous voyez des méthodes ou des fonctions avec des drapeaux, des booléens, etc., cela signifie généralement que quelqu'un a utilisé le principe DRY lors de la refactorisation, mais le code n'était pas exactement le même aux deux endroits, donc le code résultant devrait avoir être suffisamment flexible pour s'adapter aux deux comportements.
De telles abstractions généralisées sont difficiles à tester et à comprendre, car elles devraient traiter beaucoup plus de cas que le code d'origine (éventuellement dupliqué). En d'autres termes, les abstractions supportent beaucoup plus de comportements qu'il n'est nécessaire pour le fonctionnement normal du système. Ainsi, l'élimination de la duplication de code peut créer un nouveau comportement dont le système n'a pas besoin.
Par conséquent, il est vraiment important de combiner certains types de comportement, mais il peut être difficile de comprendre quel type de comportement est réellement dupliqué. Souvent, les morceaux de code se ressemblent, mais cela ne se produit que par accident.
Considérez combien il est plus facile d'éliminer la duplication de code que de le renvoyer (par exemple, après avoir créé une abstraction mal pensée). Par conséquent, nous devons penser à laisser du code en double, sauf si nous sommes absolument sûrs que nous avons une meilleure façon de s'en débarrasser.
Créer des abstractions devrait nous faire réfléchir. Si, dans le processus d'élimination du code en double, vous créez une abstraction généralisée très flexible, vous avez peut-être fait le mauvais chemin.
Cela nous amène au point suivant - intention contre comportement.
L'intention du programmeur est dénuée de sens - le comportement signifie tout
Nous louons souvent les langages de programmation, les constructions ou les extraits de code pour «révéler les intentions du programmeur». Mais quel est l'intérêt de connaître les intentions si vous ne pouvez pas prédire le comportement? Et si vous connaissez le comportement, que signifie l'intention? Il s'avère que vous devez savoir comment le logiciel doit se comporter, mais ce n'est pas la même chose que les «intentions du programmeur».
Regardons cet exemple, qui reflète très bien les intentions du programmeur, mais ne se comporte pas comme prévu:
function LastModified(props) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); }
De toute évidence, le programmeur prévoyait que ce composant React afficherait une date avec le message "Dernière modification le". Est-ce que cela fonctionne comme prévu? Pas vraiment. Et si cette date n'a pas d'importance? Tout tombe en panne. Nous ne savons pas si cela a été conçu ainsi, ou quelqu'un l’a juste oublié, et cela n’a même pas d’importance. Ce qui compte, c'est le comportement.
Et c'est exactement ce que nous devons savoir si nous voulons changer cette partie du code. Imaginez que nous devons changer la ligne en "Dernière modification". Bien que nous puissions le faire, il n'est pas clair ce qui devrait arriver si la date est manquante. Il serait préférable que nous écrivions plutôt le composant de manière à rendre son comportement plus compréhensible.
function LastModified(props) { if (!props.date) { throw "LastModified requires a date to be passed"; } return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); }
Ou même comme ça:
function LastModified(props) { if (props.date) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } else { return <div>Never modified</div>; } }
Dans les deux cas, le comportement est plus compréhensible et les intentions du programmeur n'ont pas d'importance. Supposons que nous choisissions la deuxième alternative (qui gère la valeur de date manquante). Lorsque nous sommes invités à modifier le message, nous pouvons voir le comportement et vérifier si le message "Jamais modifié" est correct ou s'il doit également être modifié.
Ainsi, plus le comportement est sans ambiguïté, plus nous avons de chances de le changer avec succès. Et cela signifie que nous pouvons avoir besoin d'écrire plus de code ou de le rendre plus précis, ou même parfois d'écrire du code en double.
Cela signifie également que nous aurons besoin de plus de classes, de fonctions, de méthodes, etc. Bien sûr, nous aimerions garder leur nombre minimal, mais nous ne devrions pas utiliser ce nombre comme métrique. La création d'un grand nombre de classes ou de méthodes crée une surcharge conceptuelle et plus de concepts apparaissent dans le logiciel que d'unités de modularité. Par conséquent, nous devons réduire le nombre de concepts, ce qui, à son tour, peut entraîner une diminution du nombre de classes.
Les coûts conceptuels contribuent à la confusion et à la complexité
Pour comprendre ce que le code fera réellement, vous devez connaître non seulement le domaine, mais également tous les concepts utilisés dans ce code (par exemple, lorsque vous recherchez l'écart-type, vous devez connaître l'affectation, l'addition, la multiplication, pour les boucles et les longueurs de tableau). Cela explique pourquoi à mesure que le nombre de concepts dans une conception augmente, sa complexité pour la compréhension augmente.
J'avais l' habitude d'écrire sur les dépenses conceptuelles , et un bon effet secondaire de la réduction du nombre de concepts dans un système est que plus de gens peuvent comprendre ce système. Cela augmente à son tour le nombre de personnes qui peuvent apporter des modifications à ce système. Certainement, une conception de logiciel qui peut être modifiée en toute sécurité par de nombreuses personnes est meilleure que celle qui ne peut être modifiée que par une petite poignée. (Par conséquent, je crois que la programmation fonctionnelle hardcore ne deviendra jamais populaire, car elle nécessite une compréhension approfondie de nombreux concepts très abstraits.)
La réduction des coûts conceptuels réduira naturellement le nombre d'abstractions et facilitera la compréhension des comportements. Je ne dis pas «ne jamais introduire un nouveau concept», je dis qu'il a son propre prix, et si ce prix l'emporte sur l'avantage, l'introduction d'un nouveau concept doit être soigneusement envisagée.
Lorsque nous écrivons du code ou un logiciel de conception, nous devons cesser de penser à l' élégance , à la beauté ou à toute autre mesure subjective de notre code. Au lieu de cela, nous devons toujours nous souvenir de ce que nous allons faire avec le logiciel.
Vous n'accrochez pas le code au mur - vous le changez
Un code n'est pas une œuvre d'art que vous pouvez imprimer et accrocher dans un musée. Le code est en cours d'exécution. Il est étudié et débogué. Et, surtout, cela change . Et souvent. Toute conception difficile à travailler doit être remise en question et revue. Toute conception qui réduit le nombre de personnes pouvant travailler avec elle devrait également être remise en question.
Le code devrait fonctionner, il devrait donc être testé. Le code a des bogues et nécessitera l'ajout de nouvelles fonctionnalités, nous devons donc comprendre son comportement. Le code vit plus longtemps que la capacité d'un programmeur particulier à le prendre en charge, nous devons donc nous efforcer d'obtenir un code compréhensible pour un large éventail de personnes.
Lorsque vous écrivez votre code ou concevez votre système, simplifiez-vous l'explication du comportement du système? Est-il devenu plus facile de comprendre comment elle se comportera? Êtes-vous concentré sur la résolution du problème juste devant vous ou sur un problème plus abstrait?
Essayez toujours de garder le comportement simple pour la démonstration, la prédiction et la compréhension, et gardez le nombre de concepts au minimum absolu.