Cet article présente un exemple concret de refactorisation Java visant à obtenir un code plus propre et une meilleure séparation des problèmes. L’idée est née de mon expérience avec le codage dans un cadre professionnel.
Il était une fois dans un code de production
Lorsque je travaillais sur du code conservant certaines données de domaine, je me suis retrouvé avec ce qui suit :
public void processMessage(InsuranceProduct product) throws Exception {
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
try {
upsert(product);
return;
} catch (SQLException ex) {
if (retry >= MAX_RETRIES) {
throw ex;
}
LOG.warn("Fail to execute database update. Retrying...", ex);
reestablishConnection();
}
}
}
private void upsert(InsuranceProduct product) throws SQLException {
//content not relevant
}
Le processMessage
fait partie d’un contrat-cadre et est appelé à conserver chaque message traité. Le code effectue une mise à jour idempotente de la base de données et gère la logique de nouvelle tentative en cas d’erreurs. La principale erreur qui m’inquiétait était une connexion JDBC expirée qui devait être rétablie.
Je n’étais pas satisfait de la version initiale de processMessage
du point de vue du code propre. Je m’attendais à quelque chose qui révélerait instantanément son intention sans avoir besoin de plonger dans le code. La méthode est pleine de détails de bas niveau qui doivent être compris pour savoir ce qu’elle fait. De plus, je voulais séparer la logique de nouvelle tentative de l’opération de base de données en cours de tentative pour la rendre facile à réutiliser.
J’ai décidé de le réécrire pour résoudre les problèmes mentionnés.
Moins procédural, plus déclaratif
La première étape consiste à déplacer le updateDatabase()
appel à une variable alimentée par lambda. Laissez l’IDE vous aider en utilisant Introduire la variable fonctionnelle refactorisation. Malheureusement, nous recevons un message d’erreur :
Aucune interface fonctionnelle applicable trouvée
La raison en est l’absence d’interface fonctionnelle fournissant une interface SAM compatible avec la méthode upsert. Pour résoudre ce problème, nous devons définir une interface fonctionnelle personnalisée qui déclare une seule méthode abstraite n’acceptant aucun paramètre, ne renvoyant rien et lançant SQLException
. Voici l’interface que nous devons fournir :
@FunctionalInterface
interface SqlRunnable {
void run() throws SQLException;
}
Avec l’interface fonctionnelle personnalisée en place, répétons la refactorisation. Cette fois, c’est réussi. Aussi, déplaçons l’affectation de la variable avant le pour boucle:
public void processMessage(InsuranceProduct product) throws Exception {
final SqlRunnable handle = () -> upsert(product);
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
try {
handle.run();
return;
} catch (SQLException ex) {
if (retry >= MAX_RETRIES) {
throw ex;
}
LOG.warn("Fail to execute database update. Retrying...", ex);
reestablishConnection();
}
}
}
Utilisez le Méthode d’extraction refactoring pour déplacer le pour boucle et son contenu à une nouvelle méthode nommée retryOnSqlException
:
public void processMessage(InsuranceProduct product) throws Exception {
final SqlRunnable handle = () -> upsert(product);
retryOnSqlException(handle);
}
private void retryOnSqlException(SqlRunnable handle) throws SQLException {
//skipped for clarity
}
La dernière étape consiste à utiliser le Variable en ligne refactoring pour aligner le handle
variable.
Le résultat final est ci-dessous.
public void processMessage(InsuranceProduct product) throws Exception {
retryOnSqlException(() -> upsert(product));
}
La méthode d’entrée de cadre indique maintenant clairement ce qu’elle fait. Il ne fait qu’une seule ligne, il n’y a donc pas de charge cognitive.
Le code de support contient des détails sur la façon dont il remplit son devoir et permet la réutilisation :
private void retryOnSqlException(SqlRunnable handle) throws SQLException {
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
try {
handle.run();
return;
} catch (SQLException ex) {
if (retry >= MAX_RETRIES) {
throw ex;
}
LOG.warn("Fail to execute database update. Retrying...", ex);
reestablishConnection();
}
}
}
@FunctionalInterface
interface SqlRunnable {
void run() throws SQLException;
}
Conclusion
Cela en valait-il la peine? Absolument. Résumons les avantages.
Le processMessage
La méthode exprime maintenant clairement son intention en utilisant une approche déclarative avec un code de haut niveau. La logique de nouvelle tentative est séparée de l’opération de base de données et placée dans sa propre méthode, qui, grâce à une bonne dénomination, révèle précisément son intention. De plus, la syntaxe Lambda permet une réutilisation facile de la fonction de nouvelle tentative avec d’autres opérations de base de données.