Entity, domain model và DTO – sao nhiều quá vậy?

Nội dung bài viết ngày hôm nay khá hay và cũng là chủ đề quan trọng trong Spring Boot. Rõ ràng mọi người cùng tìm hiểu xem data sẽ đổi khác ra làm sao khi đi qua những layer không giống nhau. Và những khái niệm Entity, Domain model và DTO là gì nhé.

1. Kiến trúc tổng quan Spring Boot

1.1. Kiến trúc source code và kiến trúc tài liệu

Trong những phần trước, mọi người đã biết được mọi ứng dụng Spring Boot đều tuân theo 2 quy mô cơ phiên bản:

  • Quy mô MVC
  • Quy mô 3 lớp (3 tier)

Và do đó, mọi người phối kết hợp lại được ứng dụng hoàn hảo có cấu trúc như sau.

😍

Sơ đồ trên dùng làm tổ chức source code trong lớp học. Nhờ đó mọi người tạo thành những Controller, Service, Repository tương ứng với những layer. Tuy nhiên, nếu xét về mặt tổ chức data, thì sơ đồ sẽ trở thành như sau.

Quy mô này cũng gồm có 3 lớp, trong đó tên những layer được đổi thành những thành phần tương ứng trong Spring Boot.

Từ đó, tương ứng với từng layer thì data sẽ có được dạng không giống nhau. Nói cách khác, mỗi layer nên làm xử lý một số trong những loại data nhất định. Mỗi dạng data sẽ có được nhiệm vụ, mục tiêu không giống nhau. Tất nhiên trong code cũng khá được chia ra tương ứng.

Ví dụ trong sơ đồ thì Controller không nên đụng tới data dạng domain model hoặc entity, chỉ được phép nhận và trả về DTO.

1.2. Vì sao phải chia nhiều dạng data

Do tuân theo nguyên tắc SoC – separation of concerns – chia tách những mối quan tâm trong thiết kế ứng dụng. Rõ ràng, mọi người đã chia nhỏ ứng dụng Spring Boot ra như sau.

Spring Boot = Presentation layer + Service layer + Data access layer

Đó là việc chia nhỏ source code theo SoC. Tuy nhiên, ở tầm mức thấp hơn thì SoC trổ tài qua nguyên tắc trước tiên của SOLID (Single responsibility – nguyên tắc đơn nhiệm), tức thị mỗi class nên làm tiến hành một nhiệm vụ duy nhất.

Do đó, trước đó data chỉ có một dạng, nhưng có nhiều layer, mỗi layer hành xử không giống nhau với data nên data đã tiến hành nhiều nhiệm vụ. Điều này vi phạm vào Single responsibility, nên mọi người cần chia nhỏ thành nhiều dạng data.

Một nguyên nhân nữa là nếu data chỉ có một dạng thì sẽ bị leak (lộ) những tài liệu nhạy cảm. Lấy ví dụ tính năng tìm kiếm bè bạn của Facebook, đúng ra chỉ trên trả về data chỉ có những info cơ phiên bản (avatar, tên,…). Nếu chỉ có một dạng data thì toàn bộ thông tin sẽ tiến hành trả về. Tuy vậy client chỉ hiển thị những info quan trọng, nhưng việc trả về toàn bộ thì kẻ xấu rất có thể tận dụng để chôm những info nhạy cảm.

Vì thế, phân tích data thành những dạng riêng lẻ cũng là một phương pháp để tăng cường bảo mật thông tin cho ứng dụng.

2. Những dạng data

2.1. Hai loại data

Theo sơ đồ trên, data trong ứng dụng Spring Boot tạo thành 2 loại chính:

  • Public: tức thị để trao đổi, share với bên phía ngoài qua REST API hoặc tiếp xúc với những service khác trong microservice. Data lúc này ở dạng DTO.
  • Private: những data dùng trong nội bộ ứng dụng, bên phía ngoài không nên biết. Data lúc này nằm trong những Domain model hoặc Entity.

Những dạng data rất có thể có nhiều tên thường gọi không giống nhau, nhưng chung quy lại vẫn thuộc 2 phần như trên. Do đó, khi ứng dụng vào kiến trúc Spring Boot thì mọi người sẽ quan tâm đến xem loại data nào phù phù hợp với layer nào (phần 2.2).

Từ 2 loại public và private trên, mọi người có 3 dạng data:

  • DTO (Data transfer object): là những class đóng gói data để chuyển giữa client – server hoặc giữa những service trong microservice. Mục tiêu tiết ra DTO là để giảm sút lượng info không quan trọng phải chuyển đi, và cũng tăng mức độ bảo mật thông tin.
  • Domain model: là những class đại diện thay mặt cho những domain, hiểu là những đối tượng người tiêu dùng thuộc business như Client, Report, Department,… chẳng hạn. Trong ứng dụng thực, những class đại diện thay mặt cho thành quả tính toán, những class làm thông số nguồn vào cho service tính toán,… được xem là domain model.
  • Entity: cũng là domain model nhưng tương ứng với table trong DB, rất có thể map vào DB được. Lưu ý chỉ có entity mới rất có thể đại diện thay mặt cho data trong DB.

Những dạng data có hậu tố tương ứng, trừ entity. Ví dụ entity User không hề có hậu tố, nếu là domain model thìa là UserModel, hoặc với DTO thìa là UserDto,… cũng vậy.

2.2. Nguyên tắc chọn data tương ứng với layer

Well tôi cũng không biết gọi nó ra làm sao nữa. Nói vậy là, từng layer trong quy mô 3 lớp sẽ tiến hành xử lý, nhận, trả về tài liệu thuộc những loại xác định.

