En tant que programmeurs, nous sommes souvent tentés d'exploiter les design patterns, les idiomes du langage, ses fonctionnalités avancées et les bibliothèques réputées, ce qui est certes recommandable. Toutefois, il est essentiel d'examiner ces techniques à la lumière des principes KISS et YAGNI avant de se lancer.

« KISS » signifie « Keep It Simple, Stupid » (reste simple, bêtement simple). C'est un principe de conception qui énonce que la simplicité doit être un objectif clé et que la complexité inutile doit être évitée. L'idée est que les solutions simples sont plus faciles à comprendre, à maintenir et à déboguer. Le principe KISS est largement appliqué dans divers domaines, dont l'ingénierie, le développement logiciel, la conception d'interfaces utilisateur et la gestion de projet.
YAGNI signifie « You Ain't Gonna Need It » (tu n'en auras pas besoin). C'est un principe du développement logiciel et des méthodologies agiles qui invite les développeurs à ne pas ajouter de fonctionnalités à leur base de code tant qu'elles ne sont pas réellement nécessaires pour résoudre un problème précis ou répondre à une exigence.
Le principe YAGNI repose sur l'idée qu'ajouter prématurément des fonctionnalités inutiles peut entraîner plusieurs problèmes potentiels.
OpenCV (Open Source Computer Vision) est une bibliothèque de fonctions de programmation principalement destinée à la vision par ordinateur en temps réel, développée par le centre de recherche d'Intel Russie à Nijni Novgorod. La bibliothèque est multiplateforme. Elle se concentre essentiellement sur le traitement d'images en temps réel.
OpenCV est un vaste projet comportant de nombreuses fonctionnalités complexes. Néanmoins, les développeurs d'OpenCV ont adopté des principes fondamentaux qui ont rendu la base de code nettement plus facile à comprendre et à maintenir.
Explorons quelques-uns des choix de conception d'OpenCV :
Modularité
1 - Une architecture basée sur des bibliothèques
Une architecture basée sur des bibliothèques facilite la réutilisation des fonctionnalités fournies et leur intégration dans d'autres projets. De plus, elle encourage des API propres et la séparation des préoccupations, rendant le système plus facile à comprendre car les développeurs peuvent se concentrer sur de petites parties d'un ensemble plus vaste.
OpenCV adopte cette approche et définit de nombreuses bibliothèques ; chacune a une responsabilité précise, et toutes utilisent la bibliothèque opencv_core.

2 - Modulariser par espaces de noms
OpenCV utilise abondamment les espaces de noms pour organiser efficacement sa base de code. Voici quelques exemples d'espaces de noms employés au sein du projet opencv_core :

OpenCV applique l'approche « Namespace-by-feature » (un espace de noms par fonctionnalité). Cette approche utilise les espaces de noms pour refléter l'ensemble des fonctionnalités. Elle regroupe tous les éléments liés à une seule fonctionnalité (et uniquement celle-ci) dans un même espace de noms. Il en résulte des espaces de noms à forte cohésion et forte modularité, avec un couplage minimal entre eux. Les éléments qui collaborent étroitement sont placés côte à côte.
Dans le cas d'OpenCV, les espaces de noms sont utilisés pour trois raisons principales :
- Modulariser les bibliothèques.
- Masquer les détails, comme pour l'espace de noms « cv::detail ». Cette approche est très intéressante si l'on veut indiquer à l'utilisateur de la bibliothèque qu'il n'a pas besoin d'utiliser directement les types de cet espace de noms, réservé à un usage interne. En C#, le mot-clé « internal » remplit ce rôle, mais en C++ il n'existe aucun moyen de masquer les types publics à l'utilisateur de la bibliothèque.
- Espace de noms anonyme : un espace de noms sans nom. Il évite de créer des variables statiques globales. L'espace de noms « anonyme » que vous avez créé ne sera accessible que dans le fichier où vous l'avez défini.
Définir le modèle de données avec des types POD
Chaque projet possède son modèle de données, et nous pouvons définir ce modèle à l'aide des types POD (plain old data). Un type POD est une structure de données représentée uniquement comme une collection passive de valeurs de champs (variables d'instance), sans recourir aux fonctionnalités orientées objet. Les avantages des types POD en programmation sont nombreux, notamment :
- Efficacité: les types POD ont généralement une disposition mémoire simple, ce qui conduit souvent à une utilisation de la mémoire plus efficace et à de meilleures performances. Ils évitent la surcharge associée aux structures de données complexes et aux fonctions membres.
- Compatibilité: les types POD sont compatibles avec les constructions de programmation de bas niveau et les formats d'échange de données, ce qui les rend adaptés à l'interfaçage avec des systèmes et des langages externes.
- Interopérabilité: les types POD peuvent être facilement transmis entre différents modules ou composants d'un système, ainsi qu'entre différents systèmes ou langages de programmation, ce qui facilite l'interopérabilité.
- Facilité d'utilisation: les types POD sont simples à manipuler et à comprendre, car ils représentent généralement des types de données de base ou des agrégats de tels types.
- Performances: les types POD offrent souvent de meilleures performances, tant en vitesse d'exécution qu'en utilisation mémoire, par rapport à des types de données plus complexes.
- Prévisibilité: comme les types POD ont une structure simple et bien définie, leur comportement est généralement plus prévisible, ce qui facilite le débogage et l'optimisation.
Dans l'ensemble, l'utilisation de types POD peut contribuer à un code plus simple, plus efficace et plus maintenable, en particulier dans les environnements critiques en performances ou aux ressources limitées.
Recherchons dans la base de code d'OpenCV les structs qui n'ont aucune méthode et ne contiennent que des champs.

