web-dev-qa-db-fra.com

Pourquoi les langages dynamiques rendent-ils plus difficile le maintien de grandes bases de code?

Les grandes bases de code sont plus difficiles à maintenir lorsqu'elles sont écrites dans des langages dynamiques. C'est du moins ce que dit Yevgeniy Brikman, développeur principal apportant le Play Framework à LinkedIn dans ne présentation vidéo enregistrée à JaxConf 201 (minute 44).

Pourquoi dit-il cela? Quelles sont les raisons?

237
Jus12

les langages dynamiques rendent plus difficile le maintien de grandes bases de code

Avertissement: je n'ai pas regardé la présentation.

J'ai fait partie des comités de conception pour JavaScript (un langage très dynamique), C # (un langage principalement statique) et Visual Basic (qui est à la fois statique et dynamique), j'ai donc un certain nombre de réflexions sur ce sujet; trop pour rentrer facilement dans une réponse ici.

Permettez-moi de commencer par dire que il est difficile de maintenir une grande base de code, point. Le gros code est difficile à écrire, quels que soient les outils dont vous disposez. Votre question n'implique pas que le maintien d'une grande base de code dans un langage de type statique est "facile"; la question suppose plutôt simplement que c'est un problème encore plus difficile de maintenir une grande base de code dans un langage dynamique que dans un langage statique. Cela dit, il y a des raisons pour lesquelles l'effort déployé pour maintenir une grande base de code dans un langage dynamique est quelque peu plus important que l'effort dépensé pour les langages typés statiquement. J'en explorerai quelques-unes dans ce post.

Mais nous sommes en avance sur nous-mêmes. Nous devons définir clairement ce que nous entendons par un langage "dynamique"; par langage "dynamique", j'entends l'opposé d'un langage "statique".

Un langage "de type statique" est un langage conçu pour faciliter la vérification automatique de l'exactitude par un outil qui n'a accès qu'au code source, et non à l'état d'exécution du programme. Les faits déduits par l'outil sont appelés "types". Les concepteurs de langage produisent un ensemble de règles sur ce qui rend un programme "de type sûr", et l'outil cherche à prouver que le programme suit ces règles; si ce n'est pas le cas, cela produit une erreur de type.

Un langage "typé dynamiquement" en revanche n'est pas conçu pour faciliter ce type de vérification. La signification des données stockées dans un emplacement particulier ne peut être facilement déterminée que par inspection pendant l'exécution du programme.

