
Le monde est incroyablement complexe, et cela n’est nulle part plus évident que dans l’art d’écrire des logiciels. Les logiciels sont le résultat d’interactions complexes entre les structures sociales, les désirs des entreprises et des connaissances limitées. La complexité de ces interactions apparaît inévitablement dans la structure du code. Notre travail en tant qu'ingénieurs est de gérer cela et de préparer nos abstractions pour gérer une complexité croissante au fil du temps. La technique la plus importante à cet égard est de laisser de la place aux éléments de logique pour qu’ils deviennent plus complexes sans s’y noyer – avec la complexité, la dose fait le poison.
C'est pourquoi l'un des conseils d'écriture de logiciels les plus courants que vous entendrez est d'écrire de nombreuses petites classes ciblées avec une logique divisée en de nombreuses petites méthodes ciblées. Les logiciels écrits dans ce style permettent à chaque élément individuel de gagner en complexité. Au sein de l'équipe de développement de Rescale, nous avons constaté que le moment de séparer la logique d'une méthode ou d'une classe en croissance est beaucoup plus précoce que ce que les développeurs pensent généralement. Nous préférons commencer à extraire des abstractions au fur et à mesure que nous écrivons les première et deuxième versions des classes et des méthodes.
Pour illustrer avec un exemple, nous avons récemment écrit du code pour analyser des fichiers XML tiers décrivant les flux de travail : fichiers à transférer, analyses à exécuter et définitions de variables. Au début, notre code d'analyse était relativement simple, principalement parce que nous ne savions pas tout ce dont nous aurions besoin pour analyser. Nous avons commencé par analyser simplement les fichiers d'entrée et les variables. Chacun d'entre eux est représenté par un nœud XML, et chaque nœud XML indique son type avec un attribut nommé type, indiquant ainsi des valeurs importantes avec des attributs dépendants du type.
Un code d'analyse initial ressemblait à :
Collection inputFileNames = getNodeStream() .filter(n -> n.getAttributes() .getNamedItem("type") .getNodeValue().equals("inputFileType")) .map(n -> n.getAttributes() .getNamedItem( "nom de fichier") .getNodeValue()) .collect(Collectors.toList()); Collection inputVariableNames = getNodeStream() .filter(n -> n.getAttributes() .getNamedItem("type") .getNodeValue().equals("inputVariableType")) .map(n -> n.getAttributes() .getNamedItem( "variableName") .getNodeValue()) .collect(Collectors.toList());
La méthode d'assistance getNodeStream ci-dessus convertit un Liste de nœuds du document dans un courant pour faciliter la manipulation.
Il y a deux choses à remarquer à propos de ce code : il utilise des chaînes magiques au lieu de constanteset duplique le code pour extraire les valeurs d'attribut. Après avoir appliqué ces refactorisations simples, le code d'analyse est moins encombré de détails d'implémentation et se lit plus fidèlement à son intention :
Collection inputFileNames = getNodeStream() .filter(n -> INPUT_FILE_TYPE.equals(getAttribute(n, TYPE))) .map(n -> getAttribute(n, FILE_NAME)) .collect(Collectors.toList()); Collection inputVariableNames = getNodeStream() .filter(n -> INPUT_VARIABLE_TYPE.equals(getAttribute(n, TYPE))) .map(n -> getAttribute(n, VARIABLE_NAME)) .collect(Collectors.toList());
Il s'agit de notre premier exemple de la façon dont les meilleures pratiques peuvent aider à préparer le code à une complexité croissante. En apparence, il ne semble pas que nous ayons fait grand-chose, mais en écrivant un code plus proche de ce que nous signifier, plutôt que ce que l'ordinateur cela, nous avons facilité l'ajout de complexité à ce code car nous aurons moins de contexte à garder en tête lorsque nous écrivons de nouveaux ajouts.
Si l'analyse de ces collections de chaînes était tout ce que nous avions fait avec ce fichier XML, ce serait bien de laisser ce code tel quel. Mais comme il s’agit d’un logiciel, les choses sont devenues plus complexes. Après avoir écrit la troisième ou la quatrième paire d'analyse type/attribut, nous avons décidé d'extraire quelques énumérations :
public enum NodeAttribute { TYPE("type"), FILE_NAME("filename"), VARIABLE_NAME("variableName"),… private final String attrName ; private NodeAttribute (String attrName) { this.attrName = attrName ; } public String getValue(Node node) { return node.getAttributes() .getNamedItem(this.attrName) .getNodeValue(); } public enum NodeType { INPUT_FILE("inputFile"), INPUT_VARIABLE("inputVariable"), OUTPUT_VARIABLE("outputVariable"), … valeur de chaîne finale privée ; private NodeType (Valeur de chaîne) { this.value = valeur ; } correspondances booléennes publiques (nœud de nœud) { return NodeAttributes.TYPE.getValue (node).equals (this.value); } }
Maintenant, le code d'analyse ressemble à :
Collection inputFileNames = getNodeStream() .filter(NodeType.INPUT_FILE::matches) .map(NodeAttribute.FILE_NAME::getValue) .collect(Collectors.toList()); Collection inputVariableNames = getNodeStream() .filter(NodeType.INPUT_VARIABLE::matches) .map(NodeAttribute.VARIABLE_NAME::getValue) .collect(Collectors.toList()); …
Il semble qu'il soit exagéré d'extraire la logique ici, mais nous étions motivés à le faire car les énumérations fournissent un emplacement central pour définir ces constantes en vue de leur réutilisation. Un autre avantage dont nous avons rapidement pris connaissance est qu'ils fournissaient espace pour que les différentes pièces deviennent plus complexes. Maintenant, vous pensez peut-être qu'il s'agit simplement d'extraire un attribut d'un nœud XML, comment cela pourrait-il devenir plus complexe ? Nous ne pensions certainement pas que cela serait ou pourrait être le cas.
Mais il s'avère que dans ce fichier XML tiers, certains nœuds font référence à des fichiers avec un attribut nommé filename et certains fichiers référencés avec un attribut nommé fileName. C'est le genre de chose qui fait maudire les programmeurs, mais heureusement, nous étions prêts à gérer cela avec facilité :
public enum NodeAttribute { TYPE("type"), FILE_NAME("filename", "fileName"), VARIABLE_NAME("variableName"), … private final String[] attrNames; private NodeAttribute(String... attrNames) { this.attrNames = attrNames; } public String getValue(Node node) { return Arrays.stream(this.AttrNames) .map(attr -> node.getAttributes().getNamedItem(attr)) .filter(attr -> attr != null) .findFirst() .orElseThrow(() -> new NoSuchElementException( "Node: " + node.getNodeValue() + " n'avait aucun attribut nommé : " + Arrays.toString(mAttrNames) )); }
Aucun de nos autres codes d'analyse n'a dû changer. Si nous avions continué à utiliser des constantes de chaîne, nous aurions dû effectuer de nombreuses mises à jour dans le code d'analyse pour vérifier le nom de fichier ou le nom de fichier, ou bien écrire des méthodes spéciales pour les nœuds référençant des fichiers. Le code serait devenu plus encombré de logique if/else. Cependant, comme nous avons fait abstraction très tôt, nous avions une place pour exprimer cette logique. Nous tenons à réitérer que nous ne nous attendions pas à cette différence dans la casse des attributs, mais c'est exactement le point : vous devriez attendons. code pour devenir plus complexe d'une manière que vous ne t'attends pas.
Pourquoi certains nœuds utilisent-ils le nom de fichier et d'autres utilisent-ils le nom de fichier ? Nous pouvons deviner que deux personnes différentes ont travaillé sur la sérialisation des différents nœuds et qu'elles ne connaissaient pas le schéma de capitalisation que l'autre avait choisi. Peut-être ont-ils communiqué verbalement et décidé du « nom de fichier » comme attribut, mais l’un d’entre eux a utilisé une douille de chameau. Ou peut-être qu'ils ont travaillé sur un nœud après l'autre et ont oublié quel système de capitalisation avait été utilisé.
Quoi qu'il en soit, la complexité de la structure sociale de l'équipe de développement tierce se manifeste dans cette différence de noms d'attributs et se reflète dans notre base de code. C'est notre travail de nous préparer à ce genre de complexité, d'être prêts à y faire face avec des structures appropriées.
