Types abstraits

Méthodes abstraites et classes abstraites

images/whitebelt.png

Une situation très courante est qu’on peut avoir deux types de clients: les "personnes physiques" et les "personnes morales" (les sociétés par ex.).

Si nous essayons de modéliser ces deux entités:

class PersonnePhysique
    String getId()
    String getPrénom()
    String getNom()
    Address getAdresse()
    double getTauxRéduction()
    Operation[] historique()
    ...
class PersonneMorale
   String getId() 1
   String getRaisonSociale()
   Address getAdresse() 2
   double getTauxRéduction() 3
   Operation[] historique() 4
   ...

1 2 3 4

Méthodes ayant un code commun avec la classe PersonnePhysique.

Manifestement l’une de ces classes ne peut "hériter" de l’autre. Dans la mesure où ces deux classes vont être utilisées dans des circonstances analogues elle peuvent mettre en commun des codes en créant un super-classe Acheteur (processus de mutualisation).

C’est cette super-classe qui sera utilisée par les codes de gestion des ventes … Il y a néanmoins des différences entre les clients "normaux" et les PersonneMorales et d’autres codes en tiendront compte. Ceci dit il n’y a d’ailleurs pas d’instance de type effectif Acheteur (on aura des PersonnePhysiques ou des PersonneMorales, ou des instances d’autres classes à venir).

définition d’une classe abstraite. 

public abstract class Acheteur {
   // champs
   private String id ;
   private double tauxRéduction ;
   private Adresse adresse ;
   // ... autres champs

   protected Acheteur(String id, Adresse adr, double reduc){
      this.id = id ;
      this.adresse = adr ;
      this.tauxRéduction = reduc ;
      // autres initialisations
   }

   protected Acheteur(String id, Adresse adr) {
      this(id, adr, 0.) ;
   }

   // méthodes
   public String getId() { return this.id ; }
   public Adresse getAdresse() { return this.adresse ;}
   public double getTauxRéduction() { return this.tauxRéduction ; }
   // ainsi de suite.... + mutateurs pour adresse, tauxRéduction ,...

   // doit être réalisé par des sous-classes
   public abstract String getDésignation() ; // méthode abstraite

   public String toString() {
      return this.id
         + " " + this.getDésignation()
         + "\n" + this.adresse + "\n"
         + "[" + this.getTauxRéduction() + "]" ;
   }
}

On ne peut pas créer des instances de cette classe (bien qu’elle ait des constructeurs -on notera ici que ce constructeur est pratiquement protected -). On a également défini un principe commun à ces classes: getDésignation() mais la super-classe (rappel on dit aussi la "classe mère") ne peut fournir un code par défaut.

Toutes les sous-classes seront obligées de fournir une réalisation concrète à la méthode getDésignation() (sinon ces sous-classes resteraient elle-mêmes abstraites).

Remarquons que la méthode "concrète" toString() utilise la méthode abstraite (abstract) getDésignation().

Une sous-classe d’une classe abstraite. 

public class PersonnePhysique extends Acheteur {
   private String prénom ;
   private String nom ;
   // autres champs

   public PersonnePhysique(String id, String pren, String nom, Adresse adr) {
      super(id, adr) ;
      this.prénom = pren;
      this.nom = nom ;
   }

   public String getPrénom() { return this.prénom ; }
   // ... ainsi de suite

   public String getDésignation() { // réalisation concrète
      return this.prénom + " " + this.nom ;
   }
}

Et maintenant un code client:

   //....
   PersonnePhysique client = new PersonnePhysique(id, fn, sn, adr) ;
   panier.setAcheteur(client) ; // setAcheteur(Acheteur a)
   //....
   // calcul dans le code de Panier
   // utilise la réduction du client (si il  y en a)
   // le polymorphisme entre en action
   double total = panier.montantTotal() ;

On voit ici le polymorphisme en action: le code de Panier aura une méthode setAcheteur(Acheteur acheteur) qui définit un principe général (il y aura un "acheteur" pour le Panier). A l’invocation on passera forcément un instance d’une des deux classes qui sont une réalisation concrète de l’abstraction 'Acheteur.

--exercice--

[Avertissement]Mutualisations impossibles

images/blackbelt.png Normalement les mutualisations de méthodes dans une super-classe (abstraite ou non) nous permettent d'éviter des duplications de code.

