Publié par

Il y a 9 ans -

Temps de lecture 7 minutes

De l’héritage à la délégation

Parmi les piliers de l’OOP, 3 sont majoritairement implémentés par nos langages, l’héritage, le polymorphisme et l’encapsulation. Et, bien qu’à l’intérêt reconnu, la délégation ne l’est que trop rarement. Elle remplace pourtant souvent avantageusement l’héritage en réduisant couplage et graphe d’objets. Voyons comment faire de cette petite sœur injustement négligée une alliée de choix.

Décliner n’est pas spécialiser

L’héritage est la réponse spontanée aux questions d’architecture logicielle. Hiérarchisant et spécialisant les objets les uns aux autres, il facilite grandement leur structuration. Malheureusement il nuit à la lisibilité et à la refactorisation du code. Il modifie parfois le comportement de la classe mère de manière minime et destinée à un seul cas d’utilisation. Le comportement en question semble ne pas avoir été spécialisé, plutôt décliné à un usage précis.

La notion de délégation permet d’approfondir cette nuance en offrant aux classes la possibilité d’être déclinées sans avoir recours à l’héritage.

Définition(s)

Déléguer une tâche revient à la confier à quelqu’un d’autre. Ainsi, en OOP, un objet peut confier la résolution d’une de ses opérations à un autre objet. L’objet endossant cette responsabilité est nommé le delegate (ou délégué). Le rôle d’un delegate est de définir du comportement laissé à charge.

  1. Java, dans ses bibliothèques standards, a recours à une version « classique » de la délégation. Comme l’énonce le pattern éponyme : une classe, au lieu d’implémenter une de ses opérations, mandate un helper — le delegate — pour ce faire ;
  2. Une version « inversée » de la délégation, plus méconnue, permet à une classe de créer une déclinaison d’une autre classe adaptée à la situation. Cette dernière déclare qu’une partie de son comportement est indéfini et doit être fourni lors de son utilisation. Lorsqu’une classe l’utilise — elle est delegate — elle accepte de définir ce comportement délégué.
    En d’autres termes : lorsqu’une classe A utilise une classe B, elle est à même de définir une partie du comportement de B. La partie en question est déterminée par B.

Contrairement à la version « classique » ou une classe confie une opération à une autre, dans la version « inversée » une classe laisse la charge d’une partie de son comportement à celles l’utilisant.

Plusieurs patterns de conception du GoF reposent sur la délégation. Le Visiteur permet d’isoler un algorithme de la classe sur laquelle il opère ; ajouter de nouvelles opérations à cette classe se fait sans la modifier. L’État permet d’isoler les statuts d’une classe et les transitions les reliant ; ajouter un statut à cette classe ou modifier leur enchaînement se fait sans la modifier. Le Médiateur permet d’isoler les interactions d’un ensemble de classes ; ajouter une interaction se fait sans modifier ces dernières. Ces patterns utilisent la délégation de manière « classique » ; ayant recours à un helper pour une partie de leurs opérations.

Cet article présente la version « inversée » de la délégation.

L’héritage de l’électricien

Illustrons ce propos à l’aide de la relève annuelle des compteurs d’électricité. Une fois l’an, des électriciens sont chargés d’accéder aux compteurs de France et de Navarre afin d’en relever la consommation. Après avoir pris rendez-vous, ils se présentent à l’abonné pour être autorisés à entrer et effectuer leur relevé.

Dans le milieu rural, l’abonné se présente par exemple au portail de sa maison et ouvre l’accès à l’électricien au garage. En ville, l’abonné se présente par exemple au digicode et ouvre l’accès à l’électricien à la pièce principale.

Une première modélisation de ces responsabilités peut être réalisée à l’aide d’une classe abstraite, Electricien, chargée de la relève du compteur, tâche toujours semblable. Deux classes — ElectricienALaCampagne, ElectricienEnVille — définissent alors les comportements respectifs de l’électricien en fonction de sa localisation.

