Est-ce une pratique sûre d'utiliser les méthodes par défaut en tant que version d'homme pauvre des traits dans Java 8?
Certains prétendent que cela peut rendre pandas triste si vous les utilisez juste pour le plaisir, car c'est cool, mais ce n'est pas mon intention. Il est aussi souvent rappelé que des méthodes par défaut ont été introduites pour prendre en charge l'évolution de l'API et la compatibilité descendante, ce qui est vrai, mais cela ne rend pas incorrect ou tordu de les utiliser comme traits de caractère en soi.
J'ai le cas d'utilisation pratique suivant à l'esprit:
public interface Loggable {
default Logger logger() {
return LoggerFactory.getLogger(this.getClass());
}
}
Ou peut-être, définissez un PeriodTrait
:
public interface PeriodeTrait {
Date getStartDate();
Date getEndDate();
default isValid(Date atDate) {
...
}
}
Certes, la composition pourrait être utilisée (voire des classes auxiliaires) mais elle semble plus verbeuse et encombrée et ne permet pas de bénéficier du polymorphisme.
Donc, est-il correct/sûr d'utiliser des méthodes par défaut comme traits de base , ou devrais-je m'inquiéter des effets secondaires imprévus?
Plusieurs questions sur SO sont liés à Java vs Scala traits; ce n'est pas le but) ici. Je ne demande pas simplement des opinions non plus. Au lieu de cela, je cherche une réponse faisant autorité ou au moins un aperçu sur le terrain: si vous avez utilisé des méthodes par défaut comme traits de caractère dans votre projet d'entreprise, est-ce que cela s'est avéré être une bombe à retardement ?
La réponse courte est: c'est sûr si vous les utilisez en toute sécurité :)
La réponse sarcastique: dites-moi ce que vous entendez par traits, et peut-être que je vous donnerai une meilleure réponse :)
Sérieusement, le terme "trait" n'est pas bien défini. De nombreux développeurs Java sont les plus familiers avec les traits tels qu'ils sont exprimés dans Scala, mais Scala est loin d'être la première langue à avoir des traits, soit en nom, soit en effet.
Par exemple, dans Scala, les traits sont avec état (peuvent avoir des variables var
); dans Fortress, ce sont des comportements purs. Les interfaces de Java avec les méthodes par défaut sont sans état; Est-ce à dire que ce ne sont pas des traits? (Indice: c'était une question piège.)
Encore une fois, à Scala, les traits sont composés par linéarisation; si la classe A
étend les traits X
et Y
, alors l'ordre dans lequel X
et Y
sont mélangés détermine comment les conflits entre X
et Y
sont résolus. En Java, ce mécanisme de linéarisation n'est pas présent (il a été rejeté, en partie, car il était trop "non-Java-like".)
La raison immédiate de l'ajout de méthodes par défaut aux interfaces était de prendre en charge évolution de l'interface, mais nous savions bien que nous allions au-delà. Que vous considériez que c'est "interface évolution ++" ou "traits--" est une question d'interprétation personnelle. Donc, pour répondre à votre question sur la sécurité ... tant que vous vous en tenez à ce que le mécanisme prend réellement en charge, plutôt que d'essayer de l'étirer volontairement à quelque chose qu'il ne prend pas en charge, vous devriez être bien.
Un objectif clé de la conception était que, du point de vue du client d'une interface, les méthodes par défaut ne pouvaient pas être distinguées des méthodes d'interface "normales". La valeur par défaut d'une méthode n'est donc intéressante que pour concepteur et implémenteur de l'interface.
Voici quelques cas d'utilisation qui correspondent bien aux objectifs de conception:
Evolution de l'interface. Ici, nous ajoutons une nouvelle méthode à une interface existante, qui a une implémentation sensible par défaut en termes de méthodes existantes sur cette interface. Un exemple serait d'ajouter la méthode forEach
à Collection
, où l'implémentation par défaut est écrite en termes de la méthode iterator()
.
Méthodes "facultatives". Ici, le concepteur d'une interface dit "Les implémenteurs n'ont pas besoin d'implémenter cette méthode s'ils sont prêts à vivre avec les limitations de fonctionnalités qui en découlent". Par exemple, Iterator.remove
A reçu une valeur par défaut qui renvoie UnsupportedOperationException
; puisque la grande majorité des implémentations de Iterator
ont de toute façon ce comportement, la valeur par défaut rend cette méthode essentiellement facultative. (Si le comportement de AbstractCollection
était exprimé par défaut sur Collection
, nous pourrions faire de même pour les méthodes mutatives.)
Méthodes pratiques. Ce sont des méthodes qui sont strictement pour la commodité, encore une fois généralement mises en œuvre en termes de méthodes non par défaut sur la classe. La méthode logger()
dans votre premier exemple en est une illustration raisonnable.
Combinateurs. Ce sont des méthodes de composition qui instancient de nouvelles instances de l'interface en fonction de l'instance actuelle. Par exemple, les méthodes Predicate.and()
ou Comparator.thenComparing()
sont des exemples de combinateurs.
Si vous fournissez une implémentation par défaut, vous devez également fournir des spécifications pour la valeur par défaut (dans le JDK, nous utilisons pour cela la balise javadoc @implSpec
) Pour aider les implémenteurs à comprendre s'ils souhaitent remplacer la méthode ou non. Certaines valeurs par défaut, comme les méthodes pratiques et les combinateurs, ne sont presque jamais annulées; d'autres, comme les méthodes facultatives, sont souvent remplacées. Vous devez fournir suffisamment de spécifications (pas seulement de la documentation) sur ce que la valeur par défaut promet de faire, afin que l'implémenteur puisse prendre une décision judicieuse quant à savoir s'il doit le remplacer.