Par l'extérieur !
Une bonne interface fournit une vue simplifiée exprimée dans le vocabulaire de l'utilisateur. Dans le cas de la programmation par objets,
une interface est généralement représentée par une classe unique ou par un groupe de classes très proches.
Réfléchissez d'abord à ce qu'un objet de la classe est du point de vue logique, plutôt que de réfléchir à la façon dont vous allez le
représenter physiquement. Imaginez par exemple que vous ayez une classe Stack (une pile) et que vous vouliez que son implémentation
utilise une LinkedList (une liste chaînée)
class Stack {
public:
private:
LinkedList list_;
}; |
La classe Stack doit-elle avoir une fonction membre get() qui retourne la LinkedList ? Ou une fonction set() qui prenne une LinkedList ? Ou encore
une constructeur qui prenne une LinkedList ? La réponse est évidemment non, puisque la conception d'une classe doit s'effectuer de
l'extérieur vers l'intérieur. Les utilisateurs des objets Stack n'ont rien à faire des LinkedLists ; ce qui les intéresse, c'est de
pouvoir faire des push (empiler) et des pop (dépiler).
Voyons maintenant un cas un peu plus subtil. Supposez que l'implémentation de la classe LinkedList soit basée sur une liste chaînée
d'objets Node (noeuds), et que chaque Node ait un pointeur sur le Node suivant :
class Node
{
};
class LinkedList {
public:
private:
Node* first_;
}; |
La classe LinkedList doit-elle avoir une fonction get() qui donne accès au premier Node ? L'objet Node doit-il avoir une fonction get()
qui permette aux utilisateurs de passer au Node suivant dans la chaîne? La question est en fait : à quoi une LinkedList doit-elle
ressembler vu de l'extérieur ? Une LinkedList est-elle vraiment une chaîne d'objets Node ? Ou cela n'est-il finalement qu'un détail
d'implémentation ? Et si c'est juste un détail d'implémentation, comment la LinkedList va-t-elle donner à ses utilisateurs la possibilité
d'accéder à chacun de ses éléments ?
Une réponse parmi d'autres : une LinkedList n'est pas une chaîne d'objets Nodes. C'est peut-être bien comme ça qu'elle est implémentée,
mais ce n'est pas ce qu'elle est. Ce qu'elle est, c'est une suite d'éléments. L'abstraction LinkedList doit donc être fournie avec une
classe "LinkedListIterator", et c'est cette classe "LinkedListIterator" qui doit disposer d'un operator++ permettant de passer à
l'élément suivant, ainsi que de fonctions get()/set() donnant accès à la valeur stockée dans un Node (la valeur stockée dans un Node est
sous l'unique responsabilité de l'utilisateur de la LinkedList, c'est pourquoi il faut des fonctions get()/set() permettant à cet
utilisateur de la manipuler comme il l'entend).
Toujours du point de vue de l'utilisateur, il pourrait être souhaitable que la classe LinkedList offre un moyen d'accéder à ses éléments
qui mimique la façon dont on accède aux éléments d'un tableau en utilisant l'arithmétique des pointeurs :
void userCode(LinkedList& a)
{
for (LinkedListIterator p = a.begin(); p != a.end(); ++p)
cout << *p << '\n';
} |
Pour implémenter cette interface, la LinkedList va avoir besoin d'une fonction begin() et d'une fonction end(). Ces fonctions devront
renvoyer un objet de type "LinkedListIterator". Et cet objet "LinkedListIterator" aura lui besoin : d'une fonction pour se déplacer vers
l'avant (de façon à pouvoir écrire ++p); d'une fonction pour pouvoir accéder à la valeur de l'élément courant (de façon à pouvoir écrire
*p); et d'un opérateur de comparaison (de façon à pouvoir écrire p != a.end()).
Le code se trouve ci-dessous. L'idée centrale est que la classe LinkedList n'a pas de fonction donnant accès aux Nodes. Les Nodes sont
une technique d'implémentation, technique qui est complètement masquée. Les internes de la classe LinkedList pourraient tout à fait
être remplacés par une liste doublement chaînée, ou même par un tableau, avec pour seule différence une modification au niveau de la
performance des fonctions prepend(elem) et append(elem).
#include <cassert>
class LinkedListIterator;
class LinkedList;
class Node {
friend LinkedListIterator;
friend LinkedList;
Node* next_;
int elem_;
};
class LinkedListIterator {
public:
bool operator== (LinkedListIterator i) const;
bool operator!= (LinkedListIterator i) const;
void operator++ ();
int& operator* ();
private:
LinkedListIterator(Node* p);
Node* p_;
};
class LinkedList {
public:
void append(int elem);
void prepend(int elem);
LinkedListIterator begin();
LinkedListIterator end();
private:
Node* first_;
}; |
Les fonctions membres suivantes sont de bonnes candidates pour être inline (à mettre sans doute dans le même .h):
inline bool LinkedListIterator::operator== (LinkedListIterator i) const
{
return p_ == i.p_;
}
inline bool LinkedListIterator::operator!= (LinkedListIterator i) const
{
return p_ != i.p_;
}
inline void LinkedListIterator::operator++()
{
assert(p_ != NULL);
p_ = p_->next_;
}
inline int& LinkedListIterator::operator*()
{
assert(p_ != NULL);
return p_->elem_;
}
inline LinkedListIterator::LinkedListIterator(Node* p)
: p_(p)
{
}
inline LinkedListIterator LinkedList::begin()
{
return first_;
}
inline LinkedListIterator LinkedList::end()
{
return NULL;
} |
Pour conclure : la liste chaînée gère deux sortes de données différentes. On trouve d'un côté les valeurs des éléments qui sont stockés
dans la liste chaînée. Ces valeurs sont sous la responsabilité de l'utilisateur de la liste et seulement de l'utilisateur. La liste
elle-même ne fera rien par exemple pour empêcher à un utilisateur de donner la valeur 5 au troisième élément, même si ça n'a pas de sens
dans le contexte de cet utilisateur. On trouve de l'autre côté les données d'implémentation de la liste (pointeurs next, etc.), dont les
valeurs sont sous la responsabilité de la liste et seulement de la liste, laquelle ne donne aux utilisateurs aucun accès (que ce soit en
lecture ou en écriture) aux divers pointeurs qui composent son implémentation.
Ainsi, les seules fonctions get()/set() présentes sont là pour permettre la modification des éléments de la liste chaînée, mais ne
permettent absolument pas la modification des données d'implémentation de la liste. Et la liste chaînée ayant complètement masqué son
implémentation, elle peut donner des garanties très fortes concernant cette implémentation (dans le cas d'une liste doublement chaînée
par exemple, la garantie pourrait être qu'il y a pour chaque pointeur avant, un pointeur arrière dans le Node suivant).
Nous avons donc vu un exemple dans lequel les valeurs de certaines des données d'une classe étaient sous la responsabilité des
utilisateurs de la classe (et la classe a besoin d'exposer des fonctions get()/set() pour ces données) mais dans lequel les données
contrôlées uniquement par la classe ne sont pas nécessairement accessibles par des fonctions get()/set().
Note : le but de cet exemple n'était pas de vous montrer comment écrire une classe de liste chaînée. Et d'abord, vous ne devriez pas
"pondre" votre propre classe liste, vous devriez plutôt utiliser l'une des classes de type "conteneur standard" fournie avec votre
compilateur. La meilleure solution est d'utiliser l'une des classes conteneurs du standard C++ , par exemple la classe template list<T>.
|