Les abonnés au réseau électrique sont représentés par deux classes — Rural, Citadin — en fonction de leur localisation. Ils s’en remettent à l’électricien spécialisé pour leur relève de compteur.

public abstract class Electricien {

	abstract void accederAuCompteur(); 

	public void releverLeCompteur() {
		accederAuCompteur();
		noterLesHeuresPleines();
		noterLesHeuresCreuses();	
	}

	private void noterLesHeuresPleines() {
		// ...
	}

	private void noterLesHeuresCreuses() {
		// ...
	}
}
public class Citadin {

	void contacte(ElectricienEnVille electricien) {
		electricien.releverLeCompteur();
	}
}
public class ElectricienEnVille {

	void accederAuCompteur() {
		saisirLeDigicode();
		ouvrirLaPortePrincipale();
	}

	private void saisirLeDigicode() {
		// ...
	}

	private void ouvrirLaPortePrincipale() {
		// ...
	}
}

Les abonnés s’en remettent à des électriciens spécialisés. Bien que l’accès au compteur soit une des problématiques de l’électricien, il ne semble pas être une de ses responsabilités. Il n’a ni à connaître le digicode des abonnés, ni à manipuler leurs clés. Ce cas de figure peut être identifié lorsque :

  • La spécialisation d’une classe est destinée à un seul cas d’utilisation ;
  • La spécialisation d’une classe est relative uniquement à son comportement ;
  • La spécialisation d’une classe n’y ajoute aucun attribut.

Les électriciens semblent ne pas avoir été spécialisés, plutôt déclinés à un usage précis. Le comportement semble ne pas être à sa place.

La délégation des clés

Cocoa, une API Objective-C d’Apple, utilise abondamment la délégation pour la gestion du comportement de ses éléments d’interface. Ceux-ci déclarent un delegate — par convention endossé par le contrôleur — auquel ils confient plusieurs responsabilités (nombre d’éléments d’une liste, possibilité d’en éditer/supprimer, format des éléments, etc) ; évitant ainsi le jour d’une myriade de MyButton.

Reprenons notre exemple d’électricien et appliquons lui cette logique.

Utiliser la délégation conduit à concevoir l’accès au compteur comme une responsabilité de l’abonné. Lui étant dédié, ce code doit figurer dans sa classe.

public class Electricien {

	DéléguéAuxClés delegate;

	public void setDelegate(DéléguéAuxClés delegate) {
		this.delegate = delegate;
	}

	public void releverLeCompteur()  {
		delegate.rendreLeCompteurAccessible();
		noterLesHeuresPlaines();
		noterLesHeuresCreuses();
	}
}
public interface DéléguéAuxClés {
	void rendreLeCompteurAccessible();
}
public class Citadin implements DéléguéAuxClés {

	void contacte(Electricien electricien) {
		electricien.setDelegate(this);
		electricien.releverLeCompteur();
	}

	void rendreLeCompteurAccessible() {
		saisirLeDigicode();
		ouvrirLaPortePrincipale();
	}

	private void saisirLeDigicode() {
		// ...
	}

	private void ouvrirLaPortePrincipale() {
		// ...
	}
}

La compréhension de cette inversion de responsabilités est rendue délicate par l’absence de ce type de délégation en Java ; en Objective-C, les développeurs sont accoutumés à cette logique. Cette considération faite, le résultat obtenu est satisfaisant. L’abonné est à même de réaliser une partie du comportement de l’électricien qui intervient chez lui. Il a même l’opportunité de déléguer à son tour cette responsabilité (à un ami ou au propriétaire, par exemple).

public class Citadin {

	DéléguéAuxClés delegate;

	void contacte(Electricien electricien) {
		electricien.setDelegate(delegate);
		electricien.releverLeCompteur();
	}
}

Au lieu d’implémenter l’interface DéléguéAuxClés, l’abonné confie la responsabilité de l’accès au compteur (que l’électricien lui confie) à quelqu’un d’autre.