(Nous pourrions également faire une distinction entre les langues à portée dynamique et les langues à portée lexicale, mais n'allons pas là-bas aux fins de cette discussion. souvent une corrélation entre les deux.)

Alors maintenant que nos termes sont clairs, parlons de grandes bases de code. Les grandes bases de code ont généralement des caractéristiques communes:

  • Ils sont trop grands pour qu'une seule personne puisse comprendre chaque détail.
  • Ils sont souvent travaillés par de grandes équipes dont le personnel évolue avec le temps.
  • Ils sont souvent travaillés longtemps, avec plusieurs versions.

Toutes ces caractéristiques présentent des obstacles à la compréhension du code, et donc des obstacles à une modification correcte du code. En bref: le temps c'est de l'argent; apporter des modifications correctes à une grande base de code est coûteux en raison de la nature de ces obstacles à la compréhension.

Étant donné que les budgets sont limités et que nous voulons faire tout ce que nous pouvons avec les ressources dont nous disposons, les responsables des grandes bases de code cherchent à réduire le coût des modifications correctes en atténuant ces obstacles. Voici certaines des façons dont les grandes équipes atténuent ces obstacles:

  • Modularisation: Le code est factorisé en "modules" d'une certaine sorte où chaque module a une responsabilité claire. L'action du code peut être documentée et comprise sans qu'un utilisateur ait à comprendre ses détails d'implémentation.
  • Encapsulation: Les modules font une distinction entre leur surface "publique" et leurs détails d'implémentation "privés" afin que ces derniers puissent être améliorés sans affecter l'exactitude du programme dans son ensemble.
  • Réutilisation: Lorsqu'un problème est résolu correctement une fois, il est résolu pour toujours; la solution peut être réutilisée dans la création de nouvelles solutions. Des techniques telles que la création d'une bibliothèque de fonctions utilitaires, ou la création de fonctionnalités dans une classe de base pouvant être étendue par une classe dérivée, ou des architectures qui encouragent la composition, sont toutes des techniques de réutilisation de code. Encore une fois, il s'agit de réduire les coûts.
  • Annotation: Le code est annoté pour décrire les valeurs valides qui pourraient entrer dans une variable, par exemple.
  • Détection automatique des erreurs: Une équipe travaillant sur un grand programme est sage de construire un appareil qui détermine tôt quand une erreur de programmation a été commise et vous en informe afin qu'elle puisse être corrigée rapidement, avant la l'erreur est aggravée par d'autres erreurs. Les techniques telles que l'écriture d'une suite de tests ou l'exécution d'un analyseur statique entrent dans cette catégorie.

Une langue typée statiquement en est un exemple; vous obtenez dans le compilateur lui-même un périphérique qui recherche les erreurs de type et vous en informe avant de vérifier le changement de code cassé dans le référentiel. A manifestement tapé language requiert que les emplacements de stockage soient annotés avec des faits sur ce qui peut y être contenu.

Donc, pour cette seule raison, les langages typés dynamiquement rendent plus difficile le maintien d'une grande base de code, car le travail effectué par le compilateur "gratuitement" est maintenant un travail que vous doit faire sous la forme d'écrire des suites de tests. Si vous voulez annoter la signification de vos variables, vous doit trouver un système pour le faire, et si un nouveau membre de l'équipe le viole accidentellement, cela doit être pris en compte dans la révision du code, pas par le compilateur.

Maintenant, voici le point clé que j'ai développé: il y a une forte corrélation entre une langue à taper dynamiquement et une langue manquant également de toutes les autres fonctionnalités qui facilitent la réduction du coût de maintenance d'une grande base de code =, et ça est la raison principale pour laquelle il est plus difficile de maintenir une grande base de code dans un langage dynamique. De même, il existe une corrélation entre un langage typé statiquement et des installations qui facilitent la programmation dans le plus grand.

Prenons l'exemple de JavaScript. (J'ai travaillé sur les versions originales de JScript chez Microsoft de 1996 à 2001.) Le dessein de JavaScript était de faire danser le singe lorsque vous le survoliez. Les scripts étaient souvent une seule ligne. Nous avons considéré que dix scripts de ligne étaient assez normaux, cent scripts de ligne étaient énormes et mille scripts de ligne étaient inconnus. Le langage n'était absolument pas conçu pour la programmation à grande échelle, et nos décisions de mise en œuvre, nos objectifs de performance, etc., étaient basés sur cette hypothèse.

Étant donné que JavaScript a été spécialement conçu pour les programmes où une personne pouvait voir le tout sur une seule page, JavaScript n'est pas seulement typé dynamiquement, mais il manque également de nombreuses autres fonctionnalités couramment utilisées lors de la programmation à grande échelle:

  • Il n'y a pas de système de modularisation; il n'y a pas de classes, d'interfaces ou même d'espaces de noms. Ces éléments sont dans d'autres langues pour aider à organiser de grandes bases de code.
  • Le système d'héritage - l'héritage prototype - est à la fois faible et mal compris. Il n'est pas du tout évident de savoir comment construire correctement des prototypes pour des hiérarchies profondes (un capitaine est une sorte de pirate, un pirate est une sorte de personne, une personne est une sorte de chose ...) JavaScript.
  • Il n'y a absolument aucune encapsulation; chaque propriété de chaque objet est cédée jusqu'à for-in construct, et est modifiable à volonté par n'importe quelle partie du programme.
  • Il n'y a aucun moyen d'annoter une restriction sur le stockage; n'importe quelle variable peut contenir n'importe quelle valeur.

Mais ce n'est pas seulement le manque d'installations qui facilite la programmation dans son ensemble. Il existe également des fonctionnalités qui rendent la tâche plus difficile.

  • Le système de gestion des erreurs de JavaScript est conçu en supposant que le script s'exécute sur une page Web, que l'échec est probable, que le coût de l'échec est faible et que l'utilisateur qui voit l'échec est la personne la moins en mesure de le réparer: le l'utilisateur du navigateur, pas l'auteur du code. Par conséquent, autant d'erreurs que possible échouent silencieusement et le programme continue d'essayer de s'embrouiller. C'est une caractéristique raisonnable étant donné les objectifs du langage, mais cela rend sûrement la programmation plus difficile car elle augmente la difficulté d'écrire des cas de test. Si jamais rien échoue, il est plus difficile d'écrire des tests qui détectent l'échec!

  • Le code peut se modifier en fonction de l'entrée de l'utilisateur via des fonctionnalités telles que eval ou en ajoutant dynamiquement de nouveaux blocs script au DOM du navigateur. Tout outil d'analyse statique pourrait même ne pas savoir quel code compose le programme!

  • Etc.