Il existe toutefois des situations où ces mutualisations n’ont pas les effets escomptés (et où on sera, malheureusement, obligé de dupliquer le même code dans les sous-classes!).

Pour bien comprendre le phénomène il faut se souvenir qu’une méthode à accès à ce qui est en portée dans le code où son invocation est définie. Si par exemple une méthode est définie dans une super-classe son code aura accès aux éléments private de cette super-classe, ensuite l’invocation de cette méthode héritée dans une sous-classe aura toujours accès à ces éléments privés de la super-classe (alors que les codes de la sous-classe n’ont pas d’accès à ces éléments privés).

Il existe des méthodes qui sont CallerSensitive c’est à dire qui ont un comportement qui est lié aux propriétés de portée du source dans lequel leur invocation est codée. Si, par exemple, le code d’invocation de méthode est dans une super-classe dans un module M et est ainsi liée au contexte de ce module, son exécution par une sous-classe dans un module différent P ne donnera pas les résultats escomptés!

Nous verrons dans des chapitres ultérieurs des méthodes comme Class getResourceAsStream ou MethodHandles.lookup et prenons cette dernière comme exemple. Cette méthode donne un accès local à la programmation dynamique (voir chapitre correspondant): "local" signifie qu’elle donnera un point d’accès à l’endroit de son invocation.

Du coup si on a un objet comme:

public abstract class ObjetDynamique {
     ....
    // accès dynamique
    abstract MethodHandles.Lookup accès();
}

On sera obligé de dupliquer exactement le même code dans les sous-classes! (on ne peut pas mutualiser le code de accès dans la classe abstraite!).

Interfaces

images/whitebelt.png

Nous sommes maintenant habitué au fait que l’A.P.I soit la partie la plus significative d’un type du point de vue des codes client.

Pour pousser un peu plus loin dans cette direction que penser d’un type qui serait défini uniquement par les services qu’il rend? Un type qui définirait des capacités (une référence de ce type sait faire ceci ou cela) mais qui ne dit rien sur la manière dont ces services sont réalisés.

Voici un exemple d’utilisation d’un tel type:

public abstract class Acheteur {
   //champs
   // ce qui sert à envoyer des messages à l'acheteur
   private Messager messageAgent; // type abstrait "pur"
   public void setMessager(Messager ms) { this.messageAgent = ms ; }
   public Messager getMessager() { return this.messageAgent ; }
}

Qu’est ce qu’un Messager ? Quelque chose qui a la capacité d’envoyer un message quand cette méthode est invoquée:

   public void envoiMessage(String message) ;

Définition d’une "interface" java. 

public interface Messager {
   public void envoiMessage(String message) ;
   // simplifié: un message pourrait être plus compliqué qu'une simple chaîne
}

On a ici la définition d’un nouveau type. La déclaration ressemble à celle d’une classe mais le mot-clef est interface (il y a d’autres différences dans le code).

Ici le service est décrit au moyen d’une seule méthode mais il pourrait y en avoir plusieurs. Ces méthodes sont toutes implicitement public et abstract (bien qu’une convention de codage fait qu’on ne les déclare jamais abstract).

Regardons maintenant des types "concrets" qui s’engagent sur le contrat de l’interface Messager:

Agents capables de rendre le service défini par une interface. 

public class Fax extends Telephone implements Messager {
     ...
   public void envoiMessage(String message) {
      // code
   }
}

public class Courrier extends Imprime  // on supposera que la classe Imprime existe
   implements Messager {
     ...
   public void envoiMessage(String message) {
      // code
   }
}
public class Courriel
   implements Messager {
     ...
   public void envoiMessage(String message) {
      // code
   }
}

On remarquera:

  • La déclaration du type comme conforme à un contrat défini par une interface: mot-clef implements.
  • Le fait que la classe concrète fournit un code qui rend effectivement le service prévu au contrat. Si la classe ne le fait pas elle est alors obligatoirement abstraite.

Les interfaces sont de vrais types! 

   Acheteur monClient =  new PersonnePhysique(id, "Tryphon", "Tournesol", adr) ;
   Acheteur monRevendeur = new PersonneMorale(id, "boucherie Sanzeau", adr2, reduction) ;
   //..... code
   monClient.setMessager( new Fax("33+9896917")) ;
   monRevendeur.setMessager( new Courriel("sanzeau@biz.be")) ;

