Interfaces et méthodes par défaut #
Vous allez comprendre l’importance des interfaces pour éviter la duplication de code et vous familiariser avec la notion de méthode par défaut, c’est à dire des méthodes concrète. Nous allons exploiter une technique intéressate : celle de la réalisation de méthodes concrètes à l’aide de méthodes abstraites !
Possibilités #
Pour étudier les possibilités offertes par les interfaces, nous allons améliorer notre étude de cas sur le gestionnaire de ressources. Nous souhaitons élaborer un service de persistance des données pour les salles de cours. Etant donné que nous ne connaissons pas encore quelle sera la meilleure base de données à utiliser, nous allons nous mettre d’accord sur les fonctionnalités qui nous seront indispensables. Nous réaliserons ensuite une première mise-en-oeuvre InMemory
, c’est-à-dire que tant que le système tourne, nous pouvons stocker et récupérer nos ressources, mais si nous redémarrons le programme, nous perdrons le contenu.
Voici notre classe Room
:
classDiagram class Room { +Room(String id, int capacity) +info() String +capacity() int +id() String }
Voici une ébauche avec les fonctionnalités qui nous intéressent:
|
|
Toutes ces méthodes sont dites abstraites car elles ne sont pas réalisées. Elles sont publiques par défaut, donc nul besoin de précéder ces méthodes par le mot-clé public
.
La puissance des interfaces est offerte par des versions relativement récentes de Java. Il est possible:
- d’avoir des constances (Java 7)
- des méthodes statiques (Java 8)
- des méthodes par défaut (concrète) (Java 8)
- des méthodes privées (Java 9)
Dès lors, il est très courant de réaliser des méthodes concrètes (par défaut) dans les interfaces. Ceci permet de réduire la duplication de code. La magie est de réaliser des méthodes concrètes à l’aide de méthodes abstraites. Ainsi, l’utilisateur implémente le minimum de méthodes tout en ayant un maximum de fonctionnalités gratuites. Dans l’exemple de notre RoomDatabase
nous pouvons en rendre plusieurs par défaut :
|
|
Voyez l’avantage : pour réaliser une implémentation concrète, il suffit de définir insert
et rooms
et toutes les autres fonctionnalités sont offertes. Il est facile de faire une version in memory pour débuter rapidement un développement ou une version fake (bidon) pour des tests unitaires par exemple. L’utilisateur peut toujours redéfinir les méthodes par défauts. Une vraie base de données à tout intérêt à le faire puisque certaines requêtes seraient probablement bien plus efficaces si elles étaient réalisées par un moteur de base de données.
Réalisons une première version in memory qui permettrait rapidement de nous concentrer sur d’autres aspects importants du projet :
|
|
Cette version redéfinit les méthodes par défaut exists
et insertIfNotPresent
.
Nous réalisons ensuite une version avec des données bidons :
|
|
Les méthodes de l’interface doivent être redéfinies avec une visibilité publique.
Inversion de dépendances #
Toute notre logique peut maintenant dépendre d’une abstraction. Pour le prouver, réalisons une première méthode qui permet de copier les données d’une base de données à une autre ainsi qu’une deuxième méthode qui vérifie si l’ajout d’une salle se comporte correctement:
|
|
Nous parlons d’inversion de dépendances lorsque tout dépend d’une abstraction. Nos implémentations (FakeRoomDatabase
et InMemoryDatabase
) dépendent de l’interface RoomDatabase
tout comme nos deux méthodes qui représentent notre logique-métier (migrate
et insertWithCheck
).
Fonctionnalités et limitations #
Les interfaces peuvent contenir des méthodes abstraites, concrètes, statiques ou privées et des constantes. Il est possible d’hériter de plusieurs interfaces.
Il est par contre impossible d’y avoir la description d’un état à l’aide de champs ou de constructeurs. Si tel devait être le cas, vous pouvez employez des classes abstraites que nous aborerons à la prochaine section.
Exemple d’une classe héritant de plusieurs interfaces #
La classe IntSequence
représente une séquence d’entiers (ou liste d’entiers). Elle respecte trois interfaces ( IntGrowable
, Clearable
et Removable
)
|
|
Exemple d’une interface héritant de plusieurs interfaces #
Une interface peut hériter d’une ou plusieurs interfaces à l’aide du mot clé extends
:
|
|
Une classe qui hérite d’une interface doit implémenter toutes les méthodes abstraites pour devenir une classe concrète. Si tel n’est pas le cas, la classe reste abstraite et ne peut être instanciée.
Conclusion #
Les interfaces mettent à disposition un puissant mécanisme d’abstraction. Elles permettent de décrire le contrat de certains de nos services ou de nos classes. Le code peut être compilé sans avoir à implémenter les méthodes. Il est possible de simuler une implémentation rapidement pour nous concentrer sur l’essence même de notre réalisation plutôt que sur des détails d’implémentation qui sont parfois inconnus en début de projet. Préférez toujours l’héritage d’interfaces à l’héritage de classes, même abstraite. Rappelez-vous qu’un des objectifs de l’héritage est de réduire la duplication de code et que grâce aux méthodes par défaut, il est possible de l’atteindre.
Quiz 1 #
Soient les déclarations suivantes:
|
|
|
|
|
|
Que va afficher l’extrait de code suivant:
MyContract c = new FunnyImpl();
c.myActionWithLog();
c = new BadImpl();
c.myActionWithLog();
Quiz 2 #
Nous avons à disposition des fonctionnalités sur les objets Runnable
, représentés par les méthodes statiques runThis
et runThat
. Nous avons malheureusement des objets de la classe classe Test
qui nous sont retournés d’une API tierce sur laquelle nous n’avons pas le contrôle. Nous aimerions toutefois passer des objets Test
à toutes méthodes prenants en arguments des Runnable
s au moindre effort. La méthode execute
devrait être exécutée lorsqu’un run
est appelé.
Vous ne pouvez modifier que la méthode main
et vous avez la possibilité de créer d’autres types. Vous ne pouvez pas modifier l’interface Runnable
, ni la classe Test
.
|
|
|
|
|
|