De toute évidence, il est possible de surmonter ces obstacles et de créer un grand programme en JavaScript; de nombreux programmes JavaScript de plusieurs millions de lignes existent désormais. Mais les grandes équipes qui construisent ces programmes utilisent des outils et ont de la discipline pour surmonter les obstacles que JavaScript jette sur votre chemin:

  • Ils écrivent des cas de test pour chaque identifiant jamais utilisé dans le programme. Dans un monde où les fautes d'orthographe sont silencieusement ignorées, cela est nécessaire. C'est un coût.
  • Ils écrivent du code dans des langages à vérification de type et le compilent en JavaScript, tel que TypeScript.
  • Ils utilisent des cadres qui encouragent la programmation dans un style plus propice à l'analyse, plus propice à la modularisation et moins susceptible de produire des erreurs courantes.
  • Ils ont une bonne discipline sur les conventions de dénomination, sur la répartition des responsabilités, sur la surface publique d'un objet donné, etc. Encore une fois, c'est un coût; ces tâches seraient exécutées par un compilateur dans un langage typé typiquement.

En conclusion, ce n'est pas simplement la nature dynamique de la frappe qui augmente le coût de maintenance d'une grande base de code. Cela seul augmente les coûts, mais c'est loin d'être le cas. Je pourrais vous concevoir un langage qui a été typé dynamiquement mais qui avait aussi des espaces de noms, des modules, l'héritage, des bibliothèques, des membres privés, etc. - en fait, C # 4 est un tel langage - et un tel langage serait à la fois dynamique et hautement adapté à la programmation dans le grand.

C'est plutôt tout le reste qui est fréquemment manquant dans un langage dynamique qui augmente les coûts dans une grande base de code. Les langages dynamiques qui incluent également des installations pour de bons tests, pour la modularisation, la réutilisation, l'encapsulation, etc., peuvent en effet réduire les coûts lors de la programmation dans le grand, mais de nombreuses langues dynamiques fréquemment utilisées ne disposent pas de ces installations. Quelqu'un doit construire eux, et cela ajoute des coûts.

579
Eric Lippert

Parce qu'ils abandonnent délibérément certains des outils que les langages de programmation offrent pour affirmer ce que vous savez sur le code.

L'exemple le plus connu et le plus évident est le typage strict/fort/obligatoire/explicite (notez que la terminologie est très controversée, mais la plupart des gens conviennent que certaines langues sont plus strictes que d'autres). Bien utilisé, il agit comme une affirmation permanente sur le type de valeurs que vous attendez à un endroit particulier, ce qui peut faciliter le raisonnement sur le comportement possible d'une ligne, d'une routine ou d'un module, simplement parce qu'il y a moins de cas possibles . Si vous ne traitez que le nom d'une personne comme une chaîne, de nombreux codeurs sont donc prêts à taper une déclaration, à ne pas faire d'exceptions à cette règle et à accepter l'erreur de compilation occasionnelle lorsqu'ils ont fait un glissement de doigt ( oublié les citations) ou du cerveau (oublié que cette cote n'est pas censée autoriser les fractions).

D'autres pensent que cela limite leur expressivité créative, ralentit le développement et introduit un travail que le compilateur devrait faire (par exemple via l'inférence de type) ou qui n'est pas du tout nécessaire (ils se souviendront simplement de s'en tenir aux chaînes). Un problème avec cela est que les gens sont assez mauvais pour prédire le type d'erreurs qu'ils commettront: presque tout le monde surestime leur propre capacité, souvent grossièrement. Plus insidieusement, le problème s'aggrave progressivement à mesure que votre base de code est grande - la plupart des gens peuvent, en fait, se souvenir que le nom du client est une chaîne, mais ajouter 78 autres entités au mélange, toutes avec des ID, certaines avec des noms et d'autres avec une série `` nombres '', dont certains sont vraiment numériques (nécessitent un calcul pour les faire) mais d'autres dont les lettres doivent être stockées, et après un certain temps, il peut devenir assez difficile de se rappeler si le champ que vous lisez est effectivement garanti évaluer à un int ou non.