La classe Acheteur a défini un méthode setMessager(Messager agent) ; on peut lui passer tout objet qui s’engage sur le contrat Messager. Le code de Acheteur sait qu’il pourra invoquer sur tout objet passé la méthode envoiMessage.

Par ailleurs il est parfaitement possible de définir des références de type Messager … et même des tableaux Messager[].

Polymorphisme dynamique avec des interfaces. 

   Acheteur[] mesClients ;
   //.....
   for( final Acheteur client: mesClients) {
      client.getMessager().envoiMessage("Meilleurs voeux!") ;
   }
   // enverra un Fax à Mr Tournesol
   // et un courriel à la maison "Sanzeau"

On remarquera que si, plus tard, on crée de nouveaux types conformes au contrat de type Messager (SMS par exemple) on n’a pas à modifier le code ci-dessus! On a un découplage entre la demande de service et sa réalisation (le code qui envoie les voeux saura déclencher un SMS sans en avoir "conscience").

Déclaration d’une interface

Les interfaces Java sont déclarées dans des fichiers source .java et font partie d’un package. Une fois compilé les fichier binaire correspondant et un fichier .class.

Le code est principalement une liste de méthodes abstraites définissant un ensemble cohérent de services. Ce code peut aussi contenir des membres static final ainsi que d’autres membres que nous verrons plus loin.

Une définition plus détaillée d’une interface. 

package com.maboite.ventes ;

public interface Messager {
   // un type énuméré serait ici bienvenu
   public static final int NON_URGENT = -1 ;
   public static final int NORMAL = 0 ;
   public static final int URGENT = 1 ;

   public void envoiMessage(String message, int urgence) throws  ExceptionRoutage,
                  ExceptionUrgence ;
}

et une utilisation:

  monClient.getMessager().envoiMessage(monMessage, Messager.URGENT) ;

Documentation d’une interface

Dans la mesure où une interface java décrit ce qu’il y a à faire (et non comment le faire) une simple liste de signatures de méthodes n’est pas vraiment suffisante. Une documentation précise du "contrat" - ce que chaque méthode doit précisément faire - est essentielle à la définition.

lire ici la documentation de cette interface Comparable -vous ferez abstraction de la notation Comparable<T> qui sera expliquée plus tard-

Réalisations conformes à une interface

Quand une classe déclare qu’elle est conforme à un contrat d’interface (via implements) elle doit fournir des codes pour les méthodes décrites dans l’interface (des codes conformes aux descriptions de la documentation -et aux signatures des services-). Si ce n’est pas le cas la classe demeure abstraite (déclarée abstract) et ce sont des sous-classes qui compléteront les services manquants.

La plupart de classes déclarent une conformité à plusieurs interfaces: elles sont déclarées de la manière suivante:

class MaClass extends QuelqueChose implements UneInterface, UneAutreInterface {

Un exemple sur la classe standard String : on notera que certaines interfaces sont juste des "marqueurs" sans spécification de méthode (ceci permet d’indiquer une propriété des objets qui pourra être testée avec l’opérateur instanceof: exemple : RandomAccess dans cette classe).

Quand une classe est conforme à une interface toutes ses sous-classes sont conformes (Ce peut être un problème avec les interfaces "marqueurs" aussi ces dernières ont-elles tendance à être remplacées par des Annotations).

Outre les interfaces "marqueurs" une autre catégorie d’interface est importante: les interfaces fonctionnelles décrivent une interface dotée d’une seule méthode (voir plus loin pour le développement de cette notion).

--exercice--

Conception avec des interfaces, découplage

images/orangebelt.png

Les interfaces sont une caractéristique extrèmement importante de Java. Elles doivent être largement utilisées , en particulier pour:

  • déclarer des services qui peuvent être réalisés de manières différentes. C'était le cas pour Messager. C’est aussi souvent le cas pour des packages qui définissent une A.P.I unifiée d’accès: une fois cette API standardisée les codes client sont portables sur différentes plate-formes avec des librairies de déploiement différentes. Ces librairies fournissent des classes spécifiques conformes au service standard.

    Un exemple avec le package java.sql (A.P.I J.D.B.C) :

