logoMột hệ thống khó hiểu thì cũng khó thay đổi
Offline Concurrency Control - Phần 4: Triển khai Optimistic trong Hexagonal

Series bài viết Offline Concurrency Control:

Part 1: Offline Concurrency Control - Phần 1: Hiện tượng

Part 2: Offline Concurrency Control - Phần 2: Optimistic - "Lời xin lỗi"

Part 3: Offline Concurrency Control - Phần 3: Optimistic và Inconsistent Read

Part 4: (Bài viết này) Offline Concurrency Control - Phần 4: Optimistic trong Hexagonal

***

Ở phần 2, chúng ta đã có demo triển khai Optimistic trong kiến trúc Layered với bài toán cập nhật thông tin product. Do đặc trưng của Layered, code kiểm soát Optimistic được đặt trong service layer có thể khiến cho bạn có thắc mắc rằng “nếu tôi chỉ quan tâm tới logic của bài toán thì tại sao code phần này lại phải để ý tới vấn đề Concurrency – là một vấn đề mang tính kỹ thuật?” Theo quan điểm của tôi, việc coi Concurrency là vấn đề ngoài nghiệp vụ là thường gặp nhưng không phải luôn luôn vậy. Nếu trong một nghiệp vụ có khả năng xung đột dữ liệu cao, ta nên coi đó là vấn đề cần lưu tâm trong logic để khi đó, lõi nghiệp vụ xây dựng được được sử dụng tại nhiều nơi khác nhau và xử lý đó được đóng gói dùng lại cao. Ngoài ra, khi phát triển kiến trúc module/component, có những module mang tính kỹ thuật như vậy và nó hoàn toàn được coi là logic business của nó, vậy ta vẫn cần đưa xử lý Concurrency vào bên trong. Bởi nếu như ta chỉ coi Concurrency là vấn đề kỹ thuật hoàn toàn thì có thể ta sẽ không tối ưu cho việc dùng lại.

Trong phần này, chúng ta sẽ triển khai cách xử lý Optimistic trong kiến trúc Hexagonal. Với ví dụ đã gặp ở phần trước, giả sử rằng, ta coi xử lý Optimistic nằm ngoài nghiệp vụ, tức là vấn đề này là thuần túy kỹ thuật.

Triển khai với Hexagonal

Bài viết này là một cách triển khai Optimistic cơ bản trong Hexagonal. Cách triển khai khác sẽ được cập nhật trong phần sau.

Phần này sẽ không nhắc lại chi tiết về triển khai Hexagonal nữa, để nắm bắt nhanh ý tưởng, ta có class diagram sau:

Hình 1: Các lớp và class chính trong kiến trúc triển khai Hexagonal

Điểm nhấn chính: class ProductOutboundAdapter – đóng vai trò đúng với tên gọi của nó, là một adapter giao tiếp với database. Vì ta mong muốn xử lý Optimistic không nằm trong Application Layer và Domain Layer nên nó sẽ nằm trong adapter này. Cụ thể như sau.

Tại Application Layer, nơi điều phối nghiệp vụ, ProductManagement định nghĩa input port và ProductManagementUseCase là use case tương ứng.

public interface ProductManagement {
    void updateProduct(Long id, String description, Integer quantity, Integer version) 
            throws InvalidProductException;
}

Giao diện cập nhật product vẫn giữ lại những nét chính so với triển khai Layered. Tại sao version vẫn nằm trong API này mặc dù ta đã quyết định rằng Optimistic được xử lý ở adapter? Đây là điều cần làm vì ứng dụng cần biết user đang sửa trên phiên bản nào của dữ liệu. Nếu, giả sử, không khai báo thông tin này, trong xử lý updateProduct, ta lấy về product rồi cập nhật thông tin đó lên thì khả năng product của user đã cũ hơn rồi.

Cùng xem use case triển khai:

@Override
public void updateProduct(Long id, String description, Integer quantity, Integer version) throws InvalidProductException {
    Optional<BusinessProduct> productOptional = productRepository.read(id);

    if (productOptional.isPresent()) {
        BusinessProduct product = productOptional.get();
        product.updateInfo(description, quantity, version);

        productRepository.save(product);
    } else {
        throw new InvalidProductException("Product id=" + id + " not found.");
    }
}

Tiếp tục dòng suy nghĩ trên, tại sao không đưa version vào tìm kiếm, khi đó ta có productRepository.read(id, version)? Làm vậy cũng được, thậm chí nó còn mang lại hiệu quả xa hơn khi ta lưu lại toàn bộ các version của product trong database. Nhưng cũng cần đặt câu hỏi: liệu ta có muốn đưa nhiều thông tin về Concurrency vào logic hay không? Hãy lựa chọn theo cách của bạn.

Còn một điểm nữa bạn có thể thấy: BusinessProduct mà không phải Product.

Đối chiếu lại diagram ở hình 1, Product class realizes hai interface: BusinessProductOperationalProduct. Đây là một sự “rắc rối hóa” của tôi để tách biệt Product theo những mối quan tâm (concerns) khác nhau: mối quan tâm nghiệp vụ và mối quan tâm giao tiếp với database. Mô hình này là ý tưởng tiếp theo trong việc ánh xạ hai mô hình Domain Model và Database Model trong Hexagonal. Nếu bạn thấy sử dụng Memento pattern là quá cồng kềnh và có phần nguyên tắc thì bạn có thể xem xét cách này.

