Code Propre
Principes et bonnes pratiques pour écrire du code maintenable, testable et évolutif en architecture hexagonale.
Un code propre est un code qui se lit comme de la prose et dont l’intention est immédiatement claire.
Principes SOLID
Les principes SOLID sont essentiels pour une architecture hexagonale réussie.
SOLID n’est pas une contrainte, c’est une boussole pour concevoir du code flexible et maintenable.
S - Single Responsibility Principle
Une classe = Une responsabilité = Une raison de changer
❌ Violation
Violation SRP
// ❌ MAUVAIS : Trop de responsabilités
@Controller
public class TrendController {
@Get
public TrendResponseDto getTrends(String keyword, String region) {
// 1. Validation
if (keyword == null || keyword.length() < 2) {
throw new IllegalArgumentException("Invalid keyword");
}
// 2. Appel API externe
String json = httpClient.get("https://api.example.com/trends?q=" + keyword);
// 3. Parsing JSON
JsonNode node = objectMapper.readTree(json);
// 4. Conversion en DTO
TrendDto dto = new TrendDto(
node.get("keyword").asText(),
node.get("value").asInt()
);
// 5. Logique métier
if (dto.getValue() > 80) {
// Envoyer notification
emailService.send(...);
}
return new TrendResponseDto(dto);
}
}Problèmes :
- ❌ Validation, API, parsing, métier, notification = 5 responsabilités
- ❌ Impossible à tester unitairement
- ❌ Modification d’une partie = risque de tout casser
O - Open/Closed Principle
Ouvert à l’extension, fermé à la modification
❌ Violation
Violation OCP
// ❌ MAUVAIS : Modification de la classe pour chaque nouveau type
@Singleton
public class TrendRepository {
public TrendResult getTrends(String keyword, String source) {
if (source.equals("mock")) {
return getMockData(keyword);
} else if (source.equals("api")) {
return getFromApi(keyword);
} else if (source.equals("database")) {
return getFromDatabase(keyword);
} else if (source.equals("cache")) { // ← Modification !
return getFromCache(keyword);
}
throw new IllegalArgumentException("Unknown source");
}
}Problèmes :
- ❌ Modification du code pour chaque nouveau type
- ❌ Risque de régression
- ❌ Violation du principe
L - Liskov Substitution Principle
Une classe fille doit pouvoir remplacer sa classe mère sans altérer le comportement
❌ Violation
Violation LSP
// ❌ MAUVAIS : L'implémentation change le contrat
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
}
@Singleton
public class DatabaseTrendRepository implements TrendRepository {
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
// ❌ Lance une exception au lieu de retourner Optional.empty()
if (!databaseAvailable) {
throw new RuntimeException("Database not available");
}
return Optional.of(...);
}
}Problèmes :
- ❌ Le contrat dit
Optional, l’implémentation lance une exception - ❌ Code client doit gérer des exceptions non prévues
- ❌ Impossible de substituer sans casser le code
I - Interface Segregation Principle
Plusieurs interfaces spécifiques valent mieux qu’une interface générale
❌ Violation
Violation ISP
// ❌ MAUVAIS : Interface trop large
public interface TrendService {
Optional<TrendResult> getTrends(TrendQuery query);
void saveTrend(TrendResult result);
void deleteTrend(Long id);
List<TrendResult> searchTrends(String keyword);
void exportToFile(String filename);
void sendEmail(String to, TrendResult result);
}
// Client qui a besoin SEULEMENT de lire les trends
@Singleton
public class GetTrendsUseCase {
private final TrendService service; // ❌ Force à dépendre de tout !
public TrendResult execute(TrendRequest request) {
return service.getTrends(request.toQuery())
.orElseThrow();
}
}Problèmes :
- ❌
GetTrendsUseCasedépend de méthodes qu’il n’utilise jamais - ❌ Changement dans
sendEmail()= recompilation deGetTrendsUseCase - ❌ Interface difficile à implémenter (trop de méthodes)
D - Dependency Inversion Principle
Le principe fondamental de l’architecture hexagonale !
Dépendre des abstractions (interfaces), pas des implémentations concrètes
❌ Violation
Violation DIP
// ❌ MAUVAIS : Dépendance directe à l'implémentation
package org.smoka.application.usecase;
import org.smoka.infrastructure.adapter.TrendRepositoryAdapter; // ❌
@Singleton
public class GetTrendsUseCase {
private final TrendRepositoryAdapter adapter; // ❌ Classe concrète
public GetTrendsUseCase(TrendRepositoryAdapter adapter) {
this.adapter = adapter;
}
}Problèmes :
- ❌ Impossible de changer d’implémentation
- ❌ Couplage fort avec l’infrastructure
- ❌ Tests difficiles (dépend d’une classe concrète)
Domain-Driven Design (DDD)
DDD structure le code autour du langage métier (Ubiquitous Language).
Ubiquitous Language
Le code doit utiliser le même vocabulaire que les experts métier.
❌ Mauvais
Vocabulaire Technique
// ❌ MAUVAIS : Vocabulaire technique, pas métier
public class DataProcessor {
public Response processRequest(Map<String, Object> params) {
String str1 = (String) params.get("param1");
String str2 = (String) params.get("param2");
// Appel API
String jsonResult = apiClient.fetch(str1, str2);
// Parsing
Map<String, Object> data = parser.parse(jsonResult);
return new Response(data);
}
}Problèmes :
- ❌ Impossible de comprendre le métier
- ❌
param1,param2,data= aucun sens métier - ❌ Expert métier ne reconnaît pas son domaine
Value Objects
Les Value Objects représentent des concepts métier immuables et sans identité.
❌ Primitives
Obsession des Primitives
// ❌ MAUVAIS : Primitives partout
public class User {
private String email; // ❌ Pas de validation
private String phone; // ❌ Pas de validation
public User(String email, String phone) {
this.email = email; // ❌ Email invalide possible
this.phone = phone; // ❌ Phone invalide possible
}
}
// Création avec données invalides
var user = new User("invalid-email", "abc"); // ❌ Compile !Problèmes :
- ❌ Pas de validation
- ❌ Duplication de la validation partout
- ❌ Pas de règles métier encapsulées
Conventions de Nommage
Un bon nom explique l’intention sans nécessiter de commentaire.
Nommage des Classes
Domain
Domain Layer
| Type | Convention | Exemples |
|---|---|---|
| Entité | Nom métier | User, Order, Product |
| Value Object | Nom métier | Email, Money, Address |
| Port Output | Verbe + Repository/Service | TrendRepository, NotificationService |
| Exception | Nom métier + Exception | InvalidEmailException, OrderNotFoundException |
Exemple complet :
// Entité
public class Order { }
// Value Object
@Value
public class OrderId {
Long value;
}
// Port
public interface OrderRepository {
Optional<Order> findById(OrderId id);
}
// Exception
public class OrderNotFoundException extends RuntimeException { }Nommage des Méthodes
Une méthode bien nommée = auto-documentée.
❌ Mauvais
Noms Vagues
// ❌ MAUVAIS : Noms vagues, pas clairs
public class TrendService {
public Data get(String s1, String s2) { }
public void doStuff(Object o) { }
public boolean check(Data d) { }
public void process() { }
}Problèmes :
- ❌ Impossible de comprendre sans lire le code
- ❌
get,doStuff,check,process= trop vague - ❌ Nécessite des commentaires
Principes de Clean Code
Fonctions Courtes
Une fonction = Une chose = Bien la faire
❌ Fonction Longue
Fonction Trop Longue
// ❌ MAUVAIS : Fonction de 50 lignes qui fait tout
public TrendResponseDto getTrends(String keyword, String region) {
// Validation (5 lignes)
if (keyword == null) throw new IllegalArgumentException();
if (keyword.length() < 2) throw new IllegalArgumentException();
if (!keyword.matches("[a-zA-Z0-9 _-]+")) throw new IllegalArgumentException();
// Appel API (10 lignes)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/trends"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request);
// Parsing JSON (10 lignes)
JsonNode root = objectMapper.readTree(response.body());
JsonNode trendsNode = root.get("trends");
List<TrendDto> trends = new ArrayList<>();
for (JsonNode node : trendsNode) {
trends.add(new TrendDto(
node.get("keyword").asText(),
node.get("value").asInt()
));
}
// Logique métier (10 lignes)
// ... encore 20 lignes
return new TrendResponseDto(trends);
}Problèmes :
- ❌ Impossible à tester unitairement
- ❌ Difficile à comprendre
- ❌ Trop de responsabilités
Éviter les Commentaires
Le code doit s’expliquer lui-même. Les commentaires mentent avec le temps.
❌ Avec Commentaires
Code avec Commentaires
// ❌ MAUVAIS : Commentaires partout
public class TrendService {
// Récupère les tendances pour un mot-clé et une région
public TrendResult getTrends(String k, String r) {
// Vérifier que le mot-clé n'est pas null
if (k == null) {
throw new IllegalArgumentException();
}
// Appeler le repository
Optional<TrendResult> result = repo.get(k, r);
// Retourner le résultat ou lancer une exception
return result.orElseThrow();
}
}Problèmes :
- ❌ Commentaires compensent des noms vagues (
k,r,repo.get) - ❌ Commentaires vont devenir obsolètes
Quand utiliser des commentaires ?
- ✅ Expliquer le pourquoi (décisions architecturales)
- ✅ Documentation Javadoc pour APIs publiques
- ❌ Jamais pour expliquer le quoi (le code doit être clair)
Checklist Clean Code
Principes SOLID
- SRP : Chaque classe a une seule responsabilité
- OCP : Extension sans modification (interfaces + implémentations)
- LSP : Les implémentations respectent le contrat
- ISP : Interfaces petites et ciblées
- DIP : Dépendre des abstractions, pas des implémentations
Domain-Driven Design
- Vocabulaire métier utilisé partout (Ubiquitous Language)
- Value Objects pour encapsuler les règles métier
- Entités avec identité claire
- Pas de dépendances techniques dans le domain
Conventions de Nommage
- Classes : Noms métier explicites
- Méthodes : Verbe + intention claire
- Variables : Pas d’abréviations (
request, pasreq) - Packages : Organisation par couche (domain, application, infrastructure)
Clean Code
- Fonctions courtes (3-10 lignes)
- Pas de commentaires inutiles (code auto-documenté)
- Pas de duplication (DRY - Don’t Repeat Yourself)
- Tests pour chaque comportement
Récapitulatif
Un code propre = SOLID + DDD + Conventions claires + Fonctions courtes
Règles d’Or
- SRP : Une classe = Une responsabilité
- DIP : Dépendre des abstractions (architecture hexagonale !)
- Ubiquitous Language : Code = langage métier
- Value Objects : Encapsuler les règles de validation
- Fonctions courtes : 3-10 lignes maximum
- Code auto-documenté : Pas de commentaires inutiles
Prochaines Étapes
Vous maîtrisez maintenant les principes de code propre. Découvrez les optimisations !
- 📖 Performance - Optimiser Micronaut et AOT
- 📖 Sécurité - Validation, sanitization, authentification
- 📖 Pièges Courants - Éviter les erreurs classiques
- 📖 Architecture Tests - Garantir SOLID avec ArchUnit