Giới thiệu về Clean Architecture – Phần 2

Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh

Trong bài viết trước, mình đã giới thiệu với các bạn những ý tưởng cơ bản của Clean Architecture. Trong bài viết này, mình sẽ đi vào chi tiết cách hiện thực Clean Architecture với một ứng dụng Java sẽ như thế nào, các bạn nhé!

  Làm thế nào để sắp xếp Clean Architecture theo Modular Patterns trong 10 phút?
  Viết code sạch (Clean code) được gì? Phần 1

Để các bạn dễ hiểu, mình sẽ lấy ví dụ ứng dụng quản lý sinh viên được đề cập trong phần 1 để viết theo Clean Architecture như sau:

Giới thiệu về Clean Architecture – Phần 2

Đây là Maven project với nhiều module các bạn nhé!

Module entities

Các bạn có thể thấy, chúng ta có module entities để định nghĩa thông tin sinh viên:

Giới thiệu về Clean Architecture – Phần 2

Để đơn giản, mình chỉ định nghĩa 2 thông tin cơ bản của sinh viên trong class Student như sau:

package com.huongdanjava.cleanarchitecture.entities;

public class Student {

private String name;
private int age;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

Và trong tập tin pom.xml của module này, mình không declare bất kỳ một library hay framework nào cả.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.huongdanjava</groupId>
<artifactId>clean-architecture-example</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>entities</artifactId>

</project>

Module use-cases

Module use-cases thì để đơn giản, mình chỉ định nghĩa một use case duy nhất là tìm kiếm thông tin sinh viên bằng tên:

Giới thiệu về Clean Architecture – Phần 2

package com.huongdanjava.cleanarchitecture.usecases.student;

import com.huongdanjava.cleanarchitecture.entities.Student;
import com.huongdanjava.cleanarchitecture.usecases.adapter.StudentAdapter;

public class FindStudentByNameUseCase {

private StudentAdapter adapter;

public FindStudentByNameUseCase(StudentAdapter adapter) {
this.adapter = adapter;
}

public Student find(String name) {
return adapter.findByName(name);
}

}

Ở đây, như các bạn thấy, mình có định nghĩa thêm một package là adapter. Trong idea của Clean Architecture thì lớp adapter sẽ nằm bên ngoài lớp use-cases nhưng ở đây, chúng ta có thể gộp lớp adapter này nằm trong module use-cases cũng được, không cần phải thêm một module adapter để định nghĩa các interface, không cần thiết lắm. Nhưng nếu các bạn muốn follow chặt chẽ idea của Clean Architecture thì có thể introduce thêm module adapter nữa cũng được.

StudentAdapter có nội dung như sau:

package com.huongdanjava.cleanarchitecture.usecases.adapter;

import com.huongdanjava.cleanarchitecture.entities.Student;

public interface StudentAdapter {

    Student findByName(String name);
}

Nội dung của tập tin pom.xml trong module use-cases, mình cũng không có một library, framework nào cả, ngoài dependency của module entities:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.huongdanjava</groupId>
<artifactId>clean-architecture-example</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>use-cases</artifactId>

<dependencies>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>entities</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

Module db

Chúng ta sẽ hiện thực phần lấy thông tin sinh viên trong module db.

Giới thiệu về Clean Architecture – Phần 2

Ở đây, mình sẽ sử dụng spring-data-jpa để làm nhiệm vụ thao tác với database nha các bạn!

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.huongdanjava</groupId>
<artifactId>clean-architecture-example</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>db</artifactId>

<dependencies>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>use-cases</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

</project>

Như các bạn thấy, mình cũng khai báo thêm Hibernate dependency cho phần implementation của JPA và thư viện Lombok để việc định nghĩa các entity đơn giản hơn!

StudentModel có nội dung như sau:

package com.huongdanjava.cleanarchitecture.db.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;

@Table(name = "student")
@Entity
@Data
public class StudentModel implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue
private Long id;

@Column
private String name;

@Column
private int age;
}

StudentRepository có nội dung như sau:

package com.huongdanjava.cleanarchitecture.db;

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

import com.huongdanjava.cleanarchitecture.db.model.StudentModel;

@Repository
public interface StudentRepository extends JpaRepository<StudentModel, Long> {

    StudentModel findByName(String name);
}

Như các bạn thấy ở đây, mình định nghĩa một query method cho phép chúng ta có thể lấy thông tin sinh viên từ tên của sinh viên đó.

Và bây giờ chúng ta có thể implement StudentAdapter trong module db như sau:

package com.huongdanjava.cleanarchitecture.db;

import org.springframework.beans.factory.annotation.Autowired;

import com.huongdanjava.cleanarchitecture.db.mapper.StudentMapper;
import com.huongdanjava.cleanarchitecture.db.model.StudentModel;
import com.huongdanjava.cleanarchitecture.entities.Student;
import com.huongdanjava.cleanarchitecture.usecases.adapter.StudentAdapter;

public class StudentAdapterImpl implements StudentAdapter {

@Autowired
private StudentRepository studentRepository;

@Override
public Student findByName(String name) {
StudentModel findByName = studentRepository.findByName(name);

return StudentMapper.toEntity(findByName);
}
}

