Introduction à la Clean Architecture

Présenté par Maël Sicard-Cras

Introduction

Qui suis-je?

Qui êtes vous?

Vous & la Clean Architecture?

Les obstacles typiques rencontrés par les développeurs

Changement de Technologie

Internal breaking changes

Code peu clair

Code non testable

Alors parlons de ...

CLEAN ARCHITECTURE

Concept charactérisé par:

  • Bonnes pratiques regroupées
  • Indépendant du framework
  • Indépendant des technologies
  • Testabilité
  • Clean code

La théorie

Notre code


                        

                        
                            const fastify = require('fastify')({ logger: true })

                            fastify.get('/api/v1/malwares', async (request, reply) => {
                                return await orm.getRepository(Malware).find();
                            })
                            
                            fastify.get('/api/v1/malwares/:id', async (request, reply) => {
                                return await orm.getRepository(Malware).findOne(request.params.id);
                            })

                            const start = async () => {
                                await fastify.listen({ port: 3000 })
                            }
                            start()
                        
                    

Le Domain Driven Design

ddd

Le Domain Driven Design

                        
                        
                            const fastify = require('fastify')({ logger: true })

                            interface MalwareEntity {
                                actor: string;
                                malwareName: string;
                            }

                            async getAllMalwares(): Promise<MalwareEntity[]> {
                                return await orm.getRepository(Malware).find();
                            }
                            
                            async getMalwareById(id: number): Promise<MalwareEntity> {
                                return await orm.getRepository(Malware).findOne(id);
                            }

                            fastify.get('/api/v1/malwares', async (request, reply) => {
                                return await getAllMalwares()
                            })
                            
                            fastify.get('/api/v1/malwares/:id', async (request, reply) => {
                                return await getMalwareById(request.params.id)
                            })

                            const start = async () => {
                                await fastify.listen({ port: 3000 })
                            }
                            start()
                        
                    

L'architecture héxagonale

hex

L'architecture héxagonale

                        
                        
                            const fastify = require('fastify')({ logger: true })

                            // Domain
                            interface MalwareEntity {
                                actor: string;
                                malwareName: string;
                            }

                            // SPI (Application <---> Database)
                            async getAllMalwares(): Promise<MalwareEntity[]> {
                                return await orm.getRepository(Malware).find();
                            }
                            
                            async getMalwareById(id: number): Promise<MalwareEntity> {
                                return await orm.getRepository(Malware).findOne(id);
                            }

                            // API (Clients <---> Application)
                            fastify.get('/api/v1/malwares', async (request, reply) => {
                                return await getAllMalwares()
                            })
                            
                            fastify.get('/api/v1/malwares/:id', async (request, reply) => {
                                return await getMalwareById(request.params.id)
                            })

                            const start = async () => {
                                await fastify.listen({ port: 3000 })
                            }
                            start()
                        
                    

Les strates de la Clean Architecture

hex

Les strates de la Clean Architecture

                        
                            // 1 - EXTERNAL INTERFACES: src/infrastructure/app.ts
                            const fastify = require('fastify')({ logger: true })

                            const start = async () => {
                                await fastify.listen({ port: 3000 })
                            }
                            start()
                            
                            // 2 - ADAPTER (SPI): src/adapter/spi/db/repository.ts
                            async getAllMalwares(): Promise<MalwareEntity[]> {
                                return await orm.getRepository(Malware).findOne(id);
                            }
                            
                            async getMalwareById(id: number): Promise<MalwareEntity> {
                                return await orm.getRepository(Malware).find();
                            }

                            // 2 - ADAPTER (API): src/adapter/api/controller.ts
                            fastify.get('/api/v1/malwares', async (request, reply) => {
                                return await new GetAllMalwaresUseCase().execute()
                            })
                            
                            fastify.get('/api/v1/malwares/:id', async (request, reply) => {
                                return await new GetOneMalwareByIdUseCase(request.params.id).execute()
                            })

                            // 3 - USE CASES: src/application/usecases/getAllMalwaresUseCase.ts
                            class GetAllMalwaresUseCase{
                                async execute(): Promise<MalwareEntity[]> {
                                    return await repository.getAllMalwares();
                                }
                            }

                            // 3 - USE CASES: src/application/usecases/GetOneMalwareByIdUseCase.ts
                            class GetOneMalwareByIdUseCase{
                                async execute(): Promise<MalwareEntity> {
                                    return await repository.getMalwareById(id);
                                }
                            }
                            
                            // 4 - ENTITIES: src/domain/entities.ts
                            interface MalwareEntity {
                                actor: string;
                                malwareName: string;
                            }
                        
                    

Dependency Rule

ca

Dependency Rule incorrecte

                        
                            //current path: src/application/usecases/GetOneMalwareByIdUseCase.ts
                            
                            import {MalwaresMySqlRepository} from "src/adapter/spi/db/malwares_repository";
                            import {MalwareEntity} from "src/domain/entities/malware_entity";

                            export class GetAllMalwaresUseCase {
                                private repository: MalwaresMySqlRepository;
                            
                                constructor() {
                                    this.repository = new MalwaresMySqlRepository();
                                }
                            
                                async execute(): Promise<MalwareEntity[]> {
                                    return await this.repository.getAllMalwares();
                                }
                            }
                        
                    

Dependency Rule correcte

                        
                            //current path: src/application/usecases/GetOneMalwareByIdUseCase.ts
                            
                            import {MalwaresRepositoryInterface} from "src/application/repositories/malwares_repository_interface";
                            import {MalwareEntity} from "src/domain/entities/malware_entity";

                            export class GetAllMalwaresUseCase {
                                private repository: MalwaresRepositoryInterface;
                            
                                constructor(repository: MalwaresRepositoryInterface) {
                                    this.repository = repository;
                                }
                            
                                async execute(): Promise<MalwareEntity[]> {
                                    return await this.repository.getAllMalwares();
                                }
                            }
                        
                    

Demo

Cats & Dogs Facts API

  • API de faits divers sur les animaux
  • 2 UseCases par animal (tous ou certains faits)
  • Structure JSON différentes entre animaux
  • Données lues depuis la DB et API externe

Hexagonal Architecture

hex_api

Data flow

lifecycle2

VsCode

Cas pratique

Décision managériale!!!

  • Pas d'investissement dans le cloud...
  • On préfère maintenir une DB en local
  • On migre le service HTTP & les données
  • Nos clients ne doivent pas s'en rendre compte

PR

Video Typescript

Video Python

Conclusion

Résumé

  • Domain driven design
  • Hexagonal architecture
  • Dependency rule
  • Dependency injection

Pros (& cons)

  • Isolation des domaines & logiques métier
  • Simple et peu couteux de changer de technologie
  • Reste flexible & scalable sur des gros projets
  • Aisance de test

Notre code ?


                        
                            const fastify = require('fastify')({ logger: true })

                            fastify.get('/api/v1/malwares', async (request, reply) => {
                                return await orm.getRepository(Malware).find();
                            })
                            
                            fastify.get('/api/v1/malwares/:id', async (request, reply) => {
                                return await orm.getRepository(Malware).findOne(request.params.id);
                            })

                            const start = async () => {
                                await fastify.listen({ port: 3000 })
                            }
                            start()
                        
                    
“It is not enough for code to work...”
Robert C. Martin (Uncle Bob)’s Clean Code: A Handbook of Agile Software Craftsmanship

Merci

Github

https://github.com/MSC29


  • clean-architecture-typescript
  • clean-architecture-python
  • clean-architecture-rust
  • clean-architecture-go

Article en ligne

https://dev.to/msc29

Q&A