       Connection conx = DriverManager.getConnection(databaseUrl) ;
       // Connection est une interface standard définie dans java.sql
  • "filtrer" les accès à un objet qui n’est vu qu’au travers de la liste des services d’une interface (par exemple dans une IHM le code graphique ne connait que certaines caractéristiques d’un objet qu’il manipule, mais il n’a pas accès à l’ensemble de l’API de l’objet réél).
  • découpler : c’est une propriété importante à la fois du point de vue technique et architectural.

Découplage architectural

images/bluebelt.png

image: architecture et decouplage

Ici on voit que différentes parties d’une application peuvent être "découplées" en définissant leurs points de contact au moyen d’une ou plusieurs interfaces. Différentes équipes peuvent se répartir le travail sans avoir à s’attendre les unes les autres. On a une grande souplesse pour positionner des tests et pour faire évoluer l’architecture de l’application.

Découplage de code

images/brownbelt.png

Voici deux codes qui sont "hyper-couplés" :

public class Modele { // ATTENTION CODE MALHEUREUX!
   //champs
   public void addVue(Vue vue) {
      // ajoute une "vue" à la liste
      // quand l'état du "modèle" change : les "vues" sont prévenues
   }

   protected void changementEtat() {
      for(Vue vue : vues) { vue.prévenir() ; }
   }

   public Object getNouvelEtat() { // ... renvoie qqch }

}
public class Vue { // ATTENTION CODE MALHEUREUX!
   private Modele modèle ;
   //...
   public Vue(Modele modèle) {
      this.modèle = modèle ;
      modèle.addVue(this) ;
      //....
   }