Ở đây, như các bạn thấy, mình có thêm một class là StudentMapper để convert data từ database sang entity và sau đó, nếu entity này được sử dụng ở đâu đó, ví dụ như module rest, chúng ta sẽ có một class Mapper khác để convert từ entity sang dto của rest để trả về cho người dùng.

package com.huongdanjava.cleanarchitecture.db.mapper;

import com.huongdanjava.cleanarchitecture.db.model.StudentModel;
import com.huongdanjava.cleanarchitecture.entities.Student;

public class StudentMapper {

public static Student toEntity(StudentModel model) {
if (model == null) {
return null;
}

Student student = new Student();
student.setName(model.getName());
student.setAge(model.getAge());

return student;
}

Việc sử dụng class Mapper này sẽ giúp chúng ta giảm sự phụ thuộc giữa các module với nhau, chúng ta có thể dễ dàng thêm mới hoặc loại bỏ bớt module mà chúng ta sẽ dùng cho ứng dụng, với ít sự thay đổi code nhất.

Module rest

Sau khi đã lấy được data từ database, bây giờ là lúc chúng ta hiện thực module rest, đảm nhận vai trò expose API cho người dùng sử dụng.

Giới thiệu về Clean Architecture – Phần 2

Tập tin pom.xml của module rest có nội dung như sau:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.huongdanjava</groupId>
<artifactId>clean-architecture-example</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>rest</artifactId>

<dependencies>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>use-cases</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

Mình sử dụng spring-web dependency để định nghĩa các RESTful API, use-cases dependency để gọi tới các use cases của ứng dung và thư viện lombok chỉ để đơn giản cho việc định nghĩa các dto.

StudentDto có nội dung như sau:

package com.huongdanjava.cleanarchitecture.rest.dto;

import lombok.Data;

@Data
public class StudentDto {

    private String name;

    private int age;

}

StudentMapper có nội dung như sau:

package com.huongdanjava.cleanarchitecture.rest.mapper;

import com.huongdanjava.cleanarchitecture.entities.Student;
import com.huongdanjava.cleanarchitecture.rest.dto.StudentDto;

public class StudentMapper {

public static StudentDto toDto(Student entity) {
if (entity == null) {
return null;
}

        StudentDto studentDto = new StudentDto();
        studentDto.setName(entity.getName());
        studentDto.setAge(entity.getAge());

        return studentDto;
    }
}

Và class StudentController expose API lấy thông tin sinh viên bằng tên có nội dung như sau:

package com.huongdanjava.cleanarchitecture.rest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.huongdanjava.cleanarchitecture.entities.Student;
import com.huongdanjava.cleanarchitecture.rest.dto.StudentDto;
import com.huongdanjava.cleanarchitecture.rest.mapper.StudentMapper;
import com.huongdanjava.cleanarchitecture.usecases.student.FindStudentByNameUseCase;

@RestController
@RequestMapping("/student")
public class StudentController {

@Autowired
private FindStudentByNameUseCase findStudentByNameUseCase;

@GetMapping("/find")
public ResponseEntity<StudentDto> findByName(@RequestParam String name) {
Student student = findStudentByNameUseCase.find(name);

return new ResponseEntity<>(StudentMapper.toDto(student), HttpStatus.OK);
}

}

Ở đây, mình đang autowired FindStudentByNameUseCase là do mình đang tận dụng benifit của ứng dụng này với Spring framework, chúng ta sẽ định nghĩa các use cases trong Spring container. Nếu ứng dụng của các bạn sử dụng những framework khác thì việc sử dụng use cases sẽ phụ thuộc vào các framework đó.

Module configuration

Như mình nói, để ứng dụng có thể chạy được, chúng ta cần có module configuration.

Giới thiệu về Clean Architecture – Phần 2

Mình đang sử dụng Spring Boot để chạy ứng dụng:

package com.huongdanjava.cleanarchitecture;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

và định nghĩa use case FindStudentByNameUseCase trong class UseCaseConfiguration:

Nội dung tập tin pom.xml của module configuration sẽ như sau:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.huongdanjava</groupId>
<artifactId>clean-architecture-example</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>configuration</artifactId>

<dependencies>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>use-cases</artifactId>
</dependency>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>db</artifactId>
</dependency>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>rest</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Nếu các bạn để ý, mình đã định nghĩa các module rest và db generic nhất có thể, và việc cấu hình ứng dụng trong module configuration sẽ quyết định ứng dụng của chúng ta chạy như thế nào! Ví dụ ở đây, mình đang sử dụng MySQL để chạy ứng dụng, sau này nếu mình muốn chuyển sang một database system khác như PostgreSQL chẳng hạn, việc mình cần làm là chỉ cần thay đổi ở module configuration này mà thôi, …

Tập tin pom.xml của parent project có nội dung như sau:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<groupId>com.huongdanjava</groupId>
<artifactId>clean-architecture-example</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<modules>
<module>rest</module>
<module>use-cases</module>
<module>db</module>
<module>entities</module>
<module>configuration</module>
</modules>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>use-cases</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>db</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.huongdanjava</groupId>
<artifactId>rest</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.22.Final</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

Đến đây thì chúng ta đã hoàn thành ứng dụng ví dụ của mình.

Giả sử mình có table student và data được tạo trong database server MySQL như sau:

thì khi chạy ứng dụng và request tới http://localhost:9090/student/find?name=Khanh, các bạn sẽ thấy kết quả như sau:
Giới thiệu về Clean Architecture – Phần 2