Chia tầng
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Quy tắc "hết hàng thì không cho đặt" đang nằm ở đâu? Nếu nó nằm lẫn trong controller của API, thì một job nhập đơn chạy mỗi đêm phải viết lại một bản, và hai bản sớm muộn sẽ lệch nhau. Chia tầng tách code theo vai trò, để quy tắc nghiệp vụ chỉ nằm ở một chỗ.
Khái niệm
🍰 Kiến trúc phân tầng (layered architecture): chia ứng dụng thành các tầng theo vai trò, mỗi tầng chỉ gọi xuống tầng ngay bên dưới.
| Tầng | Vai trò | Trong dự án cửa hàng |
|---|---|---|
| Giao diện (presentation) | nhận yêu cầu, trả kết quả | controller của API |
| Nghiệp vụ (business) | quy tắc của cửa hàng | OrderService, ShippingCalculator |
| Dữ liệu (data) | đọc ghi database | ProductStore, DbProductStore |
Ví dụ
Controller mỏng: nhận request, gọi tầng nghiệp vụ, trả kết quả. Quy tắc đặt
hàng nằm trong OrderService của bài Fake thay phụ thuộc:
// shop-api
// OrdersController.java
package com.shop.api;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
class OrdersController {
private final OrderService orders;
public OrdersController(OrderService orders) {
this.orders = orders;
}
@PostMapping
public ResponseEntity<String> place(
@RequestParam long productId,
@RequestParam int quantity) {
boolean ok = orders.place(productId, quantity);
if (!ok) {
return ResponseEntity.badRequest()
.body("Không đủ hàng");
}
return ResponseEntity.ok().build();
}
}- Controller không biết tồn kho được kiểm tra thế nào, cũng không biết có database. Nó chỉ đổi kết quả thành response HTTP: 400 hay 200.
OrderServicekhông biết mình được gọi từ API hay từ job nào khác. Đánh dấu nó bằng@Servicenhư bài Dependency injection trong Spring để controller nhận được qua constructor.- Job chạy đêm có thể gọi đúng
OrderService.place, nên hai nơi dùng cùng một quy tắc.
Thử ngay
Gọi tầng nghiệp vụ từ một test, không cần API. Thêm class này vào
src/test/java/com/shop/api/, dùng FakeProductStore của bài Fake thay phụ
thuộc, rồi chạy ./mvnw test:
// shop-api
// LayerTests.java
package com.shop.api;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
class LayerTests {
@Test
public void placeWithoutUi() {
var store = new FakeProductStore();
var pen = new Product();
pen.setId(1L);
pen.setName("Bút bi");
pen.setStock(120);
store.getProducts().add(pen);
var orders = new OrderService(store);
assertTrue(orders.place(1, 20));
assertFalse(orders.place(1, 500));
assertEquals(100, pen.getStock());
}
}Đoán trước khi chạy: test xanh hay đỏ?
Xem kết quả
[INFO] Running com.shop.api.LayerTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.059 s -- in com.shop.api.LayerTestsXanh. Đơn 20 cái được đặt, kho còn 100. Đơn 500 cái bị từ chối vì chỉ còn 100. Tầng nghiệp vụ chạy được mà không có giao diện nào, nên test được và dùng chung được.
Lỗi hay gặp
Để quy tắc nghiệp vụ và repository trong controller. Controller vừa đọc database, vừa kiểm tra tồn kho, vừa trả HTTP. Job chạy đêm không dùng lại được, còn test thì phải dựng cả API.
// SAI — shop-api: quy tắc nghiệp vụ trong controller
@PostMapping
@Transactional
public ResponseEntity<String> place(
@RequestParam long id, @RequestParam int qty) {
Optional<Product> found = products.findById(id);
if (found.isEmpty()
|| found.get().getStock() < qty) {
return ResponseEntity.badRequest()
.body("Không đủ hàng");
}
Product product = found.get();
product.setStock(product.getStock() - qty);
return ResponseEntity.ok().build();
}// ĐÚNG — shop-api: controller chỉ gọi service
// OrdersController.java
package com.shop.api;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
class OrdersController {
private final OrderService orders;
public OrdersController(OrderService orders) {
this.orders = orders;
}
@PostMapping
public ResponseEntity<String> place(
@RequestParam long id,
@RequestParam int qty) {
if (!orders.place(id, qty)) {
return ResponseEntity.badRequest()
.body("Không đủ hàng");
}
return ResponseEntity.ok().build();
}
}Tóm tắt
- Ba tầng: giao diện, nghiệp vụ, dữ liệu. Mỗi tầng chỉ gọi tầng dưới.
- Quy tắc nghiệp vụ nằm ở một chỗ, trong các service.
- Controller mỏng: nhận yêu cầu, gọi service, trả kết quả.
- Tầng nghiệp vụ không phụ thuộc giao diện, nên test và dùng lại được.
Tự kiểm tra
0/3 câuQuy tắc "đơn từ 500.000 trở lên được miễn phí giao hàng" nên nằm ở tầng nào?
Controller nên làm những việc gì?
Vì sao đặt quy tắc đặt hàng trong OrderService lại giúp job nhập đơn chạy đêm?