Blog 6 min de lecture

Quelques bonnes pratiques C++ tirées du code source d’OpenCV

Share this article
Quelques bonnes pratiques C++ tirées du code source d’OpenCV

OpenCV (Open Source Computer Vision) est une bibliothèque de fonctions de programmation destinée principalement à la vision par ordinateur temps réel, développée par le centre de recherche d'Intel à Nijni Novgorod, en Russie. La bibliothèque est multiplateforme et se concentre principalement sur le traitement d'images en temps réel.

OpenCV est largement utilisé et adopté dans le monde entier. Pour les utilisateurs finaux, il est mature et puissant ; pour les développeurs, il est bien implémenté et bien conçu. Les développeurs d'OpenCV suivent des principes fondamentaux qui rendent le code simple à comprendre et à maintenir.

Découvrons 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 rend les fonctionnalités fournies plus faciles et plus flexibles à réutiliser et à intégrer dans d'autres projets. De plus, l'architecture basée sur des bibliothèques encourage des API propres et la séparation des préoccupations, rendant le code plus facile à comprendre pour les développeurs car ils n'ont à se concentrer que sur de petites parties de l'ensemble.

OpenCV adopte cette approche en définissant plusieurs bibliothèques, chacune avec une responsabilité spécifique, qui utilisent toutes la bibliothèque opencv_core.

opencv1

2- Modulariser par espaces de noms

OpenCV fait un usage extensif des espaces de noms pour modulariser sa base de code. Par exemple, voici les espaces de noms du projet opencv_core :

opencv2

OpenCV utilise l'approche « espace de noms par fonctionnalité ». Cette approche utilise les espaces de noms pour refléter l'ensemble des fonctionnalités. Elle place tous les éléments liés à une même 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 travaillent étroitement ensemble sont placés côte à côte.

Dans OpenCV, les espaces de noms sont utilisés pour trois raisons principales :

  • Modulariser les bibliothèques.
  • Masquer les détails d'implémentation, comme avec l'espace de noms « cv::detail ». Cette approche indique clairement aux utilisateurs de la bibliothèque que les types de cet espace de noms sont destinés à un usage interne et ne doivent pas être utilisés directement. En C#, le mot-clé « internal » fait le travail, mais en C++ il n'y a aucun moyen de masquer les types publics à l'utilisateur de la bibliothèque.
  • L'espace de noms anonyme : un espace de noms sans nom. Il évite le recours à des variables statiques globales. L'espace de noms anonyme que vous créez n'est accessible que dans le fichier où vous l'avez créé.

Définir le modèle de données avec des types POD

Chaque projet a son modèle de données, et il est recommandé de définir ces données comme des types POD.

Recherchons dans la base de code d'OpenCV les structs sans méthodes et avec uniquement des champs. Pour cela, CQLinq sera utilisé pour interroger la base de code.

opencv3

Les résultats de cette requête couvrent 25 % des types définis dans les projets OpenCV. OpenCV définit presque tout son modèle de données sous forme de structs avec uniquement 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.

Recherchons les classes qui héritent de plus d'une classe de base concrète dans la base de code d'OpenCV.

opencv4

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 peuvent servir à détecter les fonctions complexes. NBLinesOfCode, le nombre de paramètres et le nombre de variables locales figurent parmi 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 qui reflète le nombre de décisions pouvant être prises au sein d'une procédure.
  • Nesting Depth est une métrique au niveau méthode qui représente la profondeur maximale des portées imbriquées dans le corps d’une méthode.
  • Max Nested Loops correspond au niveau maximal d'imbrication des boucles dans une fonction.

La valeur maximale tolérée pour ces métriques dépend surtout des choix de l’équipe ; il n’existe pas de valeurs standard.

Recherchons les méthodes qui pourraient être considérées comme complexes dans la base de code d'OpenCV.

opencv5

Seulement 1 % sont candidates à une refactorisation pour réduire leur complexité.

Le couplage

Un faible couplage est souhaitable car un changement dans une zone d'une application nécessitera moins de changements dans toute l'application. À long terme, cela peut faire gagner beaucoup de temps, d'efforts et réduire les coûts liés à la modification et à l'ajout de nouvelles fonctionnalités.

Un faible couplage peut être obtenu en utilisant des classes abstraites. Voici trois avantages clés de l'utilisation des classes abstraites :

  • Une classe abstraite offre un moyen de définir un contrat qui favorise la réutilisabilité. Si un objet implémente une classe abstraite, alors cet objet doit se conformer à un standard. Un objet qui utilise un autre objet est appelé un consommateur. Une classe abstraite est un contrat entre un objet et son consommateur.
  • Une classe abstraite fournit aussi un niveau d'abstraction qui rend les programmes plus faciles à comprendre. Une classe abstraite permet aux développeurs de raisonner sur le comportement général du code sans avoir à plonger dans les détails d'implémentation.
  • Une classe abstraite garantit un faible couplage entre les composants, ce qui permet de protéger facilement le consommateur de la classe abstraite de tout changement d'implémentation dans les classes qui l'implémentent.

Recherchons toutes les classes abstraites définies par OpenCV :

opencv6

Si notre objectif principal est de garantir un faible couplage, il y a une erreur courante avec les classes abstraites qui peut ruiner l'intérêt de leur utilisation : utiliser les classes concrètes au lieu des classes abstraites. Pour illustrer ce problème, considérons 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;
 };

Au lieu de référencer la classe abstraite IA, la classe C référence la classe A. Dans ce cas, nous perdons le bénéfice du faible couplage. Cette implémentation a 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 aussi le bénéfice du contrat offert par les interfaces.

C# a introduit dans le langage la capacité d' implémentation explicite d'interface pour garantir qu'une méthode de IA ne sera jamais appelée depuis une référence vers une classe concrète, mais seulement depuis une référence vers l'interface. Cette technique est très utile pour empêcher les développeurs de perdre les avantages de l'utilisation des interfaces.

La cohésion

Le principe de responsabilité unique stipule 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. LCOM prend ses valeurs dans l'intervalle [0-1]. LCOM HS (HS pour Henderson-Sellers) prend ses valeurs dans l'intervalle [0-2]. Une valeur LCOM HS supérieure à 1 devrait ê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, cela inclut aussi 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 tous les 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 LCOMHS supérieure à 1 devrait être considérée comme alarmante.

opencv8

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 : pas de concepts de conception avancés ni de sur-ingénierie — juste quelques principes fondamentaux appliqués de manière cohérente.

Share this article