Généralisation, spécialisation

                  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 successives

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 tombez 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.

 

 

                        Classe abstraite

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 possède un numéro, un nom d'utilisateur, un solde, un découvert autorisé.

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.

 

 

Si l'on compare cette classe avec la classe Compte :

- des attributs et méthodes sont communs :

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 ajouté 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 :

  • dans la classe Compte, utiliser le mot virtual :

    public virtual void Afficher(){
             Console.WriteLine("numéro :{0} nom :{1} solde :{2} découvert autorisé :{3}",this.Numero,this.Nom, this.Solde,DecouvertAutorise);
    }

     

  • dans la classe dérivée, indiquer que l'on redéfinit la méthode :

    public override void Afficher(){
             base.Afficher();
             Console.WriteLine("date ouverture:{0}",this.DateOuverture);
    }

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