août
2011
J’essaye de suivre régulièrement les mailing-lists concernant l’évolution du langage Java. En particulier celle des projets Coin et Lambda, traitant principalement de l’évolution du langage. Il est intéressant de voir à quel point chaque petit détail peut prendre une importance capitale, que ce soit pour des raisons de compatibilités et voir même philosophique…
Bien que ce soit des langages orientées objets, j’ai toujours dit que les C++, Java et C# offrait chacun une approche différente de la POO.
J’avais déjà bien constaté cela il y a quelques années en ce qui concerne les Templates/Generics. Il en est de même aujourd’hui avec la proposition de « Defender Methods » de Java 7 en comparaison des « Extension Methods » de C# 3.0. Car si prime abord cela semble très proche, cela implique en fait des concepts très différents !
Pourtant on part d’une même problématique dans les deux cas : l’ajout de nouvelles méthodes dans une interface existante implique forcément des incompatibilités avec toutes les classes qui l’implémente. Bien sûr dans un petit projet cela n’a pas vraiment d’importance, mais dès qu’on touche à des librairies standard ou très répandus ces changements s’avère impossible !
Pourtant, les interfaces sont utilisées massivement et ne possèdent malheureusement pas toujours une API complète dès leurs créations. La solution consiste alors à utiliser des méthodes static dans une classe utilitaire pour faire le travail, ce qui n’est pas sans défaut et surtout assez éloigné de l’approche « orienté objet »…
Les « Extension Methods » de C# 3.0
La solution choisi par .NET depuis la version 3.0 passe par les « Extension Methods », qui ne sont ni plus ni moins que des méthodes statiques.
La seule particularité vient du fait que le premier paramètre de la méthode est préfixé du mot-clef this, ce qui permet de relier la méthode d’extension au type en question.
Par exemple, pour rajouter une methode Sort()
dans l’interface IList
, on utiliserait une classe défini comme ceci :
namespace ExtensionMethods
{
public static class MyExtensions
{
public static void Sort<T>(this IList<T> list)
{
...
}
}
}
Ensuite, il suffit d’utiliser le namespace correspondant de manière des plus classique :
using ExtensionMethods;
Dès lors, on peut utiliser notre méthode Sort()
avec la même syntaxe que les méthodes d’instances, ce qui nous donne :
IList<string> list = ...;
list.Sort();
Mais on obtient malgré tout un appel de méthode statique, qui reste strictement équivalent au code suivant :
IList<string> list = ...;
MyExtensions.Sort(list);
En clair il s’agit uniquement de sucre syntaxique, avec un support intégré dans l’EDI afin de proposer l’autocompletion.
Les « Defender Methods » de Java 7
De premier abord, la solution choisi par le groupe de travail du projet Lambda semble relativement proche : on se base toujours sur l’utilisation d’une méthode statique qui contiendrait l’implémentation de la méthode.
Par exemple, pour rajouter une méthode sort()
dans l’interface List
, on utiliserait une classe défini comme ceci :
package extensionMethods;
public class MyExtensions {
public static <T> void sort(List<T> list) {
...
}
}
Toutefois, rien ne vient distinguer cette méthode des autres méthodes statiques. En fait le lien avec l’interface à modifier se fait directement dans le code de cette dernière, en définissant une méthode préfixé par le mot-clef extension auquel on associe la méthode contenant l’implémentation par défaut :
public interface List<T> {
// Déclarations des méthodes de l'interface
...
// Déclarations des méthodes d'extensions :
extension void sort() default extensionMethods.MyExtensions.<T>sort;
}
Il ne nous reste plus qu’à utiliser notre méthode, sans avoir à importer quoi que ce soit (tout est déclaré dans l’interface elle-même).
List<String> list = ...;
list.sort();
Toutefois les mécanismes sous-jacents sont différent, car la méthode d’instance sort()
existe réellement, et elle n’est pas remplacée par un appel de méthode statique.
En fait la méthode sort()
est automatiquement injectée dans le bytecode des classes qui implémentent l’interface, de deux manières :
- A la compilation, toutes les méthodes d’extensions qui ne sont pas implémentées sont automatiquement générées. Le code des méthodes manquantes est donc ajouté par le compilateur.
De ce fait il n’y a plus aucun traitement spécifique à faire à l’exécution puisque la classe implémentera bien les méthodes d’extension. - Il est toutefois possible qu’une classe n’implémente pas la méthode d’extension, en particulier si on utilise des classes compilées avec un JDK plus ancien. Dans ce cas là la machine virtuelle injectera le code de la méthode dynamiquement au lieu de générer une exception.
En clair, le principe consiste à injecter le code de la méthode dans les classes qui implémente l’interface. Au mieux directement à la compilation, au pire à l’exécution.
Avantages / Inconvénients
Pour résumer, voici les avantages/inconvénients des « Extension Methods » de C# :
- On n’est pas limité aux interfaces : on peut également rajouter des méthodes dans des classes existantes (même si dans ce cas il est plutôt conseillé d’utiliser l’héritage à la place).
- On peut rajouter des méthodes dans des interfaces/classes dont le code nous est inaccessible.
- Ce n’est que du sucre syntaxique. On reste sur un appel de méthode statique et on perd toute notion d’héritage.
- Le créateur de l’interface/classe perd un peu la main-mise qu’il a dessus, car n’importe qui peut rajouter n’importe quelle méthode, sans parler des cas de conflit potentiel en cas d’abus.
Et ceux des « Defender Methods » de Java 7 :
- Les méthodes sont réellements rajoutées aux interfaces et à leurs classes filles (soit à la compilation, soit à l’exécution). On conserve donc un vrai objet avec de vrai méthode d’instance et tout ce que cela implique (on peut utiliser la reflection, redéfinition de méthode dans les classes filles, etc.).
- Le créateur de l’interface garde la main-mise sur la cohérence de son type.
- On ne peut pas rajouter des méthodes à une interface dont on ne contrôle pas le code source. Bref il s’agit plus d’une fonction réservé aux développeurs de bibliothèque qu’aux « simples » développeurs.
- Cela reste limité aux interfaces. Il est impossible de rajouter une méthode directement dans une classe sans passer par l’héritage.
Bref : deux approches différentes d’un même problèmes, qui offrent chacune des fonctionnalités différentes…
5 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- Possibilité d'accéder au type générique en runtime
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- Recuperation du nom des parametres
- [ fuite ] memoire
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- Classes, méthodes private
- jre 1.5, tomcat 6.0 et multi processeurs
- Définition exacte de @Override
- Difference de performances Unix/Windows d'un programme?
[…] « Extension Methods » de C# 3.0 VS « Defender Methods » de Java 7 par adiGuba (27/08/2010 12:58) J’essaye de suivre régulièrement les mailing-lists concernant l’évolution du langage Java. En particulier celle des projets Coin et Lambda, traitant principalement de l’évolution du langage. Il est intéressant de voir à quel point chaque petit détail peut prendre une importance capitale, que ce soit pour des raisons de compatibilités et voir même philosophique… Bien que ce soit des langages orientées objets, j’ai toujours dit que les C++, Java et C# offrait chacun une approche différente […] […]
D’après le document : aucune des deux.
Le compilateur obligera la classe à implémenter cette méthode (et donc éventuellement à appeler explicitement une ou l’autre version).
a++
Bonjour
j’ai une petite question sur le fonctionnement de ces « Defender Methods »
imaginons que j’ai :
public class MyExtensions {
public static void sort(List list) {
...
}
}
public class MyExtensions {
public static void sort(List list) {
...
}
}
2 implémentations différentes pour une même méthode
et que j’ai également deux interfaces utilisant ces deux extensions
// Déclarations des méthodes de l'interface
...
// Déclarations des méthodes d'extensions :
extension void sort() default extensionMethods.MyExtensions.sort;
}
// Déclarations des méthodes de l'interface
...
// Déclarations des méthodes d'extensions :
extension void sort() default extensionMethods2.MyExtensions.sort;
}
Que ce passe-t-il lorsque je crée un classe qui implémente ces deux interfaces?
Quel est l’implémentation retenue? (la première trouvée dans le classloader?)
Ça me rappelle un peu les traits en Scala ou mêm plutôt l’outil Qi4J. En tous cas ce pourrait être typiquement un outils qui pourrait être utilisé pour résoudre le problème de rôle avec les objets d’un domaine.
Ce que j’écris dessous est pure spéculation sur ce qu’il serait sympa de pouvoir faire, c’est à la fois inspiré de Scala de Qi4j. Évidement le débat est le bienvenu.
Ainsi dans le cas d’un dossier voyage simple on pourrait écrire aujourd’hui :
String identifier();
Amount cost();
...
}
public interface OnewayJourney extends JourneyRecord {
Route outwardRoute();
...
}
public class OnewayJourneyImpl implements OnewayJourney {
// les implémentations ici
...
}
Maintenant si on veut donner des rôles à notre objet dans le système, en fonction du composant, du service, du usecase, etc.
L’idée serait d’écrire quelque chose comme ça:
extension BookStatus book() default org.journey.service.Booker.book;
}
public class OnewayJourneyComposite implements OnewayJourney, BookableJourney {
// implémentation de OnewayJourney
// *mais pas de BookableJourney*
}
Et en ajoutant les rôles en tant qu’interface.
extension ModificationStatus openForModification() default org.journey.service.Modifier.openForModification;
extension ModificationStatus closeModification() default org.journey.service.Modifier.closeModification;
extension ModificationStatus changeOutwardRoute(Route) default org.journey.service.Modifier.changeOutwardRoute;
...
}
public class OnewayJourneyComposite implements OnewayJourney, BookableJourney, ModifiableJourney {
// implémentation de OnewayJourney
// *mais pas de BookableJourney*
// *mais pas de ModifiableJourney*
}
Bien entendu le code ne manipulerait que les interfaces en fonction donc du rôle attendu.
Qu’en pensez vous ?
Rajouter une nouvelle façon de faire la même chose en moins bien… Moi j’aurais mis ça en inconvénient.
ssi ajouté un autre inconvénient pour java:
ureusement, la syntaxe retenue pour le moment évite la tentation de ce détournement d’usage car elle oblige de créer une seconde classe (ou une classe interne dans l’interface). Il y a des propositions sur les listes de diffusion pour autoriser le code directement dans l’interface qui ne me plairait pas vraiment.