Retour aux articles

Appliquer la Clean Architecture à une application Full Stack Spring Boot et Angular

Appliquer la Clean Architecture à une application Full Stack Spring Boot et Angular | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular

Appliquer la Clean Architecture à une application Full Stack Spring Boot et Angular

Dans le monde du développement logiciel, la complexité des applications ne cesse de croître, exigeant des architectures robustes, maintenables et évolutives. Pour les développeurs Full Stack, et en particulier pour un expert en Java Spring Boot et Angular comme Laty Gueye Samba, basé à Dakar, l'adoption de principes architecturaux solides est primordiale. Cet article explore comment la Clean Architecture, popularisée par Robert C. Martin (Uncle Bob), peut être appliquée efficacement à une application Full Stack combinant un backend Spring Boot et un frontend Angular, offrant une feuille de route pour construire des systèmes résilients.

La Clean Architecture propose une approche agnostique aux frameworks, bases de données et interfaces utilisateur, en se concentrant sur la séparation des préoccupations pour isoler la logique métier du reste de l'application. Cette approche garantit une meilleure testabilité, une plus grande flexibilité face aux changements technologiques et une maintenance simplifiée. Pour les applications métier complexes, souvent rencontrées dans des projets de gestion hospitalière ou des applications de gestion des risques, cette discipline architecturale est un atout majeur.

Principes fondamentaux de la Clean Architecture et leur application Full Stack

La Clean Architecture est basée sur le principe des cercles concentriques, où chaque cercle représente un niveau d'abstraction. La règle de dépendance fondamentale stipule que les dépendances ne peuvent aller que de l'extérieur vers l'intérieur. Cela signifie que les cercles intérieurs ne doivent avoir aucune connaissance des cercles extérieurs. Les couches typiques sont :

  • Entities (Entités) : Contiennent les règles métier de l'entreprise. Ce sont des objets métier purs, indépendants de toute application spécifique.
  • Use Cases (Cas d'Utilisation) : Encapsulent les règles métier spécifiques à l'application. Ils orchestrent le flux de données vers et depuis les entités et dirigent celles-ci pour accomplir les objectifs du cas d'utilisation.
  • Interface Adapters (Adaptateurs d'Interface) : Convertissent les données des formats les plus internes aux formats les plus externes, et vice-versa. Cela inclut les contrôleurs, les passerelles de bases de données et les présentateurs.
  • Frameworks & Drivers (Frameworks et Pilotes) : Représentent la couche la plus externe, contenant les détails d'implémentation comme les frameworks web (Spring Boot, Angular), les bases de données (JPA), et les interfaces utilisateur.

Pour une application Spring Boot et Angular, ces couches se manifestent des deux côtés. Côté backend, Spring Boot gère l'infrastructure, tandis que la logique métier réside dans les cas d'utilisation et les entités. Côté frontend, Angular prend en charge la présentation et les adaptateurs, avec la logique métier client isolée dans des services dédiés.

Implémentation côté Backend avec Spring Boot

L'application de la Clean Architecture à un backend Spring Boot implique une structuration minutieuse des packages et des modules. Un développeur Full Stack expert comme Laty Gueye Samba sait qu'il est crucial de maintenir une séparation claire pour garantir la maintenabilité, essentielle pour des projets à long terme.

Voici une structuration typique :

  • src/main/java/com/laty/project/domain : Contient les Entités (POJOs purs) et les règles métier essentielles. Pas de dépendances Spring ici.
  • src/main/java/com/laty/project/application : Les Use Cases (services d'application) et les interfaces des ports (par exemple, UserRepositoryPort) que les couches externes devront implémenter.
  • src/main/java/com/laty/project/infrastructure : Implémentations concrètes des ports (par exemple, JpaUserRepositoryAdapter utilisant Spring Data JPA), contrôleurs REST (UserController) qui transforment les DTOs en objets de domaine pour les cas d'utilisation.
// Exemple : Entité (couche Domain)
package com.laty.project.domain;

public class User {
    private Long id;
    private String username;
    private String email;

    // Getters et Setters
    public User(Long id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }
    // ...
}

// Exemple : Port de sortie (couche Application)
package com.laty.project.application.port.out;

import com.laty.project.domain.User;
import java.util.Optional;

public interface UserRepositoryPort {
    User save(User user);
    Optional<User> findById(Long id);
}

// Exemple : Cas d'utilisation (couche Application)
package com.laty.project.application.port.in;

import com.laty.project.application.port.out.UserRepositoryPort;
import com.laty.project.domain.User;

public class GetUserService { // Use Case
    private final UserRepositoryPort userRepositoryPort;

    public GetUserService(UserRepositoryPort userRepositoryPort) {
        this.userRepositoryPort = userRepositoryPort;
    }

    public Optional<User> getUserById(Long id) {
        return userRepositoryPort.findById(id);
    }
}

// Exemple : Adaptateur de persistance (couche Infrastructure)
package com.laty.project.infrastructure.adapter.out.persistence;

import com.laty.project.application.port.out.UserRepositoryPort;
import com.laty.project.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

interface SpringDataJpaUserRepository extends JpaRepository<User, Long> {
}

@Repository
public class JpaUserRepositoryAdapter implements UserRepositoryPort {
    private final SpringDataJpaUserRepository jpaRepository;

    public JpaUserRepositoryAdapter(SpringDataJpaUserRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public User save(User user) {
        return jpaRepository.save(user);
    }

    @Override
    public Optional<User> findById(Long id) {
        return jpaRepository.findById(id);
    }
}

// Exemple : Contrôleur REST (couche Infrastructure/Presentation)
package com.laty.project.infrastructure.adapter.in.web;

import com.laty.project.application.port.in.GetUserService;
import com.laty.project.domain.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    private final GetUserService getUserService;

    public UserController(GetUserService getUserService) {
        this.getUserService = getUserService;
    }

    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return getUserService.getUserById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
}

Implémentation côté Frontend avec Angular

L'application de la Clean Architecture à Angular vise à séparer la logique métier du code de l'interface utilisateur et des appels d'API. Cela permet une meilleure testabilité et une facilité de refactoring des composants UI sans impacter le cœur de l'application.

Une structure de dossiers recommandée pourrait être :

  • src/app/core/domain : Contient les interfaces TypeScript ou les classes des modèles de données (par exemple, User.ts). Ces modèles sont agnostiques à la manière dont les données sont obtenues ou présentées.
  • src/app/core/application : Les services métier qui gèrent les cas d'utilisation spécifiques à l'application. Ils orchestreront les appels aux services d'API et manipuleront les modèles de domaine.
  • src/app/core/infrastructure : Les services responsables des interactions avec les API externes (par exemple, UserApiService).
  • src/app/features : Les modules de fonctionnalités, chacun contenant ses propres composants (user-list.component.ts), services de présentation et éventuellement des services d'application locaux.
// Exemple : Modèle de domaine (couche Domain)
// src/app/core/domain/user.model.ts
export interface User {
  id: number;
  username: string;
  email: string;
}

// Exemple : Service d'API (couche Infrastructure)
// src/app/core/infrastructure/user-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../domain/user.model';

@Injectable({
  providedIn: 'root'
})
export class UserApiService {
  private apiUrl = 'http://localhost:8080/users';

  constructor(private http: HttpClient) { }

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

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

// Exemple : Service d'application (couche Application)
// src/app/core/application/user.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../domain/user.model';
import { UserApiService } from '../infrastructure/user-api.service';

@Injectable({
  providedIn: 'root'
})
export class UserService { // This is our Use Case / Application Service
  constructor(private userApiService: UserApiService) { }

  getAllUsers(): Observable<User[]> {
    // Ici, on pourrait ajouter de la logique métier spécifique au client
    // avant de retourner les données aux composants.
    return this.userApiService.getUsers();
  }

  getUserDetails(id: number): Observable<User> {
    return this.userApiService.getUserById(id);
  }
}

// Exemple : Composant de présentation (couche Presentation)
// src/app/features/user/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from '../../../core/application/user.service';
import { User } from '../../../core/domain/user.model';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {
  users: User[] = [];

  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.userService.getAllUsers().subscribe(users => {
      this.users = users;
    });
  }
}

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 des plateformes de e-commerce, la maîtrise de l'architecture logicielle propre et modulable représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. Laty Gueye Samba, Développeur Full Stack à Dakar, reconnaît l'importance de ces pratiques pour bâtir des solutions robustes et durables dans le contexte local.

Conclusion

L'application de la Clean Architecture à une application Full Stack Spring Boot et Angular, comme le pratique Laty Gueye Samba, Développeur Full Stack expert Java Spring Boot Angular basé à Dakar, offre des avantages considérables. Elle garantit que la logique métier essentielle reste isolée, testable et indépendante des détails techniques changeants. Cette séparation des préoccupations rend les applications plus maintenables, plus testables et plus résilientes face aux évolutions technologiques.

Bien que l'adoption de la Clean Architecture puisse sembler complexe au début, ses bénéfices à long terme en termes de qualité logicielle, de facilité d'évolution et de robustesse en font un investissement judicieux pour tout projet d'envergure. Pour aller plus loin dans l'exploration de ces concepts, 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