⚠️ Lisez cette lettre dans votre navigateur
pour profiter des corrections de dernière minute
Améliorer sa pratique
Quelques réflexions issues de mes observations sur les projets
L’architecture hexagonale
Lorsque je suis arrivé chez Theodo, rapidement j’ai formé les tech-leads à l’architecture hexagonale. Bien qu’elle présente plusieurs avantages :
On peut commencer à travailler directement sur les fonctionnalités avant même de décider les technologies à adopter,
Les fonctionnalités sont très faciles à tester,
Le design nous aide à trouver où ranger les règles métier.
L’architecture hexagonale vient avec une complexité obligatoire non négligeable. Pour que l’investissement en vaille le coup, la complexité essentielle doit être au moins d’un ordre de grandeur au dessus1.
Aujourd’hui, bien que toujours convaincu par les bénéfices de l’architecture hexagonale je réalise que les projets qui méritent un tel design ne sont pas courants et j’ai tendance maintenant à promouvoir a priori des architectures plus simples.
Les records
Quand les records sont apparus avec Java 14 j’avais tendance à les utiliser partout, même pour les entités métier. Je savais vaguement que ce n’était pas approprié mais il m’a fallu du temps pour en être convaincu.
Un point essentiel des records c’est que ce ne sont pas des classes ! Les principes de la POO (héritage, encapsulation, polymorphisme) sont inapplicables :
Héritage : il n’est pas possible de spécialiser un record par un autre type
Encapsulation : un records expose irrémédiablement toute sa structure interne
Polymorphisme : il est possible d’étendre des interfaces avec un record mais chercher à donner des comportements à un record entre rapidement en contradiction avec le point précédent.
Ce sont surtout les deux derniers points qui m’ont convaincu que les records ne font pas de bonnes entités métiers. Une entité métier expose des comportements et masque sa représentation interne2. Ainsi le dev peut modifier sa représentation sans risque, et l’entité centralise les règles de gestion.
Avec les records il vaut mieux se tourner vers le Data Oriented Programming :
Mauvaise pratique : mettre du comportement dans les records 👎
// Un comportement implémenté par des records 🥺
public interface Surface {
double area();
}
public record Square(double side) implements Surface {
public double area() {
return side * side;
}
}
public record Circle(double radius) implements Surface {
public double area() {
return Math.PI * radius * radius;
}
}
public record Triangle(double side1, double side2, double angle) implements Surface {
public double area() {
return (side1 * side2 * sin(angle)) / 2;
}
}
...
double areaOf(Surface surface) {
return surface.area();
}
Meilleure pratique : mettre le comportement dans une entité qui exploite des records 👍
// Une interface scellée pour donner du sens à l'ensemble des records
public sealed interface Surface permits Square, Circle, Triangle {}
public record Square(double side) implements Surface {}
public record Circle(double radius) implements Surface {}
public record Triangle(double side1, double side2, double angle) implements Surface {}
...
double areaOf(Surface surface) {
return switch (surface) {
case Square(double side) -> side * side;
case Circle(double radius) -> Math.PI * radius * radius;
case Triangle(double side1, double side2, double angle) -> (side1 * side2 * sin(angle)) / 2;
};
}
Typiquement dans une architecture hexagonale l’entité métier est une classe qui encapsule des value-objects sous forme de records. Elle contient la logique métier. La seule logique contenue dans les records consiste à garantir la validité (les invariants) des données du record.
The more tempting it is to add additional methods to the basic data carrier […], the more likely it is that a full class should be used rather than a record.3
— Ben Evans
Veille
Quelques pépites que j’ai envie de partager avec vous
Avec la hype IA on parle moins d’informatique quantique. Pourtant les ordinateurs quantiques promettent de ringardiser les systèmes cryptographiques qui sécurisent l’ensemble des télécommunications. Heureusement les algo résistant sont déjà dans les tuyaux :
Maximiliano Contieri enrichit régulièrement une base de données de code smells (292 au moment où j’écris ces lignes), et il publie également sur d’autres sujets tech tous plus intéressants les uns que les autres. Il est également l’heureux papa du livre Clean Code Cookbook4 :
Puzzler
Ici un petit challenge pour apprendre en s’amusant !
Réponse à l’énigme précédente
var list = new ArrayList<Integer>();
long count = IntStream.of(1,2,3)
.map(i -> -i)
.peek(list::add)
.count();
println(count);
println(list);
Si vous avez exécuté ce programme, vous avez constaté qu’il prétend que le tableau d’entier a une taille de 3, mais la liste remplie avec les opposés des nombres est vide.
Pour comprendre ce paradoxe, jetons un coup d’œil à la documentation de la méthode peek
:
In cases where the stream implementation is able to optimize away the production of some or all the elements (such as with short-circuiting operations like
findFirst
, or in the example described incount()
), the action will not be invoked for those elements.
Et voilà tout est dit.
Enfin c’est quand même vite dit. Si on creusait un peu le code ?
Un Stream possède certaines caractéristiques. Quand vous construisez un Stream avec la factory IntStream.of
alors il possède les caractéristiques ORDERED, IMMUTABLE, SIZED et SUBSIZED.
Chaque opérations intermédiaires peut modifier ces caractéristiques. Par exemple filter
retire SIZED et sorted
ajoute ORDERED et (bien sûr) SORTED.
Lorsqu’on invoque une opération terminale, en fonction des caractéristiques du Stream certaines optimisations peuvent s’appliquer.
Examinons ce qui se passe pour l’opération terminale count()
abstract class IntPipeline<E_IN>
...
public final long count() {
return evaluate(ReduceOps.makeIntCounting());
}
La méthode evaluate
va simplement faire quelques contrôles et invoquer evaluateParallel
ou evaluateSequential
sur l’opération retournée par makeIntCounting
. Ici le stream est séquentiel. Examinons la méthode evaluateSequential
de l’objet retourné par makeIntCounting
:
public <P_IN> Long evaluateSequential(...) {
long size = helper.exactOutputSizeIfKnown(spliterator);
if (size != -1)
return size;
return super.evaluateSequential(helper, spliterator);
}
evaluateSequential
fait un early return si “exactOutputSize
” est connu. Que fait cette méthode ?
final <P_IN> long exactOutputSizeIfKnown(...) {
int flags = getStreamAndOpFlags();
long size = StreamOpFlag.SIZED.isKnown(flags) ? spliterator.getExactSizeIfKnown() : -1;
// blablabla...
if (size != -1 && StreamOpFlag.SIZE_ADJUSTING.isKnown(flags) && !isParallel()) {
...
}
return size;
}
Souvenez-vous, notre Stream possède la caractéristique SIZED (mais pas SIZE_ADJUSTING, le if est donc sauté). La méthode va retourner directement spliterator.getExactSizeIfKnown()
.
C’est quoi un spliterator ? C’est comme un itérateur avec en plus la possibilité de partitionner l’itération.
Or le spliterator généré par IntStream.of(…) connaît la taille exacte de sa collection, c’est simplement le nombre d’arguments passés à la méthode !
Et voilà, on a vu que quand aucune opération intermédiaire ne vient effacer la caractéristique SIZED du Stream, alors toutes les opérations intermédiaire (map
, peek
…) ne sont même pas évaluées !
PS : si votre IDE vous aime, il essaiera de vous prévenir :
Énigme du jour
L’API du JDK n’a pas fini de nous surprendre !
D’après vous, quelle valeur affiche ce petit programme ?
import java.util.concurrent.LinkedBlockingDeque;
public class Main
{
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().interrupt();
var queue = new LinkedBlockingDeque<String>();
queue.put("Coucou");
String taken = queue.take();
System.out.println(taken);
}
}
Et cet autre petit programme quasiment identique ?
import java.util.concurrent.LinkedBlockingQueue;
public class Main
{
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().interrupt();
var queue = new LinkedBlockingQueue<String>();
queue.put("Coucou");
String taken = queue.take();
System.out.println(taken);
}
}