Retour aux articles

Mise en œuvre de la Clean Architecture dans un projet Full Stack Java/Angular : principes et pratique

Mise en œuvre de la Clean Architecture dans un projet Full Stack Java/Angular : principes et pratique | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
Mise en œuvre de la Clean Architecture dans un projet Full Stack Java/Angular : principes et pratique

L'architecture logicielle est le pilier de tout projet performant et durable. Dans un écosystème en constante évolution, la capacité d'un système à rester flexible, testable et maintenable est primordiale. C'est dans ce contexte que la Clean Architecture, popularisée par Robert C. Martin (Uncle Bob), offre une approche robuste pour concevoir des applications résilientes.

La Clean Architecture vise à séparer les préoccupations d'une application en couches distinctes, garantissant ainsi une indépendance vis-à-vis des frameworks, des bases de données et de l'UI. Pour un développeur Full Stack Java Spring Boot + Angular comme Laty Gueye Samba, basé à Dakar, la maîtrise de cette architecture logicielle est un atout majeur pour construire des systèmes fiables et évolutifs, adaptés aux exigences des entreprises locales et internationales.

Cet article explore les principes de la Clean Architecture et propose une feuille de route pour son implémentation pratique au sein d'un projet Spring Boot Angular, illustrant comment les compétences d'un Développeur Full Stack Dakar Sénégal peuvent être mises à profit pour des applications d'entreprise.

Les Principes Fondamentaux de la Clean Architecture

La Clean Architecture repose sur l'idée que les préoccupations d'une application doivent être organisées en couches concentriques, souvent représentées comme un oignon. Le principe clé est la Règle des Dépendances : le code des couches intérieures ne doit jamais dépendre du code des couches extérieures. Les dépendances ne peuvent se déplacer que de l'extérieur vers l'intérieur.

  • Entités (Entities / Domain) : Ce sont les objets métier de l'application et les règles d'affaires les plus générales et les plus stables. Elles ne doivent dépendre de rien.
  • Cas d'Utilisation (Use Cases / Application Business Rules) : Ils encapsulent les règles d'affaires spécifiques à l'application. Ils orchestrent le flux de données vers et depuis les Entités.
  • Adaptateurs d'Interface (Interface Adapters) : Cette couche convertit les données des cas d'utilisation vers des formats utilisables par les couches externes (présentation, persistance) et vice-versa. On y trouve les contrôleurs, les passerelles de base de données (interfaces de repositories), les présentateurs (View Models/DTOs).
  • Frameworks & Drivers : La couche la plus externe, elle contient les détails d'implémentation concrets tels que les frameworks web (Spring, Angular), les bases de données (JPA, JDBC), l'UI, les serveurs web, etc.

Cette séparation permet aux couches intérieures de rester pures et indépendantes des détails techniques externes, facilitant ainsi la maintenance, les tests unitaires et l'évolution future du système. L'expertise d'un Expert Java Spring Boot Angular est cruciale pour orchestrer cette complexité.

Mise en œuvre côté Backend avec Spring Boot

L'implémentation de la Clean Architecture dans un backend Spring Boot peut être structurée de la manière suivante, en mappant les couches architecturales aux modules ou packages Java :

1. Couche Domain (Entities)

Elle contient les POJOs (Plain Old Java Objects) représentant les concepts métier et leurs règles d'affaires. Cette couche ne dépend d'aucune infrastructure Spring ou de persistance.


// src/main/java/com/laty/cleanarch/domain/model/Product.java
package com.laty.cleanarch.domain.model;

public class Product {
    private Long id;
    private String name;
    private double price;

    // 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 double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }

    // Business Rule Example
    public boolean isValidPrice() {
        return this.price > 0;
    }
}
    

2. Couche Application (Use Cases)

Contient les services applicatifs (Use Cases) qui définissent les opérations que l'application peut effectuer. Ils collaborent avec les interfaces de persistance (définies dans l'Application) et les Entités.


// 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();
}

// src/main/java/com/laty/cleanarch/application/service/ProductService.java
package com.laty.cleanarch.application.service;

import com.laty.cleanarch.application.port.out.ProductRepositoryPort;
import com.laty.cleanarch.domain.model.Product;
import org.springframework.stereotype.Service; // Note: @Service is a Spring annotation, we bridge layers here

import java.util.List;
import java.util.Optional;

@Service // Cette annotation est un détail d'infrastructure, injectée via le Frameworks & Drivers
public class ProductService {
    private final ProductRepositoryPort productRepositoryPort;

    public ProductService(ProductRepositoryPort productRepositoryPort) {
        this.productRepositoryPort = productRepositoryPort;
    }

    public Product createProduct(Product product) {
        if (!product.isValidPrice()) {
            throw new IllegalArgumentException("Product price must be positive.");
        }
        return productRepositoryPort.save(product);
    }

    public Optional<Product> getProductById(Long id) {
        return productRepositoryPort.findById(id);
    }

    public List<Product> getAllProducts() {
        return productRepositoryPort.findAll();
    }
}
    

3. Couche Infrastructure (Interface Adapters & Frameworks/Drivers)

C'est ici que sont implémentés les détails techniques. Les contrôleurs REST, les implémentations concrètes des repositories (avec JPA par exemple), et la configuration Spring Boot résident ici. Ils adaptent les données vers et depuis la couche Application.


// src/main/java/com/laty/cleanarch/infrastructure/adapter/out/persistence/ProductJpaEntity.java
package com.laty.cleanarch.infrastructure.adapter.out.persistence;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class ProductJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    // Getters, Setters, Constructors...
}

