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é!
Để 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:
Đâ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:
Để đơ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:
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.
Ở đâ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.
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.
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:
package com.huongdanjava.cleanarchitecture.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.huongdanjava.cleanarchitecture.db.StudentAdapterImpl; import com.huongdanjava.cleanarchitecture.usecases.adapter.StudentAdapter; import com.huongdanjava.cleanarchitecture.usecases.student.FindStudentByNameUseCase; @Configuration public class UseCaseConfiguration { @Bean public FindStudentByNameUseCase findStudentByNameUseCase(StudentAdapter studentAdapter) { return new FindStudentByNameUseCase(studentAdapter); } @Bean public StudentAdapter studentAdapter() { return new StudentAdapterImpl(); } }
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:
CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` bigint(2) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; INSERT INTO student SET name='Khanh', age=33;
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: