Organiser une grande base de code: guide pratique et complet

Organiser une grande base de code: guide pratique et complet

Plonger dans une base de code massive, c'est un peu comme explorer une jungle épaisse. Chaque fichier, chaque ligne de code est une liane à démêler. Mais avec les bonnes pratiques, même les plus grandes forêts de code deviennent explorables. Voici un guide pour organiser vos bases de code, agrémenté d'exemples, pour vous aider à garder le cap.



Les Nommages: Fonctions, Variables

Les prefix dans le Nommage

Quand il s'agit de nommer vos fonctions, variables et fichiers, il existe différentes écoles de pensée. Prenons l'exemple de Microsoft dans les années 80, où les variables étaient nommées avec le type inclus dans le nom, comme strName ou intCount. Cette méthode de nommage, nommée Hungarian permettait de savoir immédiatement quel type de donnée vous manipuliez. Cependant, avec les IDE modernes, cette pratique est devenue un peu obsolète. Pourquoi ? Parce que les IDE permettent de connaître le type d'une variable simplement en pointant la souris dessus. Alors, faut-il encore inclure le type dans le nom ? Pas nécessairement. Aujourd'hui, des noms de variables clairs et explicites sont préférés. Ce type de préfixe reste cependant courant dans de nombreux styles de codage (Google, LLVM par exemple), notamment pour indiquer qu'une variable est globale.

Les Bonnes Pratiques de Nommage

Voici quelques bonnes pratiques pour le nommage, tirées du livre "Clean Code" de Robert C. Martin :

  • Utilisez des noms significatifs : Préférez customerAge à custAge.
  • Évitez les abréviations non standard : Utilisez appointment plutôt que appt.
  • Soyez cohérent : Si vous commencez avec un certain style de nommage, utilisez-le partout.
  • Nommez selon le contexte : Assurez-vous que le nom de votre variable ou fonction soit compréhensible dans son contexte.
  • Utilisez un verbe pour les fonctions : Assurez-vous que les noms des fonctions décrivent clairement l'action qu'elles accomplissent, par exemple calculateTotal ou fetchData.

Ces pratiques assurent que votre code est facile à lire et à maintenir.

Les Types de Nommages

Les noms doivent être clairs, descriptifs et suivis de conventions spécifiques. Voici quelques approches courantes utilisées dans divers langages :

  • CamelCase : Utilisée par de nombreux langages comme JavaScript (myVariableName).
  • snake_case : Très populaire en Python (my_variable_name).
  • PascalCase : Utilisée souvent pour les noms de classes en C# (MyClassName). kebab-case : Courant pour les noms de fichiers et URLs (my-file-name).
  • SCREAMING_SNAKE_CASE : Utilisée pour les constantes en JavaScript (MY_CONSTANT).
  • UpperCamelCase : Souvent utilisé en Java pour les noms de classes (MyClassName).
  • lowercase : Utilisé dans certains langages comme SQL pour les noms de table (table_name).
  • mMixedCase : Utilisé dans certaines bases de code héritées ou spécifiques (mMyVariable).

il est généralement recommandé que les développeurs suivent les conventions de nommage de la bibliothèque standard du langage. Cela favorise la cohérence et la lisibilité du code, car les noms et les styles seront uniformes à travers tout le codebase, facilitant ainsi la collaboration et la maintenance.

En C++, la bibliothèque standard utilise un style de case particulier qui diffère souvent de celui utilisé par les développeurs. La stdlib utilise principalement snake_case, alors que beaucoup de développeurs C++ préfèrent CamelCase ou PascalCase pour leurs propres variables et fonctions. Cette disparité peut créer une certaine confusion, mais c'est un exemple de comment les conventions peuvent varier même au sein d'un même écosystème de langage.

Go a une particularité intéressante : la visibilité d'une variable est déterminée par sa case. Les noms commençant par une majuscule sont exportés (publics), tandis que ceux commençant par une minuscule restent privés au package. CamelCase pour l'export : PublicVariable snake_case pour l'interne : private_variable

Cette convention simple permet de savoir instantanément si une variable est accessible en dehors de son package.

OOP ou pas OOP ?

Projets de Bas Niveau

