VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Kiến trúc và chất lượng codeKiến trúc ứng dụng
Bài 15/17
5 phút

Chia tầng

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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
Controller của API OrderService Job nhập đơn chạy đêm ProductStore
Hai nơi gọi dùng chung một tầng nghiệp vụ

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.
  • OrderService không biết mình được gọi từ API hay từ job nào khác. Đánh dấu nó bằng @Service như 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.LayerTests

Xanh. Đơ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âu
Câu 1

Quy tắc "đơn từ 500.000 trở lên được miễn phí giao hàng" nên nằm ở tầng nào?

Câu 2

Controller nên làm những việc gì?

Câu 3

Vì sao đặt quy tắc đặt hàng trong OrderService lại giúp job nhập đơn chạy đêm?

ObserverClean Architecture

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt