Vì sao kiến trúc nhiều tầng chia hệ thống thành các lớp?
Mục tiêu của việc chia lớp không phải là tạo càng nhiều thành phần càng tốt. Giá trị thực sự nằm ở separation of concerns: một thay đổi thuộc giao diện nên hạn chế ảnh hưởng đến quy tắc nghiệp vụ; một thay đổi về cách lưu dữ liệu không nên buộc toàn bộ giao diện phải viết lại. Khi ranh giới được thiết kế đúng, hệ thống dễ hiểu, dễ kiểm thử và dễ thay đổi hơn.
Cũng cần phân biệt layer và tier. Layer là ranh giới logic trong thiết kế phần mềm, còn tier thường chỉ một ranh giới triển khai hoặc thực thi riêng. Một ứng dụng có ba layer vẫn có thể chạy trong cùng một tiến trình trên một máy chủ. Vì vậy, chia hệ thống thành nhiều lớp không đồng nghĩa với việc bắt buộc phải triển khai mỗi lớp trên một máy chủ khác nhau.
Một yêu cầu đi qua các lớp như thế nào?
Cách dễ thấy nhất để hiểu kiến trúc nhiều tầng là theo dõi một yêu cầu từ lúc người dùng thao tác đến khi dữ liệu được đọc hoặc ghi. Trong mô hình ba lớp điển hình, yêu cầu đi từ giao diện đến phần xử lý nghiệp vụ, sau đó phần xử lý gọi lớp dữ liệu khi cần truy xuất trạng thái đã lưu.
Lớp giao diện tiếp nhận và trình bày
Lớp giao diện, hay presentation layer, chịu trách nhiệm tương tác với người dùng hoặc hệ thống gọi bên ngoài. Nó nhận HTTP request, dữ liệu từ biểu mẫu hoặc thao tác trên ứng dụng, chuyển chúng thành dạng mà phần xử lý phía sau có thể sử dụng, rồi trình bày kết quả.
Lớp này có thể kiểm tra những điều kiện mang tính hình thức như trường bắt buộc, định dạng ngày tháng hoặc cấu trúc request. Tuy nhiên, các quyết định nghiệp vụ quan trọng không nên nằm ở đây. Chẳng hạn, giao diện có thể phát hiện người dùng chưa nhập mã giảm giá, nhưng quy tắc “mã này được giảm bao nhiêu và có áp dụng cho đơn hàng hiện tại hay không” thuộc lớp xử lý nghiệp vụ.
Lớp xử lý thực hiện quy tắc nghiệp vụ
Business hoặc application layer là nơi hệ thống quyết định phải làm gì với yêu cầu. Lớp này điều phối use case, thực hiện quy tắc nghiệp vụ và xác định dữ liệu nào cần đọc hoặc thay đổi.
Với thao tác đặt hàng, phần xử lý có thể kiểm tra trạng thái sản phẩm, điều kiện áp dụng khuyến mại, tính tổng tiền và quyết định đơn hàng có hợp lệ hay không. Nó không nên chứa mã HTML hay câu lệnh SQL chỉ vì những công việc đó xuất hiện trong cùng một luồng xử lý.
Tách riêng phần nghiệp vụ đặc biệt quan trọng vì đây thường là phần thể hiện hành vi cốt lõi của hệ thống. Nếu quy tắc nằm rải rác trong giao diện, controller và truy vấn cơ sở dữ liệu, một thay đổi chính sách có thể buộc lập trình viên tìm và sửa nhiều vị trí không liên quan.
Lớp dữ liệu quản lý việc lưu và truy xuất
Data access layer chịu trách nhiệm giao tiếp với nguồn dữ liệu như cơ sở dữ liệu quan hệ, document database hoặc một kho lưu trữ khác. Nó chứa những chi tiết như truy vấn, ORM mapping, transaction hoặc cách chuyển dữ liệu lưu trữ thành đối tượng mà phần xử lý có thể sử dụng.
Điểm quan trọng là lớp dữ liệu không quyết định quy tắc nghiệp vụ chỉ vì nó có quyền truy cập dữ liệu. Ví dụ, truy vấn có thể lấy giá của sản phẩm, nhưng việc khách hàng nào được hưởng mức giảm giá nào vẫn là quyết định nghiệp vụ.
Khi ba trách nhiệm này được giữ riêng, luồng xử lý có thể hình dung tương đối rõ: giao diện nhận yêu cầu → lớp nghiệp vụ ra quyết định → lớp dữ liệu đọc hoặc ghi trạng thái → kết quả quay trở lại các lớp phía trên. Cấu trúc cụ thể có thể khác, nhưng nguyên tắc là mỗi ranh giới phải tương ứng với một trách nhiệm có ý nghĩa.

