C++ : le principe NVI (Non-Virtual Interface)
Rédigé par Nicolas K Aucun commentaireUn principe fondamental de la programmation orientée objet : la séparation des interfaces et des implémentations. Qu’est-ce qu’une interface ? Il s’agit d’un ensemble de méthodes accessibles depuis l’extérieur qui permettent de modifier l’état interne d’un objet.
Dans ce tuto, nous allons voir que le principe NVI pour Non-Virtual Interface peut nous aider à avoir un code plus robuste et plus en phase avec l’encapsulation : meilleure séparation entre l’interface et les détails d’implémentation, meilleure séparation des responsabilités, impossibilité pour une classe fille de "casser" une fonctionnalité de la classe mère et maintenances et évolutions facilitées.
Exemple support
Prenons un exemple simple pour illustrer le principe NVI : imaginons une classe Animal et ses classes filles Chat et Chien. Traditionnellement, on organise notre code comme suit :
#include <iostream>
#include <string>
class Animal {
public:
Animal(std::string_view p_nom) : nom{p_nom} {}
virtual void seManifester() = 0;
protected:
std::string nom;
};
class Chat : public Animal {
public:
Chat(std::string_view p_nom) : Animal{p_nom} {};
virtual void seManifester() override {
std::cout << nom << " : Miaou" << std::endl;
}
};
class Chien : public Animal {
public:
Chien(std::string_view p_nom) : Animal{p_nom} {};
virtual void seManifester() override {
std::cout << nom << " : Ouaf" << std::endl;
}
};
int main() {
auto chat = Chat{"Félix"};
auto chien = Chien{"Caramel"};
chat.seManifester();
chien.seManifester();
}
Les problèmes de ce code
Mais ce code pose un problème majeur : la classe mère n’impose pas le format de la manifestation de l’animal ("nom : [cri]"). On peut alors modifier le code pour imposer ce format :
#include <iostream>
#include <string>
class Animal {
public:
Animal(std::string_view p_nom) : nom{p_nom} {}
virtual void seManifester() {
std::cout << nom << " : ";
crier();
std::cout << std::endl;
}
virtual void crier() = 0;
protected:
std::string nom;
};
class Chat : public Animal {
public:
Chat(std::string_view p_nom) : Animal{p_nom} {};
virtual void crier() override {
std::cout << "Miaou";
}
};
class Chien : public Animal {
public:
Chien(std::string_view p_nom) : Animal{p_nom} {};
virtual void crier() override {
std::cout << "Ouaf";
}
};
int main() {
auto chat = Chat{"Félix"};
auto chien = Chien{"Caramel"};
chat.seManifester();
chien.seManifester();
}
Ok. Mais les classes Chat et Chien peuvent toujours redéfinir la méthode seManifester
puisqu’elle est virtuelle. Retirons donc le mot clé virtual
. Par ailleurs, on a ajouté une méthode crier
à Animal
qu’on a mis par défaut à public
: est-ce vraiment volontaire d’avoir modifié l’interface de notre classe ? Peut-être pas. Mettons-la plutôt private
. Et séparons les responsabilités : les classes filles n’ont pas à gérer l’attribut nom
: mettons-le également private
. On obtient :
#include <iostream>
#include <string>
class Animal {
public:
Animal(std::string_view p_nom) : nom{p_nom} {}
void seManifester() {
std::cout << nom << " : ";
crier();
std::cout << std::endl;
}
private:
std::string nom;
virtual void crier() = 0;
};
class Chat : public Animal {
public:
Chat(std::string_view p_nom) : Animal{p_nom} {};
private:
virtual void crier() override {
std::cout << "Miaou";
}
};
class Chien : public Animal {
public:
Chien(std::string_view p_nom) : Animal{p_nom} {};
private:
virtual void crier() override {
std::cout << "Ouaf";
}
};
int main() {
auto chat = Chat{"Félix"};
auto chien = Chien{"Caramel"};
chat.seManifester();
chien.seManifester();
}
Rien de bien compliqué n’est-ce pas ? Eh bien nous venons d’utiliser le principe NVI ou Non-Virtual Interface.
Définition du NVI
Le principe NVI, proposé par Herb Sutter, repose sur 4 règles :
- les interfaces publiques ne doivent pas être virtuelles,
- par défaut, les méthodes virtuelles doivent être privées,
- si l’implémentation de la classe mère doit être appelée dans les classes filles, une méthode virtuelle peut être protected,
- un destructeur doit être soit public et virtuel soit protected et pas virtuel.
Les avantages du NVI
C’est beau les principes, mais pourquoi : quelles raisons poussent à utiliser ce principe NVI ?
- Le code gagne en robustesse : la classe mère peut imposer de cette façon les sous-process qui composent l’implémentation d’une méthode.
void methodePublique() { methodePubliqueEtape1(); // privée virtuelle methodePubliqueEtape2(); // privée virtuelle }
- Le code gagne en évolutivité : puisque tout passe par la méthode publique de la classe mère (qui ne peut plus être redéfinie), on peut très facilement ajouter des assertions, ajouter des logs, ajouter un comportement à toutes les classes filles sans y toucher.
// pseudo-code : void methodePublique() { log("debut méthode publique"); if(!preconditions) throw PreconditionException{}; implementation(); // privée virtuelle if(!postconditions) throw PostconditionException{}; log("fin méthode publique"); }
- Le code gagne en clarté : l’interface publique n’est pas "polluée" par des méthodes qui font partie de l’implémentation.
Conclusion
Comme d’habitude, on peut gagner en robustesse, évolutivité et clarté en respectant certains principes simples. Bien sûr, comme toute règle et comme tout bon principe, il existe des situations où il n’est pas judicieux de l’utiliser !
Et vous, que pensez-vous de ce principe ?