Le résultat de cette requête concerne 25 % des types définis dans les projets OpenCV. OpenCV définit la quasi-totalité de son modèle de données dans des structs ne contenant que des champs.
Éviter l'héritage multiple
L'héritage multiple peut compliquer une conception et rendre le débogage plus difficile, c'est pourquoi de nombreux experts C++ recommandent de l'éviter.
Cherchons dans la base de code d'OpenCV les classes qui héritent de plus d'une classe de base concrète.

Seules quelques classes des projets de test utilisent l'héritage multiple ; ce concept est évité dans toute la base de code d'OpenCV.
Éviter de définir des fonctions complexes
De nombreuses métriques permettent de détecter les fonctions complexes ; NBLinesOfCode, le nombre de paramètres et le nombre de variables locales sont les plus basiques.
Il existe d’autres métriques intéressantes pour détecter les fonctions complexes :
- La complexité cyclomatique est une métrique logicielle procédurale populaire, égale au nombre de décisions pouvant être prises dans une procédure.
- La profondeur d'imbrication (Nesting Depth) est une métrique définie sur les méthodes, relative à la profondeur maximale de la portée la plus imbriquée dans le corps d'une méthode.
- Max Nested loop correspond au niveau maximal d'imbrication des boucles dans une fonction.
Les valeurs maximales acceptables pour ces métriques dépendent des choix de l'équipe ; il n'existe pas de seuils universels.
Recherchons les méthodes qui pourraient être considérées comme complexes dans la base de code d'OpenCV.

Seules 1 % sont candidates à une refactorisation pour réduire leur complexité.
Coupling
Un faible couplage est souhaitable, car une modification dans une partie d'une application nécessitera moins de changements dans l'ensemble de l'application. À long terme, cela peut faire gagner un temps, des efforts et un coût considérables lors de la modification d'une application ou de l'ajout de nouvelles fonctionnalités.
Un faible couplage peut être obtenu en utilisant des classes abstraites. Voici trois avantages clés qui en découlent :
- Une classe abstraite offre un moyen de définir un contrat qui favorise la réutilisation. Si un objet implémente une classe abstraite, alors cet objet doit se conformer à un standard. Un objet qui en utilise un autre est appelé un consommateur. Une classe abstraite est un contrat entre un objet et son consommateur.
- Une classe abstraite fournit également un niveau d'abstraction qui rend les programmes plus faciles à comprendre. Elle permet aux développeurs de parler du comportement général du code sans entrer dans une multitude de détails spécifiques.
- Une classe abstraite impose un faible couplage entre les composants, ce qui permet de protéger facilement le consommateur de la classe abstraite de toute modification d'implémentation dans les classes qui l'implémentent.
Recherchons toutes les classes abstraites définies par OpenCV :

Si notre objectif premier est d'imposer un faible couplage, il existe une erreur courante lors de l'utilisation des classes abstraites qui peut anéantir tout leur intérêt : utiliser des classes concrètes au lieu des classes abstraites. Pour mieux expliquer ce problème, prenons l'exemple suivant :
La classe A implémente la classe abstraite IA, qui contient la méthode calculate(). La classe consommatrice C est implémentée comme ceci :
public class C
{
….
public:
void calculate()
{
…..
m_a->calculate();
….
}
A* m_a;
};
La classe C, au lieu de référencer la classe abstraite IA, référence la classe A. Dans ce cas, nous perdons le bénéfice du faible couplage. Cette implémentation présente deux inconvénients majeurs :
- Si nous décidons d'utiliser une autre implémentation de IA, nous devons modifier le code de la classe C.
- Si des méthodes sont ajoutées à A sans exister dans IA, et que C les utilise, nous perdons également le bénéfice du contrat qu'offrent les interfaces.
C# a introduit la fonctionnalité d' implémentation explicite d'interface dans le langage pour garantir qu'une méthode de IA ne sera jamais appelée depuis une référence vers une classe concrète, mais uniquement depuis une référence vers l'interface. Cette technique est très utile pour éviter que les développeurs ne perdent le bénéfice de l'utilisation des interfaces.
Cohésion
Le principe de responsabilité unique énonce qu'une classe ne devrait pas avoir plus d'une raison de changer. Une telle classe est dite cohésive. Une valeur LCOM élevée signale généralement une classe peu cohésive. Il existe plusieurs métriques LCOM. Le LCOM prend ses valeurs dans l'intervalle [0-1]. Le LCOM HS (HS pour Henderson-Sellers) prend ses valeurs dans l'intervalle [0-2]. Une valeur LCOM HS supérieure à 1 doit être considérée comme alarmante. Voici comment calculer les métriques LCOM :
LCOM = 1 – (sum(MF)/M*F)
LCOM HS = (M – sum(MF)/F)(M-1)
Où :
- M est le nombre de méthodes de la classe (les méthodes statiques et d'instance sont comptées, y compris les constructeurs, les getters/setters de propriétés, les méthodes add/remove d'événements).
- F est le nombre de champs d’instance de la classe.
- MF est le nombre de méthodes de la classe accédant à un champ d’instance donné.
- Sum(MF) est la somme des MF sur l'ensemble des champs d'instance de la classe.
L'idée sous-jacente à ces formules peut s'énoncer ainsi : une classe est totalement cohésive si toutes ses méthodes utilisent tous ses champs d'instance, ce qui signifie que sum(MF)=M*F, et donc LCOM = 0 et LCOMHS = 0.
Une valeur LCOM HS supérieure à 1 doit être considérée comme alarmante.

Seuls quelques types ne sont pas cohésifs.
Conclusion
Si vous jetez un œil au code source d'OpenCV, vous serez surpris par la simplicité de son implémentation : aucun concept de conception inutilement avancé, aucune sur-ingénierie — simplement de sains principes de base appliqués de manière cohérente.
