Généralisation, spécialisation
Héritage
simple
Ces notions sont très importante dans le monde objet. leurs implémentations
sous forme d'héritage est à l'origine du concept de réutilisation.
Nous avons mis en oeuvre un premier niveau de classement en regroupant les objets
en classes, nous franchissions là un premier niveau d'abstraction. La généralisation
nous propose d'en franchir un second niveau.
Le concept de généralisation est largement utilisé dans le domaine scientifique
afin de modéliser un classement hiérarchique. Classement des espèces chez les
biologistes, classement des végétaux chez les naturalistes, classement des objets
célestes pour les physiciens astronomes.
Pour nous la spécialisation s'applique lorsqu'une classe affine les propriétés - attributs et comportements - d'une autre classe. Les termes de généralisation et spécialisation s'appliquent à un même type de relation entre classes; on peut employer l'un ou l'autre selon le sens de lecture.
Exemple:
Commentaires:
- Les véhicules à traction humaines et à moteurs sont des sortes de véhicules.
Elles en précisent les propriétés sur certains points, de même pour le vélo
qui spécialise les véhicules à traction humaines ou pour les motos ou voitures.
- Les relations de généralisations sont transitives: une voiture est une sorte
de véhicule à moteur mais aussi une sorte de véhicule.
- Il n'y a pas de valeur de multiplicité explicite, par principe il s'agit d'un
et un seul dans les deux sens.
- Une moto ne peut être une voiture. La contrainte par défaut est l'exclusion
d'objets communs. nous y reviendrons plus loin.
- En remontant la lecture du diagramme, on peut dire que les propriétés communes
des motos et des voitures sont factorisées dans les véhicules à moteur.
Par un raccourci de langage, certain assimilent généralisation et héritage;
ceci devrait être évité car l'héritage est une technique supporté par la plupart
des langages objets. L'héritage relève de l'implémentation, la généralisation
de l'analyse.
Quelques
écueils à éviter
La généralisation est un élément décisif du monde objet, dans celui du logiciel,
l'héritage est incontournable si l'on veut mettre en oeuvre un développement
réutilisable. Mais son utilisation sans règle n'est pas sans risque. Nous pointons
ici quelques erreurs dues à une généralisation non maîtrisée:
Hétérogénéité des sous-classes de même niveau
Le critère de construction des deux classes spécialisées
salarié et libéral est la profession, par contre la classe sportif relève d'une
tout autre classification.
Le comportement spécialisé du salarié ou du libéral ne pourrait
bénéficier au sportif. Il est préférable de faire:
Confusion
de niveau d'abstraction
Ici, la classe spécialisée ne relève pas du même niveau
d'abstraction; le modèle de voiture rend compte d'un concept, la voiture d'une
identité physique.
La spécialisation ne convient pas; celle-ci ne doit pas être
justifiée uniquement par la factorisation de propriétés - c'est le cas ici -
mais elle doit aussi relever d'un même niveau d'abstraction. La classe modèle
de voiture peut être vue comme une "méta-classe" - type de voiture - alors que
la classe voiture doit être perçue comme une classe "physique".
La théorie des ensembles fournit un substrat à la notion
de généralisation. Dans cette hypothèse, une classe spécialisée représente un
sous ensemble - par valeur - de la sur-classe; ce n'est pas le cas dans cet
exemple, il y a beaucoup plus de voitures que de modèles de voiture.
Le modèle conforme serait:
Non-respect
du principe de substitution.
Le principe de substitution a été énoncé en 1987 par B. Liskow: " Toute instance
d'une sous-classe peut être remplacée par une instance d'une sur-classe sans
modification de sémantique ". Autrement dit, si la classe B spécialise une classe
A, un objet b de B doit trouver un intérêt dans toutes les opérations de A.
Imaginons que nous disposions d'une classe liste qui fournissent les opérations
suivantes:
Si nous devons implémenter une pile, il serait tentant de faire:
Mais dans cette hypothèse la pile est dénaturée puisqu'elle va bénéficier d'opérations qui ne concernent pas une telle structure de donnée. Il est préférable de faire:
Dérivation
succéssives
Une classe dérivée peut également être spécialisée par une autre classe :
Accessibilité
des membres dans les classes dérivées.
Le mécanisme d'héritage ne rompt pas le principe d'encapsulation. Il est interdit à toute classe dérivée d'accéder aux données privées de sa super-classe. Ainsi une classe dérivée est soumise aux mêmes règles qu'une classe quelconque.
Il peut être utile parfois de donner aux classes dérivées l'accès à des données d'une super-classe, ceci est possible grâce au modificateur protected ( protégé).
Si l'on déclare:
class ProduitFrais : Produit
{
...
protected int dureeValidite;
}
alors la classe dérivée Cremerie a accès à la donnée dureeValidite, et ce droit est transmis aux classes dérivées de Cremerie; par contre la donnée dureeValidite reste privé pour la super-classe Produit.
Remarque: une méthode peut aussi être déclarée protected.
Ne tomber
pas dans la facilité apparente de déclarer tous les attributs protégés, cela
entraîne des conséquences incontrôlables: dérivation pour contourner l'encapsulation.
Imaginons que nos produits se déclinent en deux versions, nos produits frais et des produits d'épicerie.
Le modèle sera:
La classe Produit est un peu particulière, car aucun Produit ne sera instancié, en effet ce seront soit des produits d'épicerie soit des produits frais; la classe Produit est abstraite. Notez sur le modèle le style "italique" qui indique une classe abstraite.
Utilisation des classes abstraites.
Une classe abstraite est une classe qui ne peut être instanciée. Une classe
abstraite est utilisée dans deux situations particulières:
Les sous-classes
forment une couverture de la classe abstraite
Commentaires
- Les personnels
d'un lycée sont composés soit des enseignants des administratifs ou des personnels
de services.
- Les trois
sous-classes "couvrent" l'ensemble des personnels.
La classe abstraite
ne contient pas suffisamment d'informations pour instancier un objet, autrement
dit cette classe n'est présente que pour factoriser des propriétés. Il s'agit
souvent de classe qui modélise un concept.
Commentaires:
- La classe courbe modélise plutôt
un concept
- Il existe d'autres courbes que
les paraboles et hyperboles.
Une classe abstraite ne peut pas se trouver à la fin d'une hiérarchie de classe.
Une classe abstraite doit avoir au moins une sous-classe.
L'héritage en C#
1) Situation proposée.
Un compte rémunéré possède un numéro, un nom d'utilisateur, un solde, un découvert autorisé. Tous le comptes rémunérés sont soumis au même taux de rémunération; par ailleurs pour calculer à tout moment les intérêts produits, il est nécessaire de connaître la date d'ouverture du compte rémunéré.
On peut débiter ou créditer ces comptes ainsi que transférer d'un compte à un autre; un compte peut comparer son solde avec le solde d'un autre compte. Chaque compte peut afficher ses informations. Enfin chaque compte rémunéré peut retourner les intérêts produits.
Travail à faire.
Dessiner le diagramme de la classe CompteRemunere.
Si l'on compare cette classe avec la classe Compte :
- des attributs et méthodes sont commun :
Nous pouvons mettre en oeuvre l'héritage de classe :
2) L'héritage en C#
Pour déclarer une classe héritée, nous utilisons la syntaxe suivante :
class CompteRemunere : Compte{
public CompteRemunere(){
DateOuverture=DateTime.Now;
}
private DateTime DateOuverture;
private static double taux=0.025;
}
Commentaire : nous avons ajouter un constructeur par défaut -sans paramètre-
Par cette simple déclaration, la classe CompteRemunere va pouvoir bénéficier des attributs de la classe Compte et utiliser ses méthodes publiques :
class AppCompteRemunere{
static void Main(){
CompteRemunere
cr = new CompteRemunere();
cr.GetNumero();
}
}
3) Appels des méthodes.
Si nous regardons le code MSIL généré :
nous voyons que l'appel du constructeur de CompteRemunere entraîne l'appel prélable du constructeur de Compte. Nous allons surcharger le constructeur en le forçant à appeler le bon constructeur de Compte :
class CompteRemunere : Compte{
public CompteRemunere(){
DateOuverture=DateTime.Now;
}
public CompteRemunere(int
numero,string nom,float solde,float decouvertAutorise,int annee,int mois,int
jour) : base(numero,nom,solde,decouvertAutorise){
this.DateOuverture=new DateTime(annee,mois,jour);
}
private DateTime DateOuverture;
private static double taux=0.025;
}
Commentaire : c'est le mot base qui réfère la classe mère.
Utilisation :
class AppCompteRemunere{
static void Main(){
CompteRemunere
cr = new CompteRemunere(235,"toto",1500,-100,2003,11,12);
cr.Afficher();
}
}
Dans ce cas c'est bien sûr la méthode Afficher() de la classe Compte qui est appelée:
Redéfinissons la méthode Affiche de la classe CompteRemunere :
public void Afficher(){
base.Afficher();
Console.WriteLine("date ouverture:{0}",this.DateOuverture);
}
Comentaire:
L'instruction base.Afficher(); appelle la méthode Afficher de la classe Compte.
Cette fois-ci c'est la méthode de CompteRemunere qui est appelée:
4) Niveau d'accessibilité des membres.
Si nous écrivons à l'extérieur de la classe CompteRemunere :
cr.Nom ="titi";
ceci provoque une erreur de compilation, en effet les membres privés restent toujours inaccessibles, même pour le classe dérivée.
Il est possible de faire bénéficier l'accès aux membres d'une classe uniquement à ses classes dérivées, en utilisant le modificateur d'accès protected à la place de private :
protected int Numero;
permettra tous les accès au champ Numero pour les classes dérivée de Compte
Il n'est pas bon d'abuser
du niveau protected, il doit être réservé à des membres
qui sont intimements propres aux classes, par exemple une méthode commune
à elles seules.
5) Polymorphisme.
Ajoutons la classe Banque à notre projet. Dans le Main, créons une banque :
class AppCompteRemunere{
static void Main(){
Banque b = new Banque();
b.Init();
Compte cr = new
CompteRemunere(445,"titi",500,-500,2003,11,10);
cr.Afficher();
b.Ajouter(cr);
}
}
La méthode Afficher sera celle de la classe Compte!!
Pour qu'une méthode se comporte selon le type dynamique créé, il faut modifier le code des deux classes :
A ces conditions l'objet créé se comportera comme un CompteRemunere
Il est donc conseillé d'utiliser le mécanisme de méthode virtuelle dans le cas de redéfinition