Hình 2: Product entity mang hai “nhân cách” ở hai môi trường khác nhau – tính đa hình của nó được thể hiện

Khi làm việc trong lõi ứng dụng, tôi sẽ sử dụng BusinessProduct, ở đó, tôi khai báo những hành vi mang đậm chất nghiệp vụ, cụ thể là:

public interface BusinessProduct {
    void updateInfo(String description, Integer quantity, Integer version);
}

Và, ProductRepository cũng chỉ cần biết về BusinessProduct mà thôi:

public interface ProductRepository {
    Optional<BusinessProduct> read(Long id);
    void save(BusinessProduct product);
}

Còn OperationalProduct, là nơi khai báo những hành vi mang tính dữ liệu cơ bản: các getters và setters:

public interface OperationalProduct {
    void setId(Long id);
    Long getId();
    void setName(String name);
    String getName();
    void setDescription(String description);
    String getDescription();
    void setQuantity(Integer quantity);
    Integer getQuantity();
    void setVersion(Integer version);
    Integer getVersion();
    void setUpdatedAt(LocalDateTime updatedAt);
    LocalDateTime getUpdatedAt();
}

Lõi nghiệp vụ không cần biết về OperationalProduct, phần adapter sẽ quan tâm:

public class ProductOutboundAdapter implements ProductRepository {
    …
    @Override
    public void save(BusinessProduct product) {
        OperationalProduct operationalProduct = (OperationalProduct) product;
        JpaProduct jpaProduct = convertToDatabaseModel(operationalProduct);
        jpaProduct.setVersion(jpaProduct.getVersion() + 1);

        int updatedRows = jpaProductRepository.updateProductWithVersion(jpaProduct);
        if (updatedRows == 0) {
            throw new RuntimeException("Conflict detected! The product has been modified by another     user.");
        }
    }
    …
}

Bạn có thể thấy rõ cách triển khai Optimistic tại ProductOutboundAdapter này gần như y hệt với triển khai trong Layered. Lúc này, convertToDatabaseModel sẽ sử dụng getters/setters như thường gặp:

private JpaProduct convertToDatabaseModel(OperationalProduct operationalProduct) {
    JpaProduct jpaProduct = new JpaProduct();
    jpaProduct.setId(operationalProduct.getId());
    jpaProduct.setName(operationalProduct.getName());
    jpaProduct.setDescription(operationalProduct.getDescription());
    jpaProduct.setVersion(operationalProduct.getVersion());
    jpaProduct.setUpdatedAt(operationalProduct.getUpdatedAt());

    return jpaProduct;
}

Cách triển khai với 2 interface tách biệt các mối quan tâm giúp cho mỗi nơi vẫn đảm bảo clean api list: Khi sử dụng Ctrl + Space, hành vi nghiệp vụ cô đọng, có giá trị thông tin cao vẫn được hiển thị mà không bị phân tâm bởi các method của data class.

Cụ thể hơn về cơ chế Optimistic

BusinessProduct, hàm updateInfo(...) như sau:

public void updateInfo(String description, Integer quantity, Integer version) {
    if (version != this.version) {
        throw new IllegalArgumentException("Version does not match.");
    }

    this.description = description;
    this.quantity = quantity;
}

Trong code domain này, tôi vẫn phải sử dụng version vì nhận thấy để kiểm soát Concurrency, Product domain vẫn phải có thông tin version, và ta không nên mù quáng xử lý nghiệp vụ khi ta không cần quan tâm xem hai version có khớp nhau hay không.

Tuy nhiên, tôi không nhúng kiểm soát Concurrency quá nhiều, điều này thể hiện ở việc không có code nào cho thấy hành động tăng giá trị của version lên. Xử lý đó nằm ở adapter:

public class ProductOutboundAdapter implements ProductRepository {
    …
    @Override
    public void save(BusinessProduct product) {
        OperationalProduct operationalProduct = (OperationalProduct) product;
        JpaProduct jpaProduct = convertToDatabaseModel(operationalProduct);
        jpaProduct.setVersion(jpaProduct.getVersion() + 1);

        int updatedRows = jpaProductRepository.updateProductWithVersion(jpaProduct);
        if (updatedRows == 0) {
            throw new RuntimeException("Conflict detected! The product has been modified by another     user.");
        }
    }

}

Và bên trong JpaProduct:

@Entity
@Table(name = "products")
@Data
public class JpaProduct {
    …
    public void setVersion(Integer version) {
        this.oldVersion = this.version;
        this.version = version;
    }
    …
}

Có thể bạn sẽ đặt câu hỏi: 
Đoạn code kiểm soát version, jpaProduct.setVersion(…), chỉ được đặt tại hàm save() của output adapter ProductOutboundAdapter liệu có đầy đủ không? Tức là nó có thể kiểm soát được Optimistic cho nghiệp vụ ứng dụng? Nhỡ đâu có thể ai đó có một output adapter khác hay một method khác cũng cập nhật dữ liệu product thì rất có thể họ sẽ không biết về vấn đề Concurrency này và không áp dụng.

Câu trả lời cho điều này là: nếu áp dụng đúng DDD thì chỉ một hàm save(…) như vậy là đủ cho Product aggregate.

Demo: Offline-Concurrency-Demo

Bình luận
Gửi bình luận
Bình luận