Vì sao tách lớp làm giảm phụ thuộc khi hệ thống thay đổi?
Lợi ích trung tâm của kiến trúc nhiều tầng là giới hạn phạm vi ảnh hưởng của thay đổi. Khi một lớp chỉ biết hợp đồng cần thiết của lớp kế tiếp thay vì phụ thuộc vào toàn bộ chi tiết triển khai, một phần của hệ thống có thể thay đổi mà không kéo theo việc sửa mọi phần còn lại.
Ví dụ, một ứng dụng web có thể thay đổi hoàn toàn cách trình bày trang sản phẩm trong khi quy tắc tính giá vẫn giữ nguyên. Tương tự, nhóm phát triển có thể tối ưu câu truy vấn hoặc thay đổi cách ánh xạ dữ liệu mà không cần sửa mã hiển thị, miễn là dữ liệu và hành vi mà lớp phía trên cần vẫn được bảo toàn.
Cơ chế này dựa trên hai đặc tính. Thứ nhất là cohesion: những mã nguồn phục vụ cùng một trách nhiệm được đặt gần nhau. Thứ hai là coupling: số lượng giả định mà một phần phải biết về phần khác được hạn chế. Tách file hoặc tạo nhiều thư mục không tự động tạo ra hai đặc tính này; ranh giới chỉ có giá trị khi trách nhiệm thực sự được tách.
Đó cũng là lý do một hệ thống “có đủ controller, service, repository” chưa chắc đã có kiến trúc tốt. Nếu controller chứa quy tắc nghiệp vụ, service trả thẳng entity của cơ sở dữ liệu cho giao diện và mọi lớp đều biết chi tiết schema, thay đổi ở database vẫn có thể lan xuyên toàn bộ ứng dụng. Khi đó hệ thống có nhiều lớp về hình thức nhưng mức phụ thuộc thực tế vẫn cao.
Ngược lại, ranh giới tốt giúp lập trình viên xác định nơi cần sửa dựa trên bản chất của thay đổi. Thay đổi cách hiển thị đi vào lớp giao diện; thay đổi chính sách đi vào phần nghiệp vụ; thay đổi cách lưu trữ chủ yếu đi vào lớp dữ liệu. Khả năng khoanh vùng này mới là lợi ích bảo trì quan trọng của việc chia lớp.
Tách lớp hỗ trợ kiểm thử, bảo mật và mở rộng ra sao?
Khi phần nghiệp vụ không phụ thuộc trực tiếp vào màn hình hoặc một kết nối cơ sở dữ liệu cụ thể, việc kiểm thử trở nên có mục tiêu hơn. Quy tắc nghiệp vụ có thể được kiểm thử với dữ liệu đầu vào đã kiểm soát, trong khi những bài kiểm thử tích hợp tập trung vào việc các lớp giao tiếp đúng với nhau. Lỗi vì thế dễ được khoanh vùng hơn so với một khối mã vừa xử lý request, vừa tính toán, vừa truy vấn dữ liệu.
Việc chia tier vật lý còn có thể tạo thêm ranh giới bảo mật. Chẳng hạn, client chỉ giao tiếp với tầng ứng dụng, còn cơ sở dữ liệu không được mở trực tiếp cho client. Tầng ứng dụng trở thành nơi thực hiện xác thực, kiểm tra quyền và kiểm soát các thao tác được phép trước khi truy cập dữ liệu.
Tuy nhiên, layering không tự động làm hệ thống an toàn. Nếu kiểm tra quyền được triển khai thiếu nhất quán, dữ liệu nhạy cảm bị trả về quá mức hoặc thông tin xác thực bị quản lý sai, việc có ba hay năm lớp cũng không khắc phục được vấn đề. Kiến trúc chỉ tạo ra vị trí và ranh giới thuận lợi để áp dụng kiểm soát bảo mật.
Khả năng mở rộng cũng có điều kiện tương tự. Khi các tier được triển khai độc lập, một tầng ứng dụng không lưu trạng thái phiên cục bộ có thể được nhân bản để xử lý thêm lưu lượng mà chưa cần nhân bản toàn bộ hệ thống. Trong khi đó, tầng dữ liệu có thể được tối ưu theo đặc tính lưu trữ riêng.
Nhưng một ứng dụng chỉ chia thành nhiều logical layer trong cùng một tiến trình không nhờ đó mà tự động có khả năng scale từng phần độc lập. Muốn đạt lợi ích ở cấp triển khai, các ranh giới tier, trạng thái, giao tiếp mạng và cách phân phối tải cũng phải được thiết kế tương ứng.
Ranh giới giữa các lớp nên được thiết kế thế nào?
Ranh giới tốt không chỉ ngăn lớp này gọi tùy tiện sang lớp khác; nó còn quy định thông tin nào được phép đi qua và lớp nào chịu trách nhiệm cho một quyết định. Một nguyên tắc thực tế là mỗi lớp nên có thể giải thích rõ “tôi chịu trách nhiệm về điều gì” và “tôi cần gì từ lớp kế tiếp” mà không phải biết quá nhiều về cách lớp đó thực hiện công việc.
Có thể kiểm tra ranh giới bằng một số dấu hiệu:
· Giữ quy tắc nghiệp vụ ở lớp xử lý, thay vì sao chép cùng một điều kiện ở giao diện, controller và truy vấn dữ liệu
· Dùng hợp đồng giao tiếp có mục đích rõ ràng, tránh để cấu trúc bảng hoặc entity lưu trữ trở thành mô hình dùng chung cho toàn bộ ứng dụng
· Hạn chế phụ thuộc vòng, vì hai lớp phải biết chi tiết của nhau thường là dấu hiệu trách nhiệm chưa được phân chia rõ
· Để mỗi lớp che giấu chi tiết triển khai, sao cho phần sử dụng nó chủ yếu quan tâm đến hành vi và dữ liệu cần thiết
Ví dụ, nếu lớp nghiệp vụ cần tìm khách hàng theo mã, nó nên yêu cầu một thao tác thể hiện đúng nhu cầu đó. Nó không cần biết lớp dữ liệu sử dụng SQL thuần, ORM hay một cơ chế lưu trữ khác. Ngược lại, lớp dữ liệu không nên quyết định khách hàng có đủ điều kiện nhận ưu đãi chỉ vì nó đang giữ thông tin về khách hàng.
Ranh giới cũng không nên tuyệt đối hóa. Nếu để truyền một dữ liệu đơn giản mà phải đi qua hàng loạt object, mapper và lớp trung gian không tạo thêm ý nghĩa, kiến trúc có thể đang tạo ceremony nhiều hơn giá trị. Mỗi lớp cần tồn tại vì nó bảo vệ một trách nhiệm, chính sách hoặc chi tiết có khả năng thay đổi độc lập.
Khi nào nên thêm hoặc gộp một lớp?
Không có số lớp chuẩn áp dụng cho mọi hệ thống. Ba lớp — giao diện, nghiệp vụ và dữ liệu — là một cấu trúc dễ hiểu và phổ biến, nhưng không phải benchmark cho chất lượng kiến trúc.
Một lớp mới hợp lý khi có một nhóm trách nhiệm đủ khác biệt để cần ranh giới riêng. Hệ thống có thể cần tách thêm phần tích hợp với dịch vụ bên ngoài, điều phối ứng dụng hoặc các chính sách đặc thù nếu chúng đủ phức tạp và có nhịp thay đổi khác với phần còn lại. Giá trị của lớp mới phải lớn hơn chi phí thêm dependency, mapping và điểm cần bảo trì.
Ngược lại, nếu một lớp chỉ nhận đối tượng từ lớp trên rồi chuyển nguyên vẹn xuống lớp dưới mà không áp dụng quy tắc, chuyển đổi, kiểm soát hay che giấu chi tiết nào, cần xem lại lý do tồn tại của nó. Việc gộp có thể làm hệ thống đơn giản hơn mà không làm mất separation of concerns.
Đây cũng là nơi sự khác biệt giữa layer và tier trở nên quan trọng. Thêm một layer logic trong cùng ứng dụng chủ yếu làm tăng độ gián tiếp của mã nguồn. Tách thành một tier triển khai riêng còn tạo thêm giao tiếp mạng, cấu hình, quan sát hệ thống và điểm có thể xảy ra lỗi. Vì vậy quyết định tách tier cần lý do vận hành rõ ràng hơn việc đơn thuần tạo thêm một module trong code.
Một tiêu chí thực tế là quan sát cách hệ thống thay đổi. Nếu các thay đổi thuộc một trách nhiệm thường có thể được xử lý trong phạm vi hẹp, ranh giới đang phát huy tác dụng. Nếu gần như mọi thay đổi đều phải sửa xuyên qua tất cả các lớp, vấn đề có thể nằm ở cách xác định ranh giới chứ không phải ở việc hệ thống chưa có đủ lớp.
Chi phí và giới hạn của kiến trúc nhiều tầng là gì?
Kiến trúc nhiều tầng đánh đổi sự đơn giản ban đầu để lấy khả năng kiểm soát thay đổi khi hệ thống lớn dần. Mỗi ranh giới mới có thể tạo thêm interface, model trung gian, mapping và đường đi mà lập trình viên phải theo dõi. Với một ứng dụng CRUD nhỏ, phần chi phí này đôi khi lớn hơn giá trị mà nó mang lại.
Nếu các tier giao tiếp qua mạng, chi phí còn rõ hơn. Một lời gọi hàm nội bộ trở thành request qua mạng sẽ có độ trễ, khả năng timeout và lỗi kết nối. Không có một con số độ trễ chung cho mọi kiến trúc nhiều tầng vì kết quả phụ thuộc vào hạ tầng, khoảng cách mạng, tải, giao thức và khối lượng dữ liệu. Điều có thể khẳng định là mỗi ranh giới mạng bổ sung thêm một điểm phải quan sát và xử lý lỗi.
Tách quá mức còn có thể khiến luồng nghiệp vụ khó theo dõi. Một thao tác đơn giản phải đi qua quá nhiều lớp chỉ để chuyển tiếp cùng một thông tin sẽ làm mã dài hơn mà Information Gain của kiến trúc không tăng. Khi đó abstraction trở thành chi phí thay vì công cụ kiểm soát độ phức tạp.
Vì vậy, mục tiêu không phải là tối đa hóa số tầng mà là đặt ranh giới đúng nơi có trách nhiệm và lý do thay đổi khác nhau. Hệ thống nên có đủ lớp để giao diện, quy tắc nghiệp vụ và chi tiết dữ liệu không trộn lẫn, nhưng cũng chỉ nên thêm ranh giới mới khi ranh giới đó giải quyết một vấn đề cụ thể.
Kiến trúc nhiều tầng chia hệ thống thành các lớp vì những phần như giao diện, xử lý nghiệp vụ và dữ liệu có trách nhiệm cũng như lý do thay đổi khác nhau. Tách chúng bằng ranh giới rõ ràng giúp giới hạn tác động của thay đổi, hỗ trợ kiểm thử và tạo điều kiện cho bảo mật hoặc mở rộng theo từng tier khi cách triển khai phù hợp.
Giá trị của mô hình không nằm ở số lượng lớp. Một kiến trúc nhiều tầng tốt là kiến trúc trong đó mỗi lớp có nhiệm vụ rõ, phụ thuộc được kiểm soát và chi phí của ranh giới thấp hơn lợi ích mà nó tạo ra. Nếu việc chia lớp chỉ làm yêu cầu đi qua thêm nhiều thành phần nhưng không cô lập được trách nhiệm hay thay đổi, hệ thống đang có nhiều lớp về hình thức hơn là có một kiến trúc nhiều tầng hiệu quả.