Vận dụng vào quy mô 3 lớp trong sơ đồ, thì mọi người rút ra được nguyên tắc thiết kế chung:

  • Web layer: nên làm xử lý DTO, đồng nghĩa với việc những Controller nên làm nhận và trả về tài liệu là DTO.
  • Service layer: nhận vào DTO (từ controller gửi qua) hoặc Domain model (từ những service nội bộ khác). Tài liệu được xử lý (rất có thể tương tác với DB), sau cuối được Service trả về Web layer dưới dạng DTO.
  • Repository layer: chỉ thao tác trên Entity, vì đó là đối tượng người tiêu dùng thích hợp, rất có thể mapping vào DB.

So với những thành phần khác của Spring Boot mà không thuộc layer nào, thì:

  • Custom Repository: đấy là layer không trải qua repository mà thao tác trực tiếp với database. Do đó, lớp này được hành xử như Service.

2.3. Model mapping

Khi data đi qua những layer không giống nhau, nó đổi khác thành những dạng không giống nhau. Ví dụ DTO từ controller đi vào service, thì nó sẽ tiến hành map thành domain model hoặc entity, rồi khi vào Repository cần được trở thành Entity. Và trái lại cũng đúng.

Việc convert giữa những dạng data, ví dụ DTO thành Entity, DTO thành domain model, domain model thành entity hoặc trái lại, được gọi là model mapping.

Triển khai model mapping thường là dùng thư viện như ModelMapper (cách dùng sẽ có được trong bài tiếp theo). Tuy nhiên, giản dị và đơn giản nhất thì rất có thể viết code copy thuần như sau.

@Getter public class UserDto { String name; String age; public void loadFromEntity(User entity) { this.name = entity.getName(); this.age = entity.getAge(); } } @Getter public class User { String name; String age; String crush; public void loadFromDto(UserDto dto) { this.name = dto.getName(); this.age = dto.getAge(); } }

Code như trên khi tận dụng sẽ trông như này.

// Trong controller, convert từ DTO > entity User user = new User(); user.loadFromDto(userDto); // Hoặc trả về cũng tương tự, từ Entity > DTO User user = userService.getUser(username); userDto userDto = new UserDto(); userDto.loadFromEntity(user); return userDto;

Cách khác giản dị và đơn giản hơn là thay vì viết method copy thì copy trong constructor luôn luôn. Do đó, code convert sẽ ngắn lại.

User user = new User(userDto); // DTO > entity UserDto userDto = new UserDto(user); // Entity > DTO

3. Thực tiễn ra làm sao?

Khi ứng dụng vào thực tiễn, có muôn hình vạn trạng trường hợp xẩy ra. Không chỉ là thuần tuý theo mẫu sau.

Controller nhận DTO > Service chuyển DTO thành model hoặc entity, rồi xử lý > Repository nhận Entity đưa vào DB

Repository lấy từ DB ra Entity > Service xử lý sao đó rồi thành DTO > Controller và trả về DTO

Mà còn tồn tại những trường hợp khác ví như:

  • Controller không sở hữu và nhận DTO mà nhận vào thông số primitive như int, float,…
  • Nhận vào một trong những List DTO
  • Trả về một List DTO

Do đó, trong thực tiễn người ta rất có thể thay đổi cho phù phù hợp với dự án.

Ví dụ chuẩn chỉnh là Service sẽ tiến hành mapping sang DTO và trái lại, controller chỉ nhận DTO. Nhưng đôi lúc để giảm tải cho service thì việc mapping này sẽ do controller đảm nhiệm (tuy vậy controller rất có thể bị phình to, trong lúc đúng ra phải giữ controller mỏng dính – ít code nhất rất có thể).

Nhưng dù cách nào đi nữa, thì nguyên tắc chung là việc mapping luôn luôn được tiến hành ở rìa của code (edge). Tức thị nếu mapping trong service thì việc chuyển đổi phải luôn luôn nằm ở đầu, hoặc ở sau cuối method khi chúng được xử lý.

Ngoài ra, để giảm boilerplate code, mọi người thường giảm sự ngặt nghèo xuống một chút ít nếu không quan trọng. Ví dụ như:

  • Đôi lúc không cần domain model, Service rất có thể chuyển thẳng DTO thành entity.
  • Service cũng rất có thể trả về Entity hoặc Model, nếu chúng quá giản dị và đơn giản và không chứa info nhạy cảm. Lúc này sẽ không cần DTO mà controller trả về Entity hoặc Model luôn luôn để đỡ rối (tuy vậy vì phạm nguyên tắc khi public hai thằng này, nhưng cũng nên quan tâm đến).

Có nhiều ý kiến tranh cãi về việc tận dụng DTO, coi đó như thể một anti pattern. Cá thể mình không thấy vậy, nhiều lúc DTO vẫn khá hữu dụng, và rất có thể tùy biến để thích hợp và hiệu suất cao hơn.

Nội dung bài viết đến này cũng dài rồi. Nói thật đấy là bài mình tốn công nhất, do phải đụng tới không hề ít về architecture. Mới hôm qua mình còn lôi cả project cũ ra để refactor lại cho đúng chuẩn chỉnh, để nắm rõ hơn về kiến trúc mình sắp trình diễn và những side effect rất có thể xẩy ra.

Nội dung bài viết có tìm hiểu thêm ở nguồn https://www.petrikainulainen.net/software-development/design/understanding-spring-web-application-architecture-the-classic-way/ mà mình cảm thấy hay nhất. Trong link trên còn tồn tại phần kết và comment, những chúng ta cũng có thể hướng dẫn thêm.

À quên nếu người cảm thấy nội dung bài viết hay và hữu ích, hãy upvote và clip để tiếp thêm động lực cho mình nhé. Bye bye

You May Also Like

About the Author: v1000