Généralisation, spécialisation

 

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: 

Exercices

                Java et l'héritage

    Soit la classe suivante:

 

 

En Java:

class Produit
{
        public Produit(String lbl ,int px ,int qt)
        {
            libelle = lbl;
            prix = px;
            qteStock = qt;
        }
        public int valeurStock()
        {
            return qteStock * prix;
        }
        public void affiche()
        {
            System.out.print(libelle +"\t"+prix+"\t"+qteStock);
        }
private String libelle;
private int prix;
private int qteStock;
}

    Une classe AppProduit permet d'instancier des objets:

public class AppProduit
{
        public static void main(String arg[])
        {
            Produit p = new Produit("poire",12,100);
            p.affiche();
            System.out.println(p.valeurStock());
        }
}

    Ce qui produira:

 

    Si nous modélisons des produits frais qui ont une durée de validité, nous obtenons:

 

        La classe ProduitFrais spécialise la classe Produit 

    En Java nous utilisons le mot réservé extends pour traduire cette relation:

class ProduitFrais extends Produit
{
...
private int dureeValidite;
}

            Constructeur de classe dérivée.

Avant d'utiliser cette classe il nous faut régler le problème du constructeur, en effet lorsque nous écrirons:

        ProduitFrais pf = new ProduitFrais, quel constructeur sera appelé?

    Un constructeur spécifique doit être écrit:

            public ProduitFrais(String lbl,int px,int qt, int d)
                {
                super(lbl,px,qt);
                dureeValidite = d;
                }

    Notez ainsi que le constructeur de ProduitFrais appelle le constructeur de Produit, grâce à l'instruction super et initialise son champ propre. 

L'appel à super doit être la première instruction du constructeur de la classe dérivée.

    La classe ProduitFrais est opérationnelle.

public class AppProduit
{
    public static void main(String arg[])
        {
            ProduitFrais pf = new ProduitFrais("poire",12,100,10);
            pf.affiche();
            System.out.println(pf.valeurStock());
        }
}

    Ce qui produira:

        Le résultat obtenu n'est pas encore satisfaisant car la méthode affiche est la méthode d'un Produit et pas d'un ProduitFrais

    Aussi il nous faut surcharger la méthode affiche():

public void affiche()
    {
        super.affiche();
        System.out.println("\t" + dureeValidite);
}

    Cette fois l'affichage tient compte de la donnée dureeValidite:

 

    Ceci est une nouvelle application du polymorphisme: si une méthode est appelée, c'est la méthode de la classe qui est utilisée, sinon c'est la méthode de sa classe parente.   

 

Le modèle de classe devient:

 

                Mot réservé this.

    Lorsque l'on veut référencer à l'intérieur d'une méthode l'objet qui l'appelle on utilise le mot this; ainsi la déclaration du constructeur de Produit pourrait avoir la syntaxe suivante:

 public Produit(String lbl ,int px ,int qt)
        {
            this.libelle = lbl;
           this.prix = px;
            this.qteStock = qt;
        }

    Ceci sera utile surtout si nous voulons retourner la référence de l'objet qui appelle une méthode.

               Dérivations successives.

   Nous pouvons rajouter une classe Cremerie, en tant que produit frais spécialisé:

 

        En Java, la classe Cremerie va hériter de ProduitFrais:

class Cremerie extends ProduitFrais
{
        public Cremerie(String lbl,int px,int qt, int d, float qtegras)
        {
            super(lbl, px, qt, d);
            qteMatiereGrasse = qtegras;
        }
        public void affiche()
        {
            super.affiche();
            System.out.println("\t"+qteMatiereGrasse);
        }
private float qteMatiereGrasse;
}

       Si nous voulons clore toute spécialisation future, on utilise le mot réservé final:

   final class Cremerie extends ProduitFrais

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

                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.

        En Java on écrira:

abstract class Produit
{
        public Produit(String lbl,int px,int qt)
        {
            libelle = lbl;
            prix = px;
            qteStock = qt;
        }
        public int valeurStock()
        {
            return qteStock * prix;
        }
        public void affiche()
        {
            System.out.print(libelle +"\t"+prix+"\t"+qteStock);
        }
private String libelle;
private int prix;
private int qteStock;
}

class Epicerie extends Produit
{
        public Epicerie (String lbl,int px,int qt, int pds)
        {
            super(lbl,px,qt);
            poids = pds;
        }
        public affiche()
        {
            super.affiche();
            System.out.println("\t"+poids);
        }
private int poids;

Remarque: une classe abstraite peut posséder des méthodes concrètes ou abstraites; si une classe possède une méthode abstraite, toutes les classes dérivées devront implémenter cette méthode. par exemple, si la méthode affiche était déclarée:

abstract class Produit
{

...

    public abstract void affiche();

...

}

    Notez que la méthode abstraite n'a pas de "corps" ; on dit qu'elle constitue une promesse.

                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.

Travail à faire.

TP 5.