   public void prévenir() {
      Object changé = modèle.getNouvelEtat() ;
      //...
   }
}

On en peut pas compiler un de ces codes sans compiler l’autre! Ces codes sont totalement inter-dépendants: même si on peut les sous-classer (et si chacun pouvait être une classe abstraite) il y a ici un couplage fort.

Un exemple de la réalisation de ce mécanisme modèle/vue en Java standard: bien que la classe Observable et l’interface Observer soient légèrement couplées (au travers de la définition de update(Observable obs, Object obj)) les classes qui implantent le contrat d'Observer ne sont pas connues d'Observable. Ce "pattern" est un call-back (rappel en retour) et il est souvent utilisé en Java sous différentes formes (et, en général, sans aucun couplage!).

--exercice--

Extension des interfaces

images/bluebelt.png

Une fois une interface "publiée" (c’est à dire utilisée par d’autres codes que ceux de notre équipe) on ne doit pas la modifier. Si, par exemple, on ajoute une nouvelle méthode au "contrat" d’interface toutes les classes qui l’implantent doivent être modifiées (y compris donc des classes qui ne sont pas sous la resppnsabilité de l'équipe courante: c’est une violation de contrat!).

Il y a toutefois plusieurs façon d'étendre une interface publiée:

  • En créant une sous-interface
  • En la dotant de méthodes statiques ou de méthodes par défaut (nouveautés apparues avec Java 8)

Héritage d’interface

Il est possible de définir une nouvelle interface qui rajoute des éléments à un contrat d’interface.

public interface MessagerEnLigne extends Messager {
   public Acquittement vérificationAdresse(String adresse) throws  ExceptionRoutage ;
}

Ici le fax pourra implanter ce contrat mais pas le courrier "papier".

Une sous-interface peut aussi préciser des éléments du contrat de l’interface "mère" :

public interface Messager {
   ...
   // ici il n'est pas précisé ce que peut renvoyer l'opération
        public Object envoiMessage(String message, int urgence) throws  ExceptionRoutage,
                                                ExceptionUrgence ;
}
public interface MessagerEnLigne extends Messager {
        public Acquittement envoiMessage(String message, int urgence) throws  ExceptionRoutage,
                                                ExceptionUrgence ;
   public Acquittement vérificationAdresse(String adresse) throws  ExceptionRoutage ;
}

On a ici une précision sur ce que renvoie l’opération envoiMessage , de plus le polymorphisme fonctionne (principe de covariance des méthodes).

Une sous-interface peut également préciser la nature du "contrat" sans modifier les déclarations de méthodes: voir par exemple les définitions des interfaces AutoCloseable et sa sous-interface Closeable (elles déclarent la même méthode)

En réalité l’héritage d’interface n’est pas simplement là pour réaliser des extensions a posteriori d’interfaces existantes: c’est un puissant objet de conception.

Exemples:

public interface ConteneurEnLecture {
   public Object getContenu() ;
   ....
}
public interface ConteneurEnEcriture {
   public void setContenu(Object contenu) ;
   ....
}
public interface Conteneur  extends ConteneurEnLecture, ConteneurEnEcriture{
   ...
}

Sans abuser de ce principe on voit ici qu’on évite d’avoir des Conteneurs trop généraux pour lesquels la méthode setContenu rendrait une UnsupportedOperationException !

[Note]

L’héritage multiple d’interfaces pose potentiellement un problème si deux super-interfaces contiennent la même signature de méthode. Les spécifications de Java décrivent alors le comportement attendu (mais c’est une situation qu’il vaut mieux éviter car elle implique des implicites qui ne facilitent pas la lecture d’un code).

[Note]

On trouve également sur le WEB des textes qui indiquent que les interfaces permettent d’induire l’héritage multiple entre classes. C’est, bien entendu, une interprétation erronée des mécanismes de réalisation des codes de classes. Nous reverrons ce point dans le chapitre sur les patterns.

Extensions par méthodes concrètes

images/brownbelt.png

A partir de Java 8 il est devenu possible d’ajouter du code exécutable à la définition d’une interface. Ici aussi ce n’est pas un simple souci d’extension qui doit guider: on peut essayer de mettre en commun des codes associés au "contrat" d’interface (qui évitent ainsi de dupliquer des codes dans les classes qui implantent cette interface).

Soit la classe :

public class Chaine {
    Chaine précédent ;
    Chaine suivant ;
    Object contenu ;
     .... // autres champs et méthodes

(permet d’organiser des Objets dans une structure de donnée chaînée (analogue à LinkedList que nous verrons ultérieurement)

Soit maintenant un catégorie particulière d’objets qui veut être conscient de l’organisation (et éventuellement profiter d’autres services offerts par la superstructure).

// suivre l'utilisation de Objects.requireNonNull(object)
// java.util.Objects
// les objets Optional seront expliqués ultérieurement

public interface ObjetEnChaine {
    //virtual member pattern
    Chaine getChainon() ;
    void setChainon(Chaine chainon) ;

    // en fait doit rendre un Objet Optional!
    public default Object getPrécédent() {
        Chaine chaine = Objects.requireNonNull(getChainon());
        Chaine précédent = chaine.getPrécédent() ;
        return précédent != null? précédent.getContenu() : null ;
    }

    // en fait doit rendre un Objet Optional!
    public default Object getSuivant() {
        Chaine chaine = Objects.requireNonNull(getChainon());
        Chaine suivant = chaine.getSuivant() ;
        return suivant != null? suivant.getContenu() : null ;
    }

    public default Chaine enchainer(Chaine chaine) {
        Chaine res = new Chaine(this) ; // this!!!
        this.setChainon(res);
        if(chaine != null) {
            res.setPrécédent(chaine);
            res.setSuivant(chaine.getSuivant()) ;
            chaine.setSuivant(res);
        }
        return res;
    }

   // autres méthodes profitant des services délégués au chainon d'accrochage

    // une méthode statique (factory)
    public static Chaine fabrique( ObjetEnChaine... objets) {
        Chaine res = null, chainonCourant = null ;
        if(objets.length >0 ) {
           chainonCourant = objets[0].enchainer(chainonCourant) ;
            res = chainonCourant ;
            for(int ix = 1; ix < objets.length; ix++) {
                chainonCourant = objets[ix].enchainer(chainonCourant) ;
            }
        }
        return res ;
    }
}

Les codes ne peuvent pas être semblables à des codes contenus dans une classe: une interface n’a pas d'état! Ils exploitent toutefois des connaissances issues du contrat d’interface.

On a ici:

  • Des méthodes "par défaut" qui exploitent les connaissances qu’a ce code sur les propriétés des objets qui implantent l’interface. Ces objets ont la possibilité de spécialiser ces méthodes : on a donc avec une méthode default un élément du "contrat" d’interface qui connait une réalisation par défaut.

    Bien qu’une interface n’aie pas d'état on a quand même une utilisation possible de this (dans le code de enchainer )!

  • Un méthode statique décrivant un service qui utilise les caractéristiques de tous les objets ObjetEnChaine
  • on peut également avoir des méthodes privées qui permettraient de mutualiser des codes entre méthodes par défaut.
[Note]

On ne peut définir comme méthode par défaut aucune des méthodes de la classe Object : comme toute classe hérite de Object de telles méthodes par défaut seraient automatiquement spécialisées (et donc leur code ne servirait à rien!)