Introduction
Les principes de Clean Code sont souvent présentés isolément : SOLID, DRY, KISS, YAGNI… Chacun fait sens individuellement, mais leur véritable puissance se révèle dans leurs interactions. Lorsqu’ils sont appliqués ensemble, ces principes se renforcent mutuellement pour créer un système cohérent et robuste.
Dans cet article, nous allons explorer comment trois principes fondamentaux - l’inversion de dépendance (DIP), la source unique de vérité (SSOT) et Don’t Repeat Yourself (DRY) - s’articulent pour produire du code véritablement maintenable.
Le problème : des principes isolés
Code sans principes
Commençons par un exemple typique de code qui ignore ces principes :
// ❌ Mauvais exemple
class UserService {
async createUser(email: string, password: string) {
// Validation dupliquée partout
if (!email.includes('@')) {
throw new Error('Invalid email');
}
if (password.length < 8) {
throw new Error('Password too short');
}
// Logique métier couplée à l'infrastructure
const hashedPassword = await bcrypt.hash(password, 10);
const result = await db.query(
'INSERT INTO users (email, password) VALUES (?, ?)',
[email, hashedPassword]
);
// Envoi d'email couplé
await fetch('https://mailservice.com/api/send', {
method: 'POST',
body: JSON.stringify({
to: email,
template: 'welcome',
}),
});
return result;
}
async updateUser(id: string, email: string) {
// Validation re-dupliquée
if (!email.includes('@')) {
throw new Error('Invalid email');
}
await db.query('UPDATE users SET email = ? WHERE id = ?', [email, id]);
}
}
Ce code présente plusieurs problèmes :
- Logique métier couplée à l’infrastructure (database, HTTP)
- Validation dupliquée
- Impossible à tester sans base de données réelle
- Modification d’un service externe = modification de toute l’application
Premier principe : Inversion de dépendance (DIP)
Définition
Le code métier ne doit pas dépendre des détails d’implémentation. Les deux doivent dépendre d’abstractions.
Application
// ✅ Abstractions
interface UserRepository {
save(user: User): Promise<void>;
findById(id: string): Promise<User | null>;
update(user: User): Promise<void>;
}
interface EmailService {
sendWelcome(email: string): Promise<void>;
}
interface PasswordHasher {
hash(password: string): Promise<string>;
}
// ✅ Logique métier pure
class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService,
private passwordHasher: PasswordHasher
) {}
async createUser(email: string, password: string) {
const hashedPassword = await this.passwordHasher.hash(password);
const user = new User({
email,
password: hashedPassword,
});
await this.userRepository.save(user);
await this.emailService.sendWelcome(email);
return user;
}
}
Bénéfices immédiats :
- Testable avec des mocks
- Changement d’implémentation sans toucher au métier
- Dépendances explicites
Deuxième principe : Single Source of Truth (SSOT)
Le problème de la duplication
Même avec l’inversion de dépendance, nous avons encore de la duplication :
// ❌ Validation dupliquée
class UserService {
async createUser(email: string, password: string) {
if (!email.includes('@')) throw new Error('Invalid email');
// ...
}
async updateUser(id: string, email: string) {
if (!email.includes('@')) throw new Error('Invalid email');
// ...
}
}
Application de SSOT
Centralisons la logique dans l’entité elle-même :
// ✅ Source unique de vérité
class User {
private constructor(
private _id: string,
private _email: string,
private _password: string
) {}
static create(email: string, password: string): User {
User.validateEmail(email);
User.validatePassword(password);
return new User(
generateId(),
email,
password
);
}
updateEmail(newEmail: string): void {
User.validateEmail(newEmail);
this._email = newEmail;
}
private static validateEmail(email: string): void {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
if (email.length > 255) {
throw new Error('Email too long');
}
}
private static validatePassword(password: string): void {
if (password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
throw new Error('Password must contain uppercase letter');
}
}
get email(): string {
return this._email;
}
}
Nouveau service simplifié :
class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService,
private passwordHasher: PasswordHasher
) {}
async createUser(email: string, password: string) {
const hashedPassword = await this.passwordHasher.hash(password);
// La validation est dans User.create
const user = User.create(email, hashedPassword);
await this.userRepository.save(user);
await this.emailService.sendWelcome(user.email);
return user;
}
async updateUser(id: string, newEmail: string) {
const user = await this.userRepository.findById(id);
if (!user) throw new Error('User not found');
// La validation est dans user.updateEmail
user.updateEmail(newEmail);
await this.userRepository.update(user);
}
}
Troisième principe : Don’t Repeat Yourself (DRY)
Au-delà de la duplication de code
DRY ne concerne pas seulement la duplication de code, mais la duplication de connaissance et de décisions.
Duplication de connaissance
// ❌ Duplication de connaissance
class UserRepository {
async save(user: User) {
await db.query(
'INSERT INTO users (id, email, password) VALUES (?, ?, ?)',
[user.id, user.email, user.password]
);
}
}
class UserDTO {
static fromUser(user: User) {
return {
id: user.id,
email: user.email,
password: user.password,
};
}
}
// Les deux connaissent la structure de User
Solution : Centraliser la connaissance
// ✅ User connaît sa propre structure
class User {
// ...
toDatabase(): DatabaseRow {
return {
id: this._id,
email: this._email,
password: this._password,
created_at: this._createdAt,
};
}
toDTO(): UserDTO {
return {
id: this._id,
email: this._email,
// Note: pas de password dans le DTO
};
}
static fromDatabase(row: DatabaseRow): User {
// Reconstruction depuis la DB
return new User(
row.id,
row.email,
row.password,
row.created_at
);
}
}
La synergie des trois principes
Comment ils se renforcent
-
DIP + SSOT : L’inversion de dépendance crée des frontières claires où placer la logique (SSOT)
-
SSOT + DRY : Une source unique élimine naturellement la duplication de connaissance
-
DRY + DIP : Moins de duplication = moins de couplage = plus facile d’inverser les dépendances
Exemple complet
// Domain Layer (SSOT)
class User {
private constructor(
private _id: string,
private _email: Email, // Value Object
private _password: HashedPassword,
private _createdAt: Date
) {}
static create(email: string, password: string): User {
return new User(
generateId(),
Email.create(email), // Validation déléguée
HashedPassword.create(password),
new Date()
);
}
updateEmail(newEmail: string): void {
this._email = Email.create(newEmail);
}
// Serialization (DRY)
toDatabase(): DatabaseRow {
return {
id: this._id,
email: this._email.value,
password: this._password.value,
created_at: this._createdAt,
};
}
static fromDatabase(row: DatabaseRow): User {
return new User(
row.id,
Email.fromString(row.email),
HashedPassword.fromHash(row.password),
row.created_at
);
}
}
// Value Objects (SSOT + DRY)
class Email {
private constructor(private _value: string) {}
static create(email: string): Email {
if (!email.includes('@')) {
throw new Error('Invalid email');
}
return new Email(email);
}
static fromString(email: string): Email {
return new Email(email); // Déjà validé en DB
}
get value(): string {
return this._value;
}
}
// Application Layer (DIP)
class UserService {
constructor(
private userRepository: UserRepository, // Abstraction
private emailService: EmailService, // Abstraction
) {}
async createUser(email: string, password: string): Promise<User> {
const user = User.create(email, password);
await this.userRepository.save(user);
await this.emailService.sendWelcome(user.email);
return user;
}
}
// Infrastructure Layer (DIP)
class PostgresUserRepository implements UserRepository {
async save(user: User): Promise<void> {
const data = user.toDatabase(); // SSOT
await this.db.query(
'INSERT INTO users (id, email, password, created_at) VALUES ($1, $2, $3, $4)',
[data.id, data.email, data.password, data.created_at]
);
}
}
Bénéfices de l’approche combinée
1. Testabilité maximale
describe('UserService', () => {
it('should create user and send welcome email', async () => {
const mockRepo = new InMemoryUserRepository();
const mockEmail = new MockEmailService();
const service = new UserService(mockRepo, mockEmail);
await service.createUser('test@example.com', 'Password123');
expect(mockRepo.users).toHaveLength(1);
expect(mockEmail.sentEmails).toHaveLength(1);
});
});
2. Évolutivité
Changer de base de données :
// Avant : PostgreSQL
const userRepo = new PostgresUserRepository(pgClient);
// Après : MongoDB
const userRepo = new MongoUserRepository(mongoClient);
// UserService ne change pas !
const service = new UserService(userRepo, emailService);
3. Maintenabilité
Modification de la validation email :
// ✅ Un seul endroit à modifier
class Email {
static create(email: string): Email {
// Nouvelle règle : bloquer les emails temporaires
if (email.includes('tempmail')) {
throw new Error('Temporary emails not allowed');
}
if (!email.includes('@')) {
throw new Error('Invalid email');
}
return new Email(email);
}
}
// Tous les use cases bénéficient automatiquement
Pièges à éviter
Sur-ingénierie
// ❌ Trop d'abstraction
interface EmailValidator {
validate(email: string): boolean;
}
interface PasswordValidator {
validate(password: string): boolean;
}
class UserFactory {
constructor(
private emailValidator: EmailValidator,
private passwordValidator: PasswordValidator
) {}
}
// Pour créer un simple User !
Règle : N’introduire une abstraction que quand vous avez 2+ implémentations concrètes ou un besoin de test.
Mauvaise frontière de SSOT
// ❌ Validation dans le controller
class UserController {
async create(req: Request) {
if (!req.body.email.includes('@')) {
return res.status(400).send('Invalid email');
}
// ...
}
}
// ❌ Validation dans le repository
class UserRepository {
async save(user: User) {
if (!user.email.includes('@')) {
throw new Error('Invalid email');
}
// ...
}
}
Règle : La validation métier appartient au domaine (entities/value objects).
Conclusion
Les principes de Clean Code ne sont pas des règles isolées à appliquer mécaniquement. Ils forment un système cohérent où chaque principe renforce les autres :
- DIP crée des frontières claires et testables
- SSOT centralise la connaissance dans ces frontières
- DRY élimine la duplication et renforce la cohésion
Ensemble, ils produisent un code :
- Testable : grâce à l’inversion de dépendance
- Maintenable : grâce à la source unique de vérité
- Évolutif : grâce à l’élimination de la duplication
La prochaine fois que vous écrivez du code, ne pensez pas “j’applique SOLID” ou “j’applique DRY”. Pensez plutôt : “où est la connaissance ?”, “qui devrait en être responsable ?”, et “comment minimiser les dépendances ?”. Les principes suivront naturellement.
Points clés à retenir :
- Les principes Clean Code se renforcent mutuellement
- DIP + SSOT + DRY forment un système cohérent
- La connaissance doit avoir un propriétaire unique et clair
- L’abstraction sans besoin réel est de la sur-ingénierie
- Pensez “responsabilité” avant “pattern”