Introduction
Quand nous avons commencé à concevoir l'architecture de ProSilicones64 chez Portocarrero, nous avions un problème clair : le catalogue n'était pas une simple liste de produits. C'était 30 produits pouvant être fabriqués dans 19 séries de silicone différentes, chaque série avec ses propres propriétés thermiques et de dureté, applicables à 10 secteurs industriels différents, et soumis à 127 normes et certifications.
Si nous avions tout mis dans un ProductController de 2000 lignes avec des switches et des conditions, le système aurait fonctionné... pendant trois mois. Après, chaque modification aurait été un cauchemar.
Les principes SOLID nous ont donné la structure pour permettre au code de grandir. Nous ne les avons pas appliqués par purisme académique, mais parce qu'un catalogue industriel complexe a besoin d'une architecture qui reflète cette complexité sans devenir un monstre inmaintenable.
Le problème
Le WordPress que nous avons remplacé avait exactement les problèmes que SOLID prévient. Il y avait un plugin catalogue avec une classe CatalogManager qui faisait tout : requêtes produits, calcul de prix, validation de stock, génération de PDFs, envoi d'emails. 1 847 lignes dans un seul fichier.
Chaque fois que le client demandait un changement — « ajoute le secteur naval », « affiche la température maximale sur la fiche » — il fallait plonger dans ce fichier, prier pour ne rien casser, et tester manuellement parce que les tests unitaires étaient impossibles.
- 1 847 lignes dans une seule classe CatalogManager
- 34 méthodes publiques faisant tout
- 12 dépendances en dur (MySQL, mailer, logger...)
- 0 tests unitaires possibles à cause du couplage
- Chaque changement = risque de régression
C'est ce qui arrive quand on n'applique pas SRP. Et ça entraîne les autres problèmes : on ne peut pas étendre sans modifier (viole OCP), on ne peut pas substituer les composants (viole LSP), on dépend d'implémentations concrètes (viole DIP).
SRP : Chaque classe fait une seule chose
Nous avons divisé le monolithe en pièces avec des responsabilités claires
Dans ProSilicones64, les relations produit-série-secteur-norme sont le cœur du métier. Si une seule classe gère tout, n'importe quel changement aux certifications affecte le catalogue, l'exporteur, les fiches techniques. Nous avons séparé par responsabilité : chaque classe a une seule raison de changer.
Avant : CatalogManager monolithique (WordPress)
<?php
class CatalogManager
{
public function getProduct($id)
{
// Requête SQL directe
$product = $this->db->query('SELECT * FROM products WHERE id = ?', [$id]);
// Calcule le prix avec marge
$product['price'] = $product['cost'] * 1.3;
// Récupère les séries applicables
$product['series'] = $this->db->query('SELECT * FROM series WHERE...');
// Récupère les certifications
$product['certs'] = $this->db->query('SELECT * FROM certifications WHERE...');
// Génère le HTML de la fiche
$product['html'] = $this->renderProductCard($product);
// Log
$this->logger->info('Product retrieved');
return $product;
}
// ... 33 autres méthodes
}
- 6 responsabilités dans une seule méthode
- Impossible à tester sans vraie base de données
- Changer le calcul de prix affecte tout
- Changer le logger nécessite de modifier la classe
Après : Services séparés par responsabilité
<?php
// Requêtes produits uniquement
class ProductRepository
{
public function findById(int $id): ?Product
{
return $this->db->query(
'SELECT * FROM products WHERE id = ?',
[$id]
)->fetch(Product::class);
}
}
// Logique des séries uniquement
class SeriesService
{
public function getForProduct(int $productId): array
{
return $this->seriesRepo->findByProduct($productId);
}
}
// Validation des certifications uniquement
class CertificationValidator
{
public function validate(Product $p, Series $s, Sector $sec): bool
{
$required = $sec->getRequiredCertifications();
$available = $s->getCertifications();
return empty(array_diff($required, $available));
}
}
- Chaque classe testable de façon isolée
- Changer la validation ne touche pas le repository
- Réutilisable : SeriesService sert au catalogue, fiches, exports
- Nouveaux devs comprennent le code en heures, pas en semaines
OCP : Ajouter des secteurs sans modifier le code
Ouvert à l'extension, fermé à la modification
Quand le client a dit « on ajoute le secteur naval », nous n'avons eu à toucher aucune classe existante. Chaque secteur a sa classe de prérequis implémentant une interface commune. Ajouter un secteur = créer une classe + insérer des données en BD.
Avant : Switch infini qui grandit avec chaque secteur
<?php
class SectorValidator
{
public function getRequirements(string $sector): array
{
switch ($sector) {
case 'railway':
return ['EN45545-2-HL2', 'EN45545-2-HL3'];
case 'food':
return ['FDA-21CFR177', 'EU-1935-2004'];
case 'medical':
return ['ISO-10993', 'USP-CLASS-VI'];
// Chaque nouveau secteur = modifier ce switch
}
}
}
- Ajouter le secteur naval = modifier la classe existante
- Le switch grandit à l'infini
- Une erreur dans un case affecte tous
- Les équipes ne peuvent pas travailler en parallèle
Après : Pattern Strategy avec interface commune
<?php
interface SectorRequirements
{
public function getCertifications(): array;
public function validateSeries(Series $series): bool;
}
class RailwaySectorRequirements implements SectorRequirements
{
public function getCertifications(): array
{
return ['EN45545-2-HL2', 'EN45545-2-HL3'];
}
public function validateSeries(Series $series): bool
{
return $series->maxTemperature >= 150;
}
}
// Ajouter le secteur naval = créer cette classe. Rien d'autre.
class NavalSectorRequirements implements SectorRequirements
{
public function getCertifications(): array
{
return ['DNV-GL', 'USCG-164'];
}
public function validateSeries(Series $series): bool
{
return $series->saltWaterResistant === true;
}
}
- Secteur naval implémenté en 4 heures (vs 2-3 jours)
- Code existant intact = 0 régression
- Chaque secteur testable indépendamment
- Équipes peuvent travailler sur secteurs en parallèle
LSP : Tous les exporteurs sont interchangeables
Les sous-classes substituent leurs parents sans rien casser
ProSilicones64 exporte des fiches techniques en PDF, des données pour ERP en CSV, et des catalogues en XML. Tous les exporteurs implémentent la même interface : reçoivent un Product, retournent un string. Le contrôleur ne sait pas et s'en fiche du format généré.
Avant : Exporteurs inconsistants
<?php
class PdfExporter
{
public function export($product)
{
return $this->pdf->generate($product); // Binaire
}
}
class CsvExporter
{
public function export($product)
{
return [$product['name'], $product['sku']]; // Array
}
}
class XmlExporter
{
public function export($product)
{
echo '<product>' . $product['name'] . '</product>'; // Affiche
}
}
- Chaque exporteur retourne quelque chose de différent
- Le contrôleur a besoin de if/else par type
- Ne peuvent pas être échangés
- Les tests doivent vérifier un comportement spécifique
Après : Contrat commun, comportement prévisible
<?php
interface ProductExporter
{
public function export(Product $product): string;
public function getContentType(): string;
}
class PdfExporter implements ProductExporter
{
public function export(Product $product): string
{
$html = $this->template->render('fiche-technique', $product);
return $this->pdfEngine->fromHtml($html);
}
public function getContentType(): string { return 'application/pdf'; }
}
// Contrôleur générique qui fonctionne avec n'importe quel exporteur
class ExportController
{
public function download(int $id, string $format): Response
{
$product = $this->products->findById($id);
$exporter = $this->factory->create($format);
return new Response(
$exporter->export($product),
['Content-Type' => $exporter->getContentType()]
);
}
}
- Ajouter XLSX = créer classe, enregistrer dans factory
- Le contrôleur ne change jamais
- Tests avec mock générique pour tout exporteur
- Temps d'implémentation nouveau format : 2 heures
ISP : Interfaces petites et spécifiques
Personne ne dépend de méthodes qu'il n'utilise pas
Les vues publiques du catalogue ne font que lire. Le panel admin lit et écrit. Le système de recherche ne fait que filtrer. Au lieu d'une interface géante que tout le monde implémente, nous séparons par cas d'usage.
Avant : Interface gonflée obligeant à tout implémenter
<?php
interface ProductRepositoryInterface
{
public function findById(int $id): ?Product;
public function findBySlug(string $slug): ?Product;
public function findAll(): array;
public function save(Product $product): void;
public function delete(int $id): void;
public function search(string $query): array;
public function export(string $format): string;
public function import(string $data): void;
// 12 méthodes au total
}
// Le catalogue public implémente ça mais laisse 6 méthodes vides
- Classes avec méthodes vides ou throw Exception
- Changer search() affecte ceux qui ne font que lire
- Les tests doivent mocker 12 méthodes pour en utiliser 2
- Violation implicite de SRP
Après : Interfaces ségrégées par usage
<?php
interface ReadableProducts
{
public function findById(int $id): ?Product;
public function findBySlug(string $slug): ?Product;
}
interface WritableProducts
{
public function save(Product $product): void;
public function delete(int $id): void;
}
interface SearchableProducts
{
public function search(string $query): array;
}
// Le catalogue public n'a besoin que de lire
class PublicCatalogController
{
public function __construct(private ReadableProducts $products) {}
}
// L'admin a besoin de lire et écrire
class AdminController
{
public function __construct(
private ReadableProducts $reader,
private WritableProducts $writer
) {}
}
- Chaque contrôleur ne dépend que de ce qu'il utilise
- Les tests mockent 2-3 méthodes, pas 12
- Les changements de recherche n'affectent pas le catalogue
- Code mort éliminé : -70%
DIP : La logique ne connaît pas l'infrastructure
Dépendre des abstractions, pas des implémentations
Le SeriesService ne sait pas si le cache est Redis, des fichiers JSON, ou de la mémoire. Il sait juste qu'il a quelque chose qui implémente CacheInterface. Changer de système de cache = changer une ligne dans le conteneur de dépendances.
Avant : Dépendances en dur
<?php
class ProductService
{
public function __construct()
{
// Dépendances concrètes, impossible à changer
$this->cache = new RedisCache('localhost:6379');
$this->db = new MySQLConnection('host=db user=root');
}
}
- Les tests nécessitent Redis et MySQL réels
- Passer à Memcached = réécrire la classe
- Impossible à utiliser dans un environnement sans Redis
- La classe en sait trop sur l'infrastructure
Après : Injection d'abstractions
<?php
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl): void;
}
class ProductService
{
public function __construct(
private CacheInterface $cache,
private ProductRepository $repository
) {}
}
// En production
$service = new ProductService(new FileCache('/var/cache'), $repo);
// En tests
$service = new ProductService(new ArrayCache(), $mockRepo);
- Tests sans infrastructure réelle : s'exécutent en 0,3s
- Migrer FileCache vers Redis = 1 ligne
- Environnement de dev sans dépendances externes
- La logique métier ne change pas quand le cache change
Métriques réelles du projet ProSilicones64
Comparatif entre le WordPress original et l'architecture SOLID après 6 mois en production :
| Métrique | WordPress original | Architecture SOLID |
|---|---|---|
| Fichier le plus gros | 1 847 lignes | 187 lignes |
| Temps pour ajouter un secteur | 2-3 jours | 4 heures |
| Temps pour nouvel exporteur | 1-2 jours | 2 heures |
| Tests unitaires | 0 | 47 |
| Bugs en 6 mois | N/A | 3 mineurs |
| Changements causant régression | Fréquents | 0 |
Conclusion
Chez Portocarrero, nous n'appliquons pas SOLID parce que ça fait bien sur un CV. Nous l'appliquons parce qu'un catalogue industriel avec des relations complexes a besoin d'une architecture qui peut grandir. Si vous mettez tout dans un fichier géant, ça marchera aujourd'hui et ce sera l'enfer demain.
Les cinq principes se résument à une idée : chaque pièce fait une chose, les pièces se connectent par des contrats clairs, et ajouter une fonctionnalité signifie ajouter de nouvelles pièces, pas modifier celles qui existent.
ProSilicones64 est en production depuis six mois. Il a grandi avec trois nouveaux secteurs, deux formats d'export, une intégration ERP, et un système de fiches techniques dynamiques. Le code du premier jour fonctionne toujours sans changement. C'est ce que SOLID permet.
Si chaque changement dans votre système est une aventure, si vous avez des fichiers de milliers de lignes que personne ne veut toucher, ou si ajouter une fonctionnalité signifie des semaines de travail... votre architecture a besoin d'une révision. Chez Portocarrero nous pouvons faire un audit technique : identifier les couplages, mesurer la complexité, et proposer un plan de refactoring avec des priorités claires.
Demander un audit de code →