Biến nó thành kinh điển

Lập trình web sau bao nhiêu năm đã không còn xa lạ với chúng ta nữa. Hiện nay đã có hàng triệu website ra đời với muôn hình vạn trạng. Tuy nhiên cũng phải nói lại rằng "chắc là" cũng phải có đến 80% trong số đó đó là các website tin tức, viết và đăng bài đơn thuần (giống như blog của mình đang viết bằng wordpress này). Chúng ta sẽ không nói đến loại website này nhé, chúng ta sẽ nói đến việc xây dựng các web service. Các web service ngày nay có thể là các API gateway cho hệ thống microservice, các API cung cấp cho các ứng dụng SPA (Single Page Application) hay các API cung cấp cho bên thứ 3. Bạn có thể thấy rằng các web service này đều có liên hệ trực tiếp với các khách hàng, các đối tác hay các dịch vụ quan trọng khác, chính vì vậy chúng ta cần có một thiết kế để giúp các web service này chạy ổn định, dễ dàng mở rộng, và quan trọng nhất là có thể biến nó trở thành mô hình kinh điển để có thể tái sử dụng về sau này.

Từ thuở sơ khai

Về cơ bản từ xưa đến nay thì lập trình web chỉ đơn giản là lập trình ra một server trung gian để truy vấn cơ sở dữ liệu theo nghiệp vụ và trả về dữ liệu cho người dùng, code thế nào thì code miễn sao lấy được dữ liệu từ database trả cho client là được, kiểu:

<?php
$stmt = $conn->prepare("SELECT id, firstname, lastname FROM MyGuests");
$stmt->execute(); 
?>

Bởi thế mới sinh ra cái lỗi kinh điển SQL Injection.

Đến ngày hiện đại

Thật sự phải cám ơn Liferay rất nhiều đã cho mình học hỏi được rất nhiều thứ

Liferay thiết kế các lớp vào 3 tầng:

  1. Portlet: là tầng sẽ giao tiếp với client để nhận dữ liệu, gọi đến tầng Service xử lý và trả về kết quả cho client, bạn có thể tham ở đây
  2. Tầng Service: là tầng sẽ xử lý các logic nghiệp vụ và gọi xuống tầng Persistence để đọc ghi dữ liệu vào cơ sở dữ liệu, bạn có thể tham khảo ví dụ ở đây
  3. Tầng Persistence: Là tầng giao tiếp với cơ sở dữ liệu.

Cái hay của Liferay nằm ở công cụ gen code tự động từ file xml, có nghĩa là chúng ta chỉ cần tạo ra 1 file xml khai báo một số thứ và nó sẽ giúp chúng ta tạo ra code của tầng Persistence và một số lớp của tầng Service. Ở thời điểm đó nó thực sự rất tiện, việc chia thành nhiều tầng giúp cho việc thay đổi dễ dàng, ta có thể gen lại code mà không làm ảnh hưởng đến tầng Portlet hay một số lớp ở tầng Service.

Mở mang tầm mắt

Dự án đầu tiên mang lại cho mình hiểu biết thật sự lớn là đó dự án sử dụng Concur APIs. Đây là dự án đầu tiên mà mình thoát khỏi cái bóng của database.

Dự án này yêu cầu bọn mình phải kết hợp dữ liệu từ nhiều nguồn khác nhau để trả về cho client, dữ liệu ở thời đó chủ yếu là xml (SOAP APIs) và nó thực sự khó khăn. Bọn mình đã phải thảo luận rất nhiều để đưa ra được một thiết kế phù hợp, loại bỏ đi những phụ thuộc chồng chéo lẫn nhau, cuối cùng đã ra được một mô hình đơn giản như hình ở bên trên.

Xây dựng thiết kế mẫu

Sau nhiều năm đi làm thì cuối cùng mình cũng đã thiết kế ra được một mô hình phù hợp cho phần lớn các dự án web service của mình.

Các mũi tên to mô tả đường đi của dữ liệu và các mũi tên nhỏ mô tả sự phụ thuộc giữa các lớp. Thiết kế này bao gồm:

  1. Servlet: Đây là lớp sẽ hứng các request và trả về response cho client, lớp này thường do các thư viện cài đặt rồi nên chúng ta cũng sẽ không cần quan tâm
  2. Interceptor: Là lớp chịu trách nhiệm kiểm tra request, chúng ta có thể kiểm tra token, quyền, chặn các ip trong blacklist, các API không được phép gọi hay in ra log thông tin ở đây, ví dụ:
@Override
public void postHandle(RequestArguments arguments, Method handler) {
    logger.info(
        "request: {}, response: {}",
        arguments.getRequest().getRequestURI(),
        arguments.getResponse().getStatus()
    );
}
  1. Controller: Là lớp tiếp nhận request và trả lại response cho Servlet, khác với Servlet, request và response ở Controller thường ở dạng đối tượng java. Bạn không nên code nghiệp vụ ở đây mà hãy chuyển công việc phức tạp đó cho tâng Service, ví dụ:
@DoPost("/add")
public AuthorResponse addAuthor(@RequestBody AddAuthorRequest request) {
    authorValidator.validate(request);
    final AddAuthorData addAuthorData = requestToDataConverter.toData(request);
    final AuthorData authorData = authorService.saveAuthor(addAuthorData);
    return dataToResponseConverter.toResponse(authorData);
}
  1. Valiator: Là lớp chuyên kiểm tra dữ liệu đầu vào, ví dụ nhự độ dài, kiểu dữ liệu, ... ở đây trước khi cho phép dữ liệu đi xuống dưới tầng Service, ví dụ:
public void validate(AddAuthorRequest request) {
    if (request.getAuthorName().length() > MAX_NAME_LENGTH) {
        throw new HttpBadRequestException("name length must < " + MAX_NAME_LENGTH);
    }
}
  1. RequestToDataConverter: Là lớp chuyển đổi dữ liệu từ dạng request sang dạng dữ liệu (Data) chuyên dùng cho tầng service, ví dụ:
public AddAuthorData toData(AddAuthorRequest request) {
    return AddAuthorData.builder()
        .authorName(request.getAuthorName())
        .build();
}
  1. Service: Là lớp chuyên để xử lý nghiệp vụ, chúng ta sẽ viết tất cả logic ở đây, ví dụ:
final Category category = categoryRepository.findById(data.getCategoryId());
if (category == null) {
    throw new InvalidCategoryIdException(
        "category: " + data.getCategoryId() + " not found"
    );
}

final Book book = dataToEntityConverter.toEntity(data);
bookRepository.save(book);
return entityToDataConverter.toData(book, author, category);
  1. DataToEntityConverter: Là lớp chuyển đổi dữ liệu từ dạng data sang dạng đối tượng (Entity) của tầng Repository để có thể lưu xuống cơ sở dữ liệu, ví dụ:
public Author toEntity(AddAuthorData data) {
    final Author entity = new Author();
    entity.setName(data.getAuthorName());
    entity.setCreatedTime(LocalDateTime.now());
    entity.setUpdatedTime(LocalDateTime.now());
    return entity;
}
  1. Repository: Là lớp chuyên giao tiếp với cơ sở dữ liệu, ngày nay lớp này đa phần được tự động gen code hoặc được tự động cài đặt thông qua các framework như Spring hay EzyData, ví dụ:
@EzyRepository
public interface AuthorRepository extends EzyDatabaseRepository<Long, Author> {
}
  1. EntityToDataConverter: Là lớp chuyển đổi dữ liệu từ dạng Entity sang dạng Data để Service có thể trả lại cho tầng Controller. Tầng controller không bao giờ nên biết đến Entity, vì sao vậy? Mình sẽ nói ở bài sau nhé. Ví dụ:
public AuthorData toData(Author author) {
    return AuthorData.builder()
        .id(author.getId())
        .name(author.getName())
        .build();
}
  1. DataToResponseConverter: Là lớp chuyển đổi dữ liệu từ dạng Data sang dạng Response để controller trả lại cho Servlet, ví dụ:
public AuthorResponse toResponse(AuthorData data) {
    return AuthorResponse.builder()
        .id(data.getId())
        .name(data.getName())
        .build();
}
  1. ExceptionHandler: Là lớp chuyên xử lý exception, nếu có thể bạn đừng nên xử lý exception ở controller mà hãy gom tất cả việc xử lý exception về 1 lớp duy nhất, nó sẽ giúp bạn cực kì dễ quản lý và debug. Khi xử lý exception xong lớp này sẽ trả kết quả trực tiếp cho Servlet chứ không phải trả lại cho controller, ví dụ:
@TryCatch(IllegalArgumentException.class)
public ResponseEntity handleException(IllegalArgumentException e) {
    logger.info("invalid argument: {}", e.getMessage());
    return ResponseEntity.badRequest(e.getMessage());
}

Có vẻ như thiết kế này tương đối nhiều lớp và phức tạp đúng không? Nhưng nó phải như vậy thì mới có thể đảm bảo khả năng mở rộng và đối ứng với thay đổi dễ dàng nhất. Phức tạp thì đã có tool, điều đó không đáng ngại bằng việc một ngày nào đó chúng ta phải đập đi xây lại toàn bộ web service vì không thể nâng cấp được nữa, vì nó đã trở thành một núi rác.

Cùng nhìn lại

Mình đã đi qua cái thời mà kiểu "chẳng biết cái chi chi", code thì miễn sao cho chạy được, vì ở thời điểm mình mới ra trường thì chẳng có mấy ai hướng dẫn, trường đại học thì chỉ dạy những thứ cơ bản, nên phải tự mò mẫm để đưa ra được một mô hình phù hợp mà có lẽ các bạn đều biết hết rồi (đừng cười mình nhé, :D). Tại sao mình lại chọn thiết kế này? Và làm sao có thể dùng công cụ để gen ra code? Mình sẽ nói ở những bài kế tiếp nhé.

Tham khảo

  1. Ví dụ