Clean architecture - Gả khổng lồ sinh sau đẻ muộn (phần 1)

Clean architecture - Gả khổng lồ sinh sau đẻ muộn (phần 1)

Xem nhanh

Một trong những architecture mình thích dùng nhất trong các project của mình từng làm. Clean architecture, hexagonal architecture, onion architecture, ... là những architecture có các concepts khá là tương tự nhau. Và mục đích cuối cùng của chúng là giúp cho chúng ta (developer) dễ dàng phát triển, maintain hơn. Đặc biệt là các project lớn (có business phức tạp).

Giới thiệu một qua một chút

Tác giả của Clean architecture là một ông chú rất nổi tiếng trong thế giới phần mềm – Robert C. Martin (Uncle Bob). Các bạn có thể tìm đọc khá nhiều sách hay ho của chú như: Clean Architecture, Clean Code, các principle như SOLID, …. Ông chú để lại rất nhiều di sản, trong đó có Clean architecture nổi tiếng.

Đây là bài blog của tác giả vào năm 2012 - The Clean Architecture. Ngoài ra các bạn có thể đọc thêm trong cuốn sách của tác giả.

Có 2 ý cực kì quan trọng mà tác giả đề cập đến về lợi ích mà clean architecture đem lại:

  • Independent - không phụ thuộc vào framework, libs, implement chi tiết của database, hay kể cả UI, mà hoàn toàn độc lập. Vậy cái gì ở đây độc lập? Chính là core business logics. Toàn bộ nghiệp vụ chương trình sẽ được tách biệt với thế giới bên ngoài - độc lập.
  • Testable - sau khi implement xong core business logics, chúng ta có thể thực hiện viết unit test để verify toàn bộ business logics (mà không cần phải phụ thuộc vào các layer bên ngoài như database, UI, lib, framework, ...). Unit test là một trong nhứ thứ quan trọng ngang hàng với production code. Nó sẽ giúp chúng ta maintain sau này rất dễ dàng. Nhưng mình thấy khá nhiều người trong các bạn ít chú tâm đến unit test.

Và có một điểm cực kì quan trọng khác mình hi vọng các bạn có thể nhìn ra. Clean architecture (và các architecture mình đề cập ở trên) đều cô lập (isolate) core business logic lại một chỗ. Có thể các bạn chưa cảm nhận được lợi ích của việc này. Nhưng mình đã từng trãi qua những project có business siêu phức tạp (và cũng có các thành phần legacy). Và điểm quan trọng hơn là, business của nó không cô lập (tập trung lại) một nơi. Mà chúng phân tán khắp nơi trong các layer. Dẫn tới…change business/fix bugs/maintain cực kì cực kì khó khăn và tốn cost. Và các architecture như clean sẽ giúp chúng ta giải quyết được hầu hết các vấn đề trên.

Để lại những dòng này và chờ tới cuối mình nhắc lại!

The clean architecture

Hình ở trên là cái hình huyền thoại mà những ai đang tìm hiểu Clean architecture chắc chắn biết.

Và mình hi vọng sau khi đọc hết bài viết này. Bạn sẽ hiểu được clean architecture.

Ok, chúng ta cùng đi vào chi tiết và implement demo.

So sánh layered architecture với clean architecture

Xem hình phát hiểu luôn nha các bạn.

Compare layered architecture với clean architecture

Nếu các bạn thường xuyên sử dụng các framework backend như Nestjs, Spring boot, Laravel, … Có thể các bạn không để ý nhưng các bạn hầu như sẽ code theo layered architecture (phân chia theo từng lớp). Các bạn sẽ hay code như thế này. Request đi vào controller xong xuống service, rồi tới repository kết hợp với entity (mapping luôn với ORM).

Các project nhỏ, chỉ cần CRUD bình thường thì chúng ta chỉ cần layered architecture là quá đủ rồi. Nhưng nếu project của bạn cần linh hoạt hơn, khả năng mở rộng cao hơn. Ví dụ business logic ngày càng phức tạp, lại dễ thay đổi theo nhu cầu khách hàng. Hoặc bạn định hướng thời gian sống của project lên đến hàng chục năm, có thể sẽ phải thay đổi công nghệ, framework nhiều, hoặc đổi luôn database. Hoặc use case khác là tích hợp với các external services khác, … Thì lúc này dường như các architecture như Clean mới đáp ứng được.

Và mình nhắc lại một chút. Clean architecture (hình vẽ trên) cô lập core business logics lại và không phụ thuộc vào các thành phần bên ngoài như UI hay database. Nên nó cực kì linh hoạt và dễ mở rộng.

Chi tiết hơn vào các thành phần của Clean architecture

Entities

Trong cái hình đầu tiên của tác giả, nó chính là vòng tròn nhỏ nhất màu vàng – Entities. Mình hay gọi là domain layer, và nó thuộc về core business logics (khung vuông màu vàng ở hình vẻ thứ 2 của mình). Nếu các bạn chưa từng implement gì liên quan tới các architecture dạng này thì sẽ không hiểu nó là gì. Người ta cứ nói nó là nơi chứa các enterprise business logics nhưng bản chất thực sự nó là gì.

Cơ bản nó là các object (model) chứa các business logic trong đó thôi. Trong Clean architecture thì một entity nó là một object hoặc một cụm object. Ví dụ trong use case tạo một user. Thì entity ở đây là object `User.java` và các business logics của nó. Tí mình lấy ví dụ cho tường minh.

Nếu mapping qua Domain-driven design, entities có thể hiểu là bao gồm các aggregate, entity, value object trong DDD.

Use case

Use case chính là vòng tròn màu đỏ thứ 2 trong cái hình ban đầu. Mình hay gọi là application layer. Và layer này chứa application business logics. Và nó cũng thuộc về core business logics. Những logic ở đây bao gồm flow của chương trình, tương tác với các entities (layer trong) như load entities, save entities, …. Bạn có thể hiểu nó giống như một orchestrator để điều phối flow của một request vậy. Tí nữa xem ví dụ của mình để hiểu chi tiết hơn nha.

Entities (domain layer) và use case (application layer) chính là 2 thành phần quan trọng nhất và được cô lập lại một nơi gọi là core business logics. Và vẫn là nhắc lại, nó không phụ thuộc vào các thành phần bên ngoài khác như framework, UI, database, ….

Interface Adapters

Mình hay gọi đây là Presentation layer – cũng chính là vòng tròn màu xanh lá ở hình gốc. Theo như tác giả có đề cập, layer này sẽ chứa các adapter để convert data từ bên ngoài (Web, database) vào bên trong (application, domain) và ngược lại. Ví dụ trong use case tạo User, các object như CreateUserCommand object hay UserDto có thể được nằm ở layer này. Và những logic convert data thô từ request vào các object này sẽ nằm ở đây.

Tương tự như thế cho response, các object response và adapter convert data sẽ nằm ở đây. Ngoài ra controller, view, presenter cũng được nằm ở đây.

Frameworks and Drivers

Mình hay gọi đây là infrastructure layer – vòng tròn ngoài cùng ở hình gốc. Đây là nơi chứa các detail implement của database, các service bên ngoài hay các driver, framework.

Các bạn lưu ý ở use case, chúng ta chỉ thao tác với các interface của database thông qua repository pattern thôi, hoặc muốn giao tiếp với external service cũng phải thông qua interface. Ở layer đó hoàn toàn không thấy được implement chi tiết của chúng. Và những implement chi tiết đó sẽ nằm ở infrastructure layer này.

Giải thích sơ sơ rồi mình cũng đi vào ví dụ chi tiết.

Dependency rule

Dependency rule (quy tắc phụ thuộc) là một khía cạnh quan trọng trong kiến trúc phần mềm Clean Architecture do Robert C. Martin (Uncle Bob) đề xuất. Nó đóng vai trò then chốt trong việc thiết kế và xây dựng các hệ thống phần mềm linh hoạt, dễ bảo trì và mở rộng.

Bạn hãy nhìn cái hình đầu tiền thấy mấy cái mũi tên hướng vào trong không. Đây chính là chiều của dependency.

Dependency rule hướng dẫn cách thức các thành phần trong Clean Architecture tương tác và phụ thuộc lẫn nhau.

Các thành phần không được phép phụ thuộc trực tiếp vào các thành phần ở lớp bên ngoài.

Thay vào đó, sự tương tác diễn ra thông qua các abstraction (sự trừu tượng) và dependency inversion (đảo ngược phụ thuộc). Ví dụ đơn giản, trong các usecase, không được import các dependency ở ngoài như database (thuộc layer ngoài cùng).

Copy
// Ví dụ ở đây là một file Use case.
// Các implementation của các repositories trong này thuộc về infra layer.
// Nhưng ở đây nếu import trực tiếp implementation chi tiết của các repo 
// thì sẽ vi phạm dependency rule.
// Cho nên ở đây UserRepository phải là một interface 
// (abstraction với layer bên ngoài) mới thỏa mãn dependency rule
public class CreateUserUseCaseImpl implements CreateUserUseCase {
    private UserRepository userRepository;
    private RoleRepository roleRepository;
    private UserDomainService userDomainService;
    private UserEventPublisher publisher;
  // ...

Lưu ý:

  • Dependency rule không cấm hoàn toàn sự phụ thuộc giữa các thành phần.
  • Mục tiêu là giảm thiểu phụ thuộc trực tiếp và khuyến khích sử dụng abstraction.

Đi vào use case thực tế – design và implement

Các bạn nếu đọc nhiều các bài blog của mình, thì cũng biết mình không thích chém gió xuông. Đã viết bài thì viết cực kì chi tiết, có design, implement hẳn hoi.

Use case trong bài này sẽ là: mình có một trang báo điện tử, use case sẽ là tạo một author user. author user chính là người có thể tạo và quản lý bài post của họ. Sau khi tạo user xong, sẽ có một event UserCreatedEvent bắn và sync user qua một Redis server khác. Event này sẽ được bắn lên Kafka cluster. Các bạn nhớ flow này nha. Để tí xem hinh hay xem code nó dễ hiểu hơn.

Folder structure và giải thích một chút về flow của use case

Dưới đây là folder structure mình có design theo từng layer mình đã giải thích ở trên.

  • domain folder chính là domain layer, application chính là application layer. Hai ông này chính là core của software (core business logics).
  • controller folder thuộc về presentation layer.
  • infra folder thuộc về infrastructure layer.
  • Còn dto folder thì theo bạn thuộc về layer nào?

Về nơi đặt dto, mình recommend cho các bạn 2 cách đặt.

  • Cách một là đặt như mình ở bên dưới, gom hết vào folder dto.
  • Cách hai mình thấy mọi người cũng hay xài. dto phục vụ cho layer nào thì đặt tại layer đó. Ví dụ tại controller, sẽ có PostUserRequest.java, PostUserResponse.java. Ở trong use case sẽ có CreateUserUseCaseInput.java, CreateUserUseCaseOutput.java chẳng hạn.
application/
├── eventpublisher/
├── exception/
├── service/
├── repository/
│   ├── UserRepository.java
│   └── ...
└── usecase/
domain/
├── exception/
│   ├── UserNotFoundExeption.java
│   └── ...
├── valueobject/
│   ├── UserName.java
│   └── ...
├── entity/
│   ├── User.java
│   └── ...
└── service/
dto/
infra/
├── persitence/
│   ├── UserRepositoryImpl.java
│   └── ...
└── messaging/
controller/
├── UserController.java
└── ...

Trước khi đi vào code thì chúng ta cùng xem một cái hình nha. Sau bài viết này, mình mong muốn bạn hiểu hết chi tiết của cái hình là quá ok rồi.

create user usecase

Ở hình trên, ngay domain layer, mình dùng luôn các concept của Domain Driven Design nha. Dành cho bạn nào chưa biết thì ghé bài này của mình.

Bản chất các entities trong Clean architecture nó được mapping qua DDD y chang hình vẽ thôi.

Bây giờ mình sẽ giải thích flow của request một chút:

  • Request đi tới controller UserController - interface adapters layer hay presentation layer. Ngay ở layer này dữ liệu sẽ được transform sang dạng thích hợp nhất với các layer ở trong - domain layer và application layer. Ở đây mình dùng UserDto để chứa dữ liệu từ request nha.

  • Request đi tiếp vào application layer thông qua use case CreateUserUseCase interface. Ở đây use case chịu các trách nhiệm sau:

    • Điều phối flow của chương trình - business flow như thao tác với UserRepository để kiểm tra xem email có tồn tại chưa. Thao tác với RoleRepository để kiểm tra role có tồn tại hay không.
    • Sau đó sẽ tương tác với domain layer để tạo User entity (hay User aggregate).
    • Sau đó sẽ dùng Repository để save User xuống database và bắn event lên Kafka.
  • Khi use case thao tác với domain layer thì các business logics của use case này sẽ được đảm bảo trong domain layer (trong các model và service).

  • Và khi thao tác với các thành phần như repository, event publisher (những thành phần bên ngoài) thì use case chỉ thao tác với interface (không bao giờ use case nhìn thấy được implement chi tiết của infra). Đây chính là cái hay của Clean architecture. Và đây cũng chính là nguyên lý Dependency Inversion Principle trong SOLID.

  • Và cuối cùng các implement chi tiết của database hay event publisher sẽ nằm ở infrastructure layer.

Tới đây bài viết cũng dài rồi, mình sẽ tạm dừng phần một ở đây. Trong phần hai mình sẽ tiến hành code thực tế. Các bạn hãy đón xem nha.

Các bài viết cùng chủ đề