Mettre en œuvre la Clean Architecture dans une application Spring Boot et Angular
Dans le monde du développement logiciel, la complexité des applications ne cesse de croître. Pour les systèmes d'entreprise, les applications de gestion hospitalière ou les plateformes de gestion des risques, une architecture robuste et maintenable est primordiale. C'est dans ce contexte que la Clean Architecture, popularisée par Robert C. Martin (Uncle Bob), offre une approche structurée pour construire des applications résilientes, testables et indépendantes des frameworks et des bases de données.
La Clean Architecture vise à séparer les préoccupations d'une application en couches concentriques, chacune ayant un rôle bien défini et des dépendances strictes vers les couches intérieures. Pour un Développeur Full Stack à Dakar, Sénégal, expert en Java Spring Boot et Angular comme Laty Gueye Samba, l'adoption de cette méthodologie est cruciale pour développer des applications performantes et évolutives. Cet article explore comment un Expert Java Spring Boot Angular peut intégrer efficacement les principes de la Clean Architecture dans des projets impliquant ces deux technologies.
L'objectif de cet article est de détailler les concepts clés de la Clean Architecture et de fournir des exemples concrets de son implémentation tant côté backend avec Spring Boot que côté frontend avec Angular, permettant ainsi de construire une architecture logicielle cohérente et facile à maintenir.
Principes fondamentaux de la Clean Architecture
Au cœur de la Clean Architecture se trouve le principe de la "Règle des Dépendances". Cette règle stipule que les dépendances doivent toujours pointer vers l'intérieur, c'est-à-dire que les couches extérieures peuvent dépendre des couches intérieures, mais jamais l'inverse. Les couches sont généralement structurées comme suit, de l'intérieur vers l'extérieur :
- Entities (Entités) : Contiennent la logique métier de l'entreprise. Ce sont les règles les plus générales et de haut niveau.
- Use Cases (Cas d'utilisation) : Contiennent la logique métier spécifique à l'application. Ils orchestrent le flux de données vers et depuis les entités.
- Interface Adapters (Adaptateurs d'interface) : Convertissent les données des couches intérieures en formats compréhensibles par les frameworks et bases de données externes, et vice-versa.
- Frameworks & Drivers (Frameworks et Pilotes) : Composent la couche la plus externe, incluant les bases de données, les frameworks web (Spring Boot, Angular), l'interface utilisateur, etc.
L'indépendance est le maître mot : indépendance des frameworks, de l'interface utilisateur, de la base de données, et de tout dispositif externe. Cette indépendance assure une flexibilité et une testabilité accrues, des qualités indispensables pour tout développeur full stack confronté à des exigences métier complexes.
Implémentation de la Clean Architecture avec Spring Boot (Backend)
Pour le backend Spring Boot, la Clean Architecture se traduit par une organisation modulaire des packages et des classes, respectant la Règle des Dépendances. Voici une approche courante :
1. Couche Domaine (Entities)
Cette couche contient la logique métier pure, indépendante de toute persistance ou framework. Elle inclut les entités et les objets de valeur.
// src/main/java/com/laty/cleanarch/domain/model/Product.java
package com.laty.cleanarch.domain.model;
import java.math.BigDecimal;
public class Product {
private Long id;
private String name;
private BigDecimal price;
private int quantity;
public Product(Long id, String name, BigDecimal price, int quantity) {
this.id = id;
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public void decreaseQuantity(int amount) {
if (this.quantity < amount) {
throw new IllegalArgumentException("Not enough stock.");
}
this.quantity -= amount;
}
}
2. Couche Application (Use Cases / Ports)
Cette couche définit les cas d'utilisation de l'application. Elle contient les interfaces des services métier (ports entrants) et des dépôts (ports sortants), mais pas leurs implémentations. Elle orchestre les entités pour réaliser les opérations métier.
// src/main/java/com/laty/cleanarch/application/port/in/ManageProductUseCase.java
package com.laty.cleanarch.application.port.in;
import com.laty.cleanarch.domain.model.Product;
import java.util.List;
import java.util.Optional;
public interface ManageProductUseCase {
Product createProduct(Product product);
Optional<Product> getProductById(Long id);
List<Product> getAllProducts();
Product updateProduct(Long id, Product product);
void deleteProduct(Long id);
void sellProduct(Long productId, int quantity);
}
// src/main/java/com/laty/cleanarch/application/port/out/ProductRepositoryPort.java
package com.laty.cleanarch.application.port.out;
import com.laty.cleanarch.domain.model.Product;
import java.util.List;
import java.util.Optional;
public interface ProductRepositoryPort {
Product save(Product product);
Optional<Product> findById(Long id);
List<Product> findAll();
void deleteById(Long id);
}
L'implémentation du cas d'utilisation se trouve également dans cette couche, dépendant des ports de sortie :
// src/main/java/com/laty/cleanarch/application/service/ManageProductService.java
package com.laty.cleanarch.application.service;
import com.laty.cleanarch.application.port.in.ManageProductUseCase;
import com.laty.cleanarch.application.port.out.ProductRepositoryPort;
import com.laty.cleanarch.domain.model.Product;
import org.springframework.stereotype.Service; // Annotation Spring
import java.util.List;
import java.util.Optional;
@Service
public class ManageProductService implements ManageProductUseCase {
private final ProductRepositoryPort productRepositoryPort;
public ManageProductService(ProductRepositoryPort productRepositoryPort) {
this.productRepositoryPort = productRepositoryPort;
}
@Override
public Product createProduct(Product product) {
return productRepositoryPort.save(product);
}
@Override
public Optional<Product> getProductById(Long id) {
return productRepositoryPort.findById(id);
}
@Override
public List<Product> getAllProducts() {
return productRepositoryPort.findAll();
}
@Override
public Product updateProduct(Long id, Product updatedProduct) {
return productRepositoryPort.findById(id)
.map(product -> {
product.setName(updatedProduct.getName());
product.setPrice(updatedProduct.getPrice());
product.setQuantity(updatedProduct.getQuantity());
return productRepositoryPort.save(product);
})
.orElseThrow(() -> new IllegalArgumentException("Product not found with id: " + id));
}
@Override
public void deleteProduct(Long id) {
productRepositoryPort.deleteById(id);
}
@Override
public void sellProduct(Long productId, int quantity) {
Product product = productRepositoryPort.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("Product not found."));
product.decreaseQuantity(quantity); // Logique métier dans l'entité
productRepositoryPort.save(product);
}
}
3. Couche Infrastructure & Adapters (Frameworks & Drivers)
C'est la couche la plus externe, qui contient les détails d'implémentation. Elle inclut les contrôleurs REST, les implémentations de dépôts utilisant JPA, et d'autres intégrations de frameworks.
// src/main/java/com/laty/cleanarch/infrastructure/adapter/out/persistence/ProductJpaAdapter.java
package com.laty.cleanarch.infrastructure.adapter.out.persistence;
import com.laty.cleanarch.application.port.out.ProductRepositoryPort;
import com.laty.cleanarch.domain.model.Product;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
public class ProductJpaAdapter implements ProductRepositoryPort {
private final ProductJpaRepository productJpaRepository; // Une interface JPA classique
public ProductJpaAdapter(ProductJpaRepository productJpaRepository) {
this.productJpaRepository = productJpaRepository;
}
@Override
public Product save(Product product) {
// Mappage de Product vers une entité JPA si nécessaire.
// Ici, nous supposons que Product est directement une entité JPA pour simplifier.
return productJpaRepository.save(product);
}
@Override
public Optional<Product> findById(Long id) {
return productJpaRepository.findById(id);
}
@Override
public List<Product> findAll() {
return productJpaRepository.findAll();
}
@Override
public void deleteById(Long id) {
productJpaRepository.deleteById(id);
}
}
// src/main/java/com/laty/cleanarch/infrastructure/adapter/in/web/ProductController.java
package com.laty.cleanarch.infrastructure.adapter.in.web;
import com.laty.cleanarch.application.port.in.ManageProductUseCase;
import com.laty.cleanarch.domain.model.Product;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ManageProductUseCase manageProductUseCase;
public ProductController(ManageProductUseCase manageProductUseCase) {
this.manageProductUseCase = manageProductUseCase;
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product createdProduct = manageProductUseCase.createProduct(product);
return ResponseEntity.ok(createdProduct);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return manageProductUseCase.getProductById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = manageProductUseCase.getAllProducts();
return ResponseEntity.ok(products);
}
// Autres endpoints (PUT, DELETE, etc.)
}
Implémentation de la Clean Architecture avec Angular (Frontend)
Côté frontend, Angular peut également bénéficier de la Clean Architecture pour maintenir la clarté et la testabilité, en particulier dans les applications métier complexes. L'objectif est de séparer l'UI (composants) de la logique métier et des appels API.
1. Couche Domaine (Models)
Cette couche définit les interfaces et les types pour les entités de l'application frontend. C'est la représentation des données métier.
// src/app/core/domain/product.model.ts
export interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
2. Couche Application (Services / Use Cases)
Les services Angular jouent le rôle des cas d'utilisation. Ils encapsulent la logique métier spécifique à l'application frontend et interagent avec les adaptateurs (les services HTTP ou les services de persistance locaux).
// src/app/core/application/product.usecase.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Product } from '../domain/product.model';
import { ProductGateway } from '../domain/product.gateway'; // Port sortant
@Injectable({
providedIn: 'root'
})
export class ProductUseCase {
constructor(private productGateway: ProductGateway) {} // Injection du port
getAllProducts(): Observable<Product[]> {
return this.productGateway.getAll();
}
getProductById(id: number): Observable<Product> {
return this.productGateway.getById(id);
}
createProduct(product: Product): Observable<Product> {
return this.productGateway.create(product);
}
// Autres méthodes pour gérer les produits (update, delete, sell)
}
Le ProductGateway est une interface (port) qui serait définie dans la couche domaine pour découpler la logique d'application de l'implémentation de l'accès aux données.
// src/app/core/domain/product.gateway.ts
import { Observable } from 'rxjs';
import { Product } from './product.model';
export abstract class ProductGateway {
abstract getAll(): Observable<Product[]>;
abstract getById(id: number): Observable<Product>;
abstract create(product: Product): Observable<Product>;
abstract update(id: number, product: Product): Observable<Product>;
abstract delete(id: number): Observable<void>;
}
3. Couche Infrastructure & Adapters (Components / API Services)
Cette couche contient l'implémentation de l'interface utilisateur (composants) et les services qui interagissent directement avec l'API backend.
// src/app/infrastructure/api/product-api.service.ts (Adapter)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Product } from '../../core/domain/product.model';
import { ProductGateway } from '../../core/domain/product.gateway'; // Implémente le port
@Injectable({
providedIn: 'root'
})
export class ProductApiService extends ProductGateway { // Implémente l'interface
private apiUrl = 'http://localhost:8080/products'; // URL de l'API Spring Boot
constructor(private http: HttpClient) {
super();
}
getAll(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
getById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
create(product: Product): Observable<Product> {
return this.http.post<Product>(this.apiUrl, product);
}
update(id: number, product: Product): Observable<Product> {
return this.http.put<Product>(`${this.apiUrl}/${id}`, product);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Les composants se contentent de récupérer les données via les cas d'utilisation et de les afficher, en évitant toute logique métier complexe.
// src/app/presentation/product-list/product-list.component.ts (Framework & Driver)
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Product } from '../../core/domain/product.model';
import { ProductUseCase } from '../../core/application/product.usecase';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
products$: Observable<Product[]>;
constructor(private productUseCase: ProductUseCase) {} // Injection du cas d'utilisation
ngOnInit(): void {
this.products$ = this.productUseCase.getAllProducts();
}
// Méthodes pour gérer les interactions utilisateur, qui appellent le useCase
onProductSelected(id: number): void {
// Naviguer vers les détails, etc.
}
}
Point de vue : développeur full stack à Dakar
Pour un développeur travaillant sur des systèmes comme des applications de gestion des risques ou de gestion hospitalière, la maîtrise de l'architecture logicielle propre, et notamment de la Clean Architecture, représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. Cette approche garantit la livraison de solutions robustes et facilement maintenables, des qualités très recherchées par les entreprises de la région.
Conclusion
L'implémentation de la Clean Architecture dans une application combinant Spring Boot et Angular est une démarche stratégique pour tout Développeur Full Stack à Dakar, Sénégal soucieux de la qualité et de la pérennité de ses projets. Cette approche permet de créer des applications hautement testables, indépendantes des technologies externes et dont l'évolution est facilitée. La séparation des préoccupations en couches distinctes, comme démontré par Laty Gueye Samba dans des applications métier complexes, offre une flexibilité indispensable pour s'adapter aux changements d'exigences et aux évolutions technologiques.
En adoptant ces principes d'architecture logicielle, les équipes de développement peuvent construire des systèmes résilients, performants et faciles à maintenir, répondant ainsi aux défis posés par les applications d'entreprise modernes. Un Expert Java Spring Boot Angular qui maîtrise ces concepts est un atout inestimable pour tout projet de développement.
Pour approfondir vos connaissances sur la Clean Architecture, il est recommandé de consulter les ressources officielles :
À propos de l'auteur
Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.
Contact : latygueyesamba@gmail.com | Dakar, Sénégal