// src/main/java/com/laty/cleanarch/infrastructure/adapter/out/persistence/ProductJpaRepository.java
package com.laty.cleanarch.infrastructure.adapter.out.persistence;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductJpaRepository extends JpaRepository<ProductJpaEntity, Long> {
}

// src/main/java/com/laty/cleanarch/infrastructure/adapter/out/persistence/ProductPersistenceAdapter.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;
import java.util.stream.Collectors;

@Component
public class ProductPersistenceAdapter implements ProductRepositoryPort {

    private final ProductJpaRepository productJpaRepository;

    public ProductPersistenceAdapter(ProductJpaRepository productJpaRepository) {
        this.productJpaRepository = productJpaRepository;
    }

    @Override
    public Product save(Product product) {
        ProductJpaEntity entity = mapToJpaEntity(product);
        ProductJpaEntity savedEntity = productJpaRepository.save(entity);
        return mapToDomain(savedEntity);
    }

    @Override
    public Optional<Product> findById(Long id) {
        return productJpaRepository.findById(id).map(this::mapToDomain);
    }

    @Override
    public List<Product> findAll() {
        return productJpaRepository.findAll().stream()
                .map(this::mapToDomain)
                .collect(Collectors.toList());
    }

    private ProductJpaEntity mapToJpaEntity(Product product) {
        // Mapping logic from Domain Product to JpaEntity
        ProductJpaEntity entity = new ProductJpaEntity();
        entity.setId(product.getId());
        entity.setName(product.getName());
        entity.setPrice(product.getPrice());
        return entity;
    }

    private Product mapToDomain(ProductJpaEntity entity) {
        // Mapping logic from JpaEntity to Domain Product
        Product product = new Product();
        product.setId(entity.getId());
        product.setName(entity.getName());
        product.setPrice(entity.getPrice());
        return product;
    }
}


// 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.service.ProductService;
import com.laty.cleanarch.domain.model.Product;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        try {
            Product createdProduct = productService.createProduct(product);
            return ResponseEntity.ok(createdProduct);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(null); // Or proper error handling DTO
        }
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return productService.getProductById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }
}
    

Approche Côté Frontend avec Angular

Bien que la Clean Architecture soit principalement axée sur le backend, ses principes peuvent inspirer la structuration d'une application Angular pour une meilleure séparation des préoccupations :

  • Couche Domaine (Modèles et Interfaces) : Définir des interfaces et des classes pour les objets métier (par exemple, Product, User) qui sont indépendantes des détails de l'API ou de l'UI.
  • Couche Application (Services) : Les services Angular gèrent la logique d'interaction avec le backend (appels HTTP). Ils peuvent également contenir des règles métier spécifiques au frontend ou l'orchestration de l'état (avec NgRx ou NGRX par exemple).
  • Couche Présentation (Composants) : Les composants Angular sont responsables de l'affichage de l'UI et de l'interaction utilisateur. Ils consomment les services pour récupérer et afficher les données, et évitent d'intégrer des logiques métier complexes.
  • Couche Infrastructure (API Clients / Adapters) : Des modules ou services spécifiques pour interagir avec des APIs tierces ou des services d'infrastructure (par exemple, des adapters pour des bibliothèques de stockage local).

// src/app/domain/models/product.model.ts
export interface Product {
  id: number;
  name: string;
  price: number;
}

// src/app/application/services/product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Product } from '../../domain/models/product.model';
import { environment } from '../../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private apiUrl = `${environment.backendUrl}/api/products`;

  constructor(private http: HttpClient) { }

  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.apiUrl);
  }

  getProductById(id: number): Observable<Product> {
    return this.http.get<Product>(`${this.apiUrl}/${id}`);
  }

  createProduct(product: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.apiUrl, product);
  }
}

// src/app/presentation/components/product-list/product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../../../application/services/product.service';
import { Product } from '../../../domain/models/product.model';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  products: Product[] = [];
  errorMessage: string | null = null;

  constructor(private productService: ProductService) { }

  ngOnInit(): void {
    this.productService.getProducts().subscribe({
      next: (data) => {
        this.products = data;
      },
      error: (error) => {
        this.errorMessage = 'Erreur lors du chargement des produits.';
        console.error('There was an error!', error);
      }
    });
  }
}
    

Point de vue : développeur full stack à Dakar

Pour un développeur travaillant sur des systèmes d'information complexes ou des applications de gestion des risques, comme celles rencontrées dans des projets de gestion hospitalière ou des systèmes ERP, la maîtrise de la Clean Architecture représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. Elle permet de garantir la robustesse et l'évolutivité des solutions logicielles.

Conclusion

La mise en œuvre de la Clean Architecture dans un projet Spring Boot Angular, comme démontré par l'approche d'un développeur Full Stack Java Spring Boot + Angular tel que Laty Gueye Samba à Dakar, est une démarche stratégique pour construire des applications résilientes, faciles à tester et à maintenir. Bien que l'investissement initial puisse sembler plus important, les bénéfices à long terme en termes de flexibilité et de réduction de la dette technique sont considérables.

En adoptant ces principes, les développeurs peuvent créer des systèmes qui non seulement répondent aux besoins actuels, mais sont également prêts à s'adapter aux défis futurs du développement logiciel. L'expertise dans l'application d'une architecture logicielle solide est ce qui distingue un Développeur Full Stack Dakar Sénégal capable de livrer des solutions de haute qualité.

Pour approfondir ce sujet, 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