Le choix entre la programmation orientée objet (OOP) et la programmation procédurale dépend largement du projet en question. Comme l'expliquent des experts en génie logiciel, tels que Robert C. Martin et Grady Booch, l'OOP offre des avantages en termes de modularité et de réutilisation du code, mais elle peut également introduire une complexité supplémentaire, particulièrement dans les projets de bas niveau. Pour les systèmes de bas niveau, où la performance et la gestion fine de la mémoire sont critiques, les langages procéduraux sont souvent privilégiés. Par exemple, Linux utilise principalement le C, un langage procédural. Comme le souligne John Lions dans son commentaire du code source de Unix, le C est idéal pour les opérations de bas niveau grâce à sa proximité avec le matériel et sa capacité à manipuler directement les ressources système.

Projets Collaboratifs Open Source

Dans les projets collaboratifs open source, la simplicité et la clarté du code sont essentielles pour faciliter la contribution et la maintenance par une large communauté. Mozilla, par exemple, utilise Rust, un langage orienté système et non OOP. Rust est conçu pour offrir des garanties de sécurité et de gestion de la mémoire sans la complexité de l'OOP, ce qui, comme le mentionne Steve Klabnik dans "The Rust Programming Language", facilite la collaboration ouverte et réduit les erreurs. De même, Google utilise Go, un langage orienté système. Go est connu pour sa simplicité et son efficacité, comme décrit par Donovan et Kernighan dans "The Go Programming Language". Cette simplicité aide à maintenir un codebase propre et facile à comprendre, ce qui est essentiel pour les grands projets collaboratifs.

Applications Extensibles

À l'inverse, pour des applications extensibles, l'OOP peut offrir des avantages significatifs. Apple a crée Objective-C et Swift en pensant avant tout à l'OOP, ce qui, selon Chris Lattner, le créateur de Swift, facilite la modularité, la réutilisation du code et le développement rapide d'interfaces utilisateur complexes.

La Frontière entre OOP et non OOP

Il est important de noter que la frontière entre OOP et non OOP est assez floue. De nombreux langages intègrent des concepts des deux paradigmes, et il est possible de programmer de manière procédurale dans un langage orienté objet, et vice versa. Cependant, l'implicite et l'ambiguïté dans le code peuvent nuire à la collaboration, en particulier dans les projets open source. Des fonctionnalités telles que les templates, les macros avancés, et la surcharge d'opérateur peuvent introduire des niveaux de complexité et d'ambiguïté qui rendent le code difficile à comprendre et à maintenir pour les nouveaux contributeurs. Comme le mentionne Bjarne Stroustrup dans "The C++ Programming Language", bien que ces fonctionnalités soient puissantes, elles doivent être utilisées avec parcimonie et avec une documentation claire pour éviter de rendre le code inaccessible.

Pour trancher

Pour trancher, il est souvent compliqué d’hérité des classes de base du language; à cause de comportements imprévues ou indéfini de manière propre; notamment en C++. Face à une structure de données complexe et difficilement réutilisable dans d’autre projet; utilisant en premier lieu une classe de la librairie standard; il est préférable d’utiliser des fonctions procédurales. (CF, merge_ast_matches)

Et donc ?

En conclusion, comme le résume Bertrand Meyer dans "Object-Oriented Software Construction", le choix entre OOP et procédural doit être guidé par la nature du projet, les besoins de performance, la complexité du système, et la dynamique de l'équipe de développement. L'OOP peut être plus adapté aux applications nécessitant une structure modulaire et réutilisable, tandis que la programmation procédurale peut offrir des avantages en termes de performance et de simplicité pour les projets de bas niveau et les collaborations open source. Autoriser l'OOP mais limiter l'utilisation de certaines fonctionnalités avancées du langage ou du préprocesseur dans le cadre d'un Coding Style strict peut également aider à rendre les projets plus maintenables et accessibles. Les revues aident à gagner cette clarté.

Bien Découper son Code avec des Design Patterns

Le Découpage en Fichiers

Le découpage du code est crucial pour maintenir un projet structuré et lisible. Java, par exemple, impose une classe par fichier. Voici quelques règles de base à considérer :

  • Fonctions par fichier : Limitez le nombre de fonctions par fichier pour éviter le chaos et faciliter la navigation dans le code.
  • Lignes par fonction : Essayez de ne pas dépasser 50 lignes par fonction pour garder le code lisible. Certains préconisent même des fonctions très courtes.