Prenons, pour finir, le cas d’un Parisien (hérité de Citadin) habitant un appartement sur cour. Accéder à son immeuble demande la saisie d’un premier digicode puis, une fois dans la cour, l’accès à sa cage d’escalier en demande un second. Utiliser l’héritage conduit à créer un nouvel électricien héritant de ElectricienEnVille encore plus spécialisé. Et, si ce parisien dispose d’un garage (dans le sixième arrondissement) la création d’encore un autre électricien. Malheureusement, seul un graphe d’héritage plus complexe encore pourra modéliser l’idée que les électriciens des campagnes et des villes doivent pouvoir accéder tous deux à un garage.

Chaque type d’abonné présente un cas de figure d’accès au compteur différent. Utiliser l’héritage conduit à créer un nombre équivalent d’électriciens à celui de situations. Utiliser la délégation « inversée » permet de se limiter à un unique électricien. Dans les deux cas, une classe par type d’abonné est nécessaire, dans le second, chacun d’entre eux endosse la responsabilité de rendre le compteur accessible.

Cette délégation « inversée » n’est pas une inversion de responsabilités à proprement parler. Au contraire, elle tend à rapprocher les responsabilités des classes ayant les moyens de les remplir (plutôt que de transmettre les moyens de les remplir à d’autres). Par l’étude des comportements, elle réduit le couplage et le graphe d’objets tout en facilitant la lisibilité et la refactorisation.

Déléguer des responsabilités n’a finalement jamais été aussi sécurisant.

Publié par

Publié par Yves Amsellem

Développeur depuis 5 ans — les 2 derniers chez Xebia — Yves tire de son expérience sur des sites à fort trafic une culture de la qualité, de l'effort commun et de l'innovation. Spécialisé du style d'architecture ReST, il intervient sur des projets web à forte composante JavaScript et NoSQL. Avec Benoît Guérout, il développe la librairie open source Jongo — Query in Java as in Mongo shell

Commentaire

4 réponses pour " De l’héritage à la délégation "

  1. Publié par , Il y a 9 ans

    Je trouve ton article intéressant et met en lumière l’erreur que nous avons à utiliser l’héritage pour juste structurer son code (hérité de Simula). Or, si on se réfère à Smalltalk, l’héritage est d’abord un moyen pour catégoriser nos objets.

    A côté de ceci, je crois que c’est abusif de dire que l’héritage est un pilier de l’OOP. En effet, le terme d’héritage est flou : ce peut être du sous-typage (typage du premier ordre) ou de la sous-classification (typage du second ordre) par exemple. Or ce n’est pas la même chose : dans un cas tu considères le type d’objets, dans l’autre une famille polymorphique de types. De plus, avec les langages objet à prototype (Self, Io, …), le terme d’héritage n’a pas vraiment de sens. En fait, c’est par le polymorphisme que provient l' »héritage » comme étant, potentiellement, une technique permettant de l’avoir ; le polymorphisme étant un pilier de la POO (comme tu le précises).

    En fait, je pense que nous avons tendance à « abuser » de l’héritage pour en fait profiter du polymorphisme. Ceci est probablement dû à la restriction des langages de programmation, que l’on utilise quotidiennement, au typage du premier ordre. Avec un langage qui supporte le typage du second ordre, le polymorphisme y découle naturellement et il n’y a plus vraiment besoin de faire de l’héritage pour profiter du polymorphisme.

  2. Publié par , Il y a 9 ans

    Il manque clairement un support des delegates en java (c’est quelque chose que j’envie à c#, ainsi que les properties). A priori, l’idée est dans les tuyaux de Lombok.

    A noter que noop, un langage made-in-google (mais peu actif), propose de supprimer l’héritage: http://code.google.com/p/noop/wiki/ProposalForComposition

  3. Publié par , Il y a 9 ans

    Question un peu HS, mais avec quel logiciel tu fais tes diagrammes ? J’en cherche désespérément un avec le même rendu…

  4. Publié par , Il y a 9 ans

    @thierryler yUML est très bien pour ça, bien qu’un peu lent parfois (en beta)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.