Par conséquent, de nombreuses décisions qui conviennent bien à un projet de prototype rapide fonctionnent beaucoup moins bien dans un énorme projet de production - souvent sans que personne ne remarque le point de basculement. C'est pourquoi il n'y a pas de langage, de paradigme ou de cadre unique (et pourquoi il est stupide de se demander quelle langue est la meilleure).

57
Kilian Foth

Pourquoi ne demandez-vous pas à l'auteur de cette présentation? C'est sa revendication, après tout, il devrait le sauvegarder.

Il existe de nombreux projets très vastes, très complexes et très réussis développés dans des langages dynamiques. Et il y a beaucoup d'échecs spectaculaires de projets écrits dans des langages typés statiquement (par exemple le fichier de cas virtuel du FBI).

Il est probablement vrai que les projets écrits dans des langages dynamiques ont tendance à être plus petits que les projets écrits dans des langages à typage statique, mais c'est un herring red: la plupart des projets écrits dans des langages à typage statique ont tendance à être écrits dans des langages comme Java ou C, qui ne sont pas très expressifs. Alors que la plupart des projets écrits dans des langages dynamiques ont tendance à l'être dans des langages très expressifs comme Scheme, CommonLisp, Clojure, Smalltalk, Ruby, Python.

Donc, la raison pour laquelle ces projets sont plus petits n'est pas parce que vous ne pouvez pas écrire de grands projets dans dynamique langues, c'est parce que vous n'avez pas besoin = pour écrire de grands projets dans expressif langages… il faut simplement beaucoup moins de lignes de code, beaucoup moins de complexité pour faire la même chose dans un langage plus expressif.

Les projets écrits en Haskell, par exemple, ont également tendance à être assez petits. Non pas parce que vous ne pouvez pas écrire de gros systèmes dans Haskell, mais simplement parce que vous n'avez pas have to.

Mais regardons au moins ce qu'un système de type statique a à offrir pour écrire de gros systèmes: un système de type vous empêche d'écrire certains programmes. Voilà son travail. Vous écrivez un programme, le présentez au vérificateur de type, et le vérificateur de type dit: "Non, vous ne pouvez pas écrire cela, désolé." Et en particulier, les systèmes de type sont conçus de telle manière que le vérificateur de type vous empêche d'écrire de "mauvais" programmes. Programmes contenant des erreurs. Donc, dans ce sens, oui, un système de type statique aide à développer de grands systèmes.

Cependant, il y a un problème: nous avons le problème de l'arrêt, le théorème de Rice et de nombreux autres théorèmes d'incomplétude qui nous disent essentiellement une chose: il est impossible d'écrire un vérificateur de type qui peut toujours déterminer si un programme est de type sûr ou non. Il y aura toujours un nombre infini de programmes pour lesquels le vérificateur de type ne peut pas décider s'ils sont sûrs ou non. Et il n'y a qu'une seule chose sensée à faire pour le vérificateur de type: rejeter ces programmes comme non sûrs pour le type. Et un nombre infini de ces programmes ne seront, en fait, pas de type sûr. Cependant, également un nombre infini de ces programmes sera être sûr de type! Et certains d'entre eux seront même utiles! Ainsi, le vérificateur de type vient de nous empêcher d'écrire un programme utile et sécurisé, simplement parce qu'il ne peut pas prouver sa sécurité de type.

IOW: le but d'un système de type est de limiter l'expressivité.

Mais, que se passe-t-il si l'un de ces programmes rejetés résout réellement notre problème d'une manière élégante et facile à entretenir? Ensuite, nous ne pouvons pas écrire ce programme.

Je dirais que c'est fondamentalement un compromis: les langages typés statiquement vous empêchent d'écrire de mauvais programmes au détriment de vous empêcher occasionnellement d'écrire de bons programmes. Les langages dynamiques ne vous empêchent pas d'écrire de bons programmes au détriment de ne pas vous empêcher non plus d'écrire de mauvais programmes.

L'aspect le plus important pour la maintenabilité des grands systèmes est l'expressivité, tout simplement parce que vous n'avez pas besoin de créer un système aussi grand et complexe en premier lieu.

30
Jörg W Mittag