Par exemple, l'école 42 impose des règles strictes : 25 lignes par fonction et 5 fonctions par fichier.

Des auteurs comme Robert C. Martin insistent sur le fait qu'une fonction devrait accomplir une seule tâche et être aussi concise que possible, parfois même réduite à une seule ligne de code. De plus, il est recommandé de ne pas mélanger les niveaux de complexité dans les fonctions : la fonction est-elle de bas niveau ? De haut niveau ? Est-elle bien découpée en appels vers des fonctions primitives ? Chaque fonction devrait avoir un niveau de complexité et un comportement bien définis pour éviter toute confusion et faciliter la maintenance du code.

Les Design Patterns

Quelques exemples communs :

  • Pattern Visitor : Utilisé pour séparer les algorithmes de la structure des objets; utilisé par exemple dans les parsers/lexers générés par ANTLR.
  • Pattern Décorateur : Idéal pour ajouter des fonctionnalités à des objets sans modifier leur structure de base.
  • Pattern Registre : Idéal pour agréger des types entre eux sans avoir à maintenir la liste des types à deux endroits.
  • ...

La réflexion peut aussi épauler les design patterns. La réflexion est la capacité d'un langage à s'analyser lui-même; cette capacité peut être très puissante. Java avec un module ou PHP natif pour ne citer qu'eux, peuvent automatiser le Pattern Registre en listant toutes les classes enfants d'une autre classe. On ainsi par exemple lister les classes décorés.

Les design patterns peuvent directement influencer la structuration du code en fichiers ou en classes. À noter que les design patterns permettent souvent de réduire le volume de code à maintenir. Il est cependant important de s'assurer que le coût en runtime ne soit pas trop impacté. Il est souvent possible d'effectuer ce type de calculs (comme la construction d'un registre ou la détection des classes décorées) pendant la phase de compilation.

L'Utilisation des Outils de Versionning

Git et ses Bonnes Pratiques

L'utilisation de Git est essentielle pour gérer une base de code massive. Voici quelques conseils :

  • .gitignore : Assurez-vous de bien ignorer les fichiers temporaires et de configuration.
  • Gestionnaires de paquets : Utilisez des gestionnaires comme npm ou Composer pour gérer vos dépendances sans inclure leurs fichiers dans le dépôt. Cela allège le projet et permet une installation propre à partir des fichiers de configuration. Il est important de bien ajouter au gitignore les dossiers générés par votre gestionnaire de paquets, au risque d'alourdir inutilement votre git voir d'y exposer des secrets.
  • L'importance des submodules : Les submodules sont utiles pour gérer des dépendances dans vos projets indépendamment des gestionnaires de paquets. Ils permettent de lier des répertoires externes sans les inclure directement dans le dépôt principal, facilitant ainsi la gestion des mises à jour, des versions, et la réutilisation du code entre plusieurs projets.
  • Fichiers .env : Stockez vos variables d'environnement et secrets dans des fichiers sécurisés, jamais dans le dépôt de code (voir la suite).

Gestion des Secrets

La notion de secrets est cruciale pour l'intégration et le déploiement continus (CI/CD). Les fournisseurs de services comme GitHub et GitLab proposent des solutions dédiées pour gérer les secrets de manière sécurisée. Par exemple :

  • GitHub Secrets : Permet de stocker des secrets et de les utiliser dans les workflows GitHub Actions.
  • GitLab CI/CD Secrets : Fournit des moyens similaires pour les pipelines GitLab CI/CD. D'autres solutions existent également pour Kubernetes, comme Kubernetes Secrets, qui permettent de gérer les informations sensibles de manière sécurisée dans les environnements Kubernetes.

Important : un secret ajouté par erreur à l'historique GIT requiert de réécrire l'historique GIT pour y supprimer les secrets. Nous vous recommandons BFG Repo-Cleaner pour cela.


Avec ces bonnes pratiques et ces exemples, vous devriez être prêt à organiser et gérer efficacement même les plus grandes bases de code. Nous pouvons bien-sûr vous y aider au besoin.

Bonne chance et happy coding !