Les types statiques explicites sont une forme correcte de documentation universellement comprise et garantie qui n'est pas disponible dans les langages dynamiques. Si cela n'est pas compensé, votre code dynamique sera simplement plus difficile à lire et à comprendre.

11
MikeFHay

Considérez une grande base de code comprenant des liaisons de base de données et une riche suite de tests et permettez-moi de souligner quelques avantages des langages statiques par rapport aux langages dynamiques. (Certains exemples peuvent être idiosyncratiques et ne s'appliquer à aucun langage statique ou dynamique.)

L'idée générale - comme d'autres l'ont souligné - est que le système de type est une "dimension" de votre programme qui expose certaines informations à des outils automatisés traitant votre programme (compilateur, outils d'analyse de code, etc.). Avec un langage dynamique, ces informations sont fondamentalement supprimées et donc non disponibles. Avec un langage statique, ces informations peuvent être utilisées pour aider à écrire des programmes corrects.

Lorsque vous corrigez un bogue, vous commencez avec un programme qui semble bon pour votre compilateur mais dont la logique est défectueuse. Lorsque vous corrigez le bogue, vous effectuez une modification corrigeant localement la logique de votre programme (par exemple au sein d'une classe) mais brisant cette logique à d'autres endroits (par exemple, les classes collaborant avec la précédente). Puisqu'un programme écrit dans un langage statique expose beaucoup plus d'informations au compilateur¹ qu'un programme écrit dans un langage dynamique, le compilateur vous aidera à localiser les autres endroits où la logique se casse plus qu'un compilateur pour un langage dynamique. Cela est dû au fait qu'une modification locale interrompra l'exactitude de type du programme à d'autres endroits, vous obligeant ainsi à corriger l'exactitude de type globalement avant d'avoir la possibilité d'exécuter à nouveau le programme.

Un langage statique applique la correction de type d'un programme, et vous pouvez supposer que toutes les erreurs de type que vous rencontrez lorsque vous travaillez sur le programme correspondraient à un échec d'exécution dans une traduction hypothétique du programme dans un langage dynamique, donc le premier a moins de bogues que ce dernier. En conséquence, cela nécessite moins de tests de couverture, moins de tests unitaires et moins de corrections de bugs, en un mot, c'est plus facile à maintenir.

Bien sûr, il y a un compromis: bien qu'il soit possible d'exposer beaucoup d'informations dans le système de type et donc de saisir la chance d'écrire des programmes fiables, il peut être difficile de combiner cela avec une ~ API flexible.

Voici quelques exemples d'informations que l'on peut encoder dans le système de type:

- Const correctness le compilateur peut garantir qu'une valeur est passée en "lecture seule" à une procédure.²

- schéma de base de données le compilateur peut garantir que le code liant le programme à une base de données correspond à la définition de la base de données. Ceci est très utile lorsque cette définition change. (Entretien!)

- Ressources système le compilateur peut garantir que le code utilisant une ressource système ne le fait que lorsque la ressource est dans le bon état. Par exemple, il est possible de coder l'attribut close ou open d'un fichier dans le système de type.

¹ Il n'est pas utile de distinguer ici un compilateur et un interprète, si une telle différence existe.

6
user40989

Parce que le typage statique permet un meilleur outillage, ce qui améliore la productivité d'un programmeur lorsqu'il essaie de comprendre, de refactoriser ou d'étendre une grande base de code existante.

Par exemple, dans un grand programme, nous aurons probablement plusieurs méthodes avec le même nom. Par exemple, nous pourrions avoir une méthode add qui ajoute quelque chose à un ensemble, une autre qui ajoute deux entiers, une autre qui dépose de l'argent sur un compte bancaire, ...). Dans les petits programmes, de telles collisions de noms sont peu susceptibles de se produire. Dans les grands programmes travaillés par plusieurs personnes, ils se produisent naturellement.

Dans un langage typé statiquement, ces méthodes peuvent être distinguées par les types sur lesquels elles opèrent. En particulier, un environnement de développement peut découvrir, pour chaque expression d'appel de méthode, quelle méthode est invoquée, ce qui lui permet d'afficher une info-bulle avec la documentation de cette méthode, de trouver tous les sites d'appel pour une méthode ou de prendre en charge les refactorings (tels que l'inlining de méthode, renommer la méthode, modifier la liste des paramètres, ...).

4
meriton