Tớ hiểu rồi.
Thôi để tớ giải ngố cho cậu luôn, vì có vẻ như cậu là người không kiên nhẫn.
Về vấn đề của cậu, thực ra cậu muốn biết SRP (Single responsibility priciple) được áp dụng trong Controller như thế nào, chứ không chi tiết tới mức mô hình MVC, hay các nguyên lý còn lại. Vậy nên, tớ chỉ giải thích SRP được áp dụng như thế nào trong phần mềm, cậu sẽ tự suy luận ra nó được sử dụng như thế nào trong mô hình MVC nhé! (không khó lắm đâu, tớ hứa! )
TL;DR: SRP là nguyên tắc quan trọng, nên được áp dụng mọi lúc mọi nơi trong phần mềm.
Nếu cậu không đủ kiên nhẫn, hoặc đang chậm deadline, cậu nên dừng lại ở đây
Tổng quan về cách áp dụng SRP
Câu này thực ra không đúng đâu (tớ hỏi lại cậu lý do để hiểu nguyên nhân tại sao cậu lại đưa ra câu đó, nhưng có vẻ như cậu cũng không biết lý do ), không ai tạo ra các controller chỉ có 1 method để thoả mãn SRP cả.
Hẹp trong phạm vi của 1 ứng dụng, cậu có 3 scope để áp dụng SRP:
- Method
- Class/object
- Application architechture
Tớ sẽ đi sâu vào việc SRP được thể hiện như thế nào theo từng scope đó.
Method
Method là 1 trong những thành phần nhỏ nhất của 1 phần mềm, vậy nên phần này chắc là dễ hiểu nhất đối với cậu. SRP ở đây được thể hiện rất đơn giản: mỗi method chỉ làm duy nhất 1 task. Làm được điều đó, nó sẽ chỉ có duy nhất 1 lý do để sửa method đó thôi.
Vậy nên, nếu cậu có bất kỳ method nào làm nhiều hơn 1 việc, ví dụ:
- Method nào đó có tên
saveOrDeleteProfile
: làm 2 nhiệm vụ lưu - hoặc xoá profile.
- Method nào đó có tên
filterStudentAndTeacherList
, với giá trị trả về là 1 mảng 2 list: 1 cái chứa danh sách học sinh, 1 cái chứa danh sách giáo viên.
method đó sẽ được coi là vi phạm SRP.
Tại sao phải áp dụng SRP cho từng method? Lý do chủ yếu là cho code maintainability.
- Nếu cậu không áp dụng SRP cho từng method, cậu sẽ gặp rắc rối khi sửa chữa gì đó.
Ví dụ, method saveOrDeleteProfile
trên của cậu, được dùng ở tất cả những feature cần save profile lẫn delete profile.
Giờ, nếu cậu cần sửa lại tính năng save profile, cậu sẽ phải sửa hàm saveOrDeleteProfile
, và test tất cả các feature sử dụng hàm này, bất kể feature cần tính năng save hay delete. Điều tương tự với tính năng delete profile.
- Nếu cậu không áp dụng SRP cho từng method, code của cậu sẽ có tiềm năng trở thành spagetti code.
Ví dụ, method filterStudentAndTeacherList
ở trên, giờ giả sử sau 1 thời gian chạy, hệ thống của cậu bắt đầu chia sinh viên thành 2 loại: sinh viên dự bị đại học và sinh viên đại học. Cậu sửa lại method trên, trả về mảng gồm 3 list: sinh viên đại học, giảng viên và sinh viên dự bị đại học - chỉ cần thêm 1 if nữa trong vòng lặp.
Sau đó 2 năm, trường cậu bắt đầu đào tạo cả hệ cao đẳng, cậu sẽ có 2 loại sinh viên nữa: sinh viên cao đẳng và dự bị cao đẳng. Cậu sẽ có 2 khối if nữa, và mảng trả về sẽ có 2 list nữa.
Sau đó, trường của cậu bắt đầu có thêm giảng viên part time, giảng viên cao cấp,… Cậu sẽ nhận thấy code của cậu sẽ giống như bát mì spagetti - mọi thứ chồng chéo lên nhau.
Dễ hiểu đúng không?
Class/object
Class/object của cậu cũng cần được áp dụng SRP. Nó được thể hiện bằng cách mỗi class/object chỉ nên được design cho 1 mục đích. Tất cả các method/property/các parameter từng hàm trong class/object đó đều nên chỉ cần biết thông tin mà nó được thiết kế để đảm nhiệm, và chỉ làm đúng nhiệm vụ mà nó được thiết kế thôi.
Phần này sẽ hơi khó hiểu với cậu (đó là lý do cậu đưa ra nhận định mỗi function lại phải tạo 1 controller khác nhau), nên tớ sẽ đưa ra ví dụ.
- Class
UserController
làm việc điều phối các flow liên quan tới user, nên nó chỉ cần quan tâm tới các business liên quan tới User, mà không quan tâm tới business khác, ví dụ như Order. Nó chỉ nên biết các service class giúp nó hoàn thành các nhiệm vụ của nó, không cần quan tâm tới service Item.
UserController
vì làm nhiệm vụ điều phối, nên sẽ không cần quan tâm lấy user info từ đâu (nó là việc của UserService), không cần quan tâm tới làm thế nào để validate dữ liệu từ request (UserRequestValidator làm việc đó), không cần quan tâm tới khi gặp lỗi/ngoại lệ sẽ trả về dữ liệu gì (ExceptionHandler sẽ làm điều đó).
Nếu cậu có 1 nghiệp vụ mới liên quan tới user review, cậu có nên nhờ UserController
làm việc đó? Cậu nên hỏi UserReviewController
, vì đó là vấn đề của cậu, không phải của UserController
.
Giờ cậu biết áp dụng SRP vào UserController như thế nào rồi nhé!
- Class
FacebookPost
được thiết kế để chứa dữ liệu 1 post facebook, sử dụng trong business logic layer. Nó không nên tham gia vào việc chứa dữ liệu lấy từ database (FacebookPostDto
nên nắm giữ nhiệm vụ này), cũng không nên tham gia vào việc chứa dữ liệu sử dụng để trả về client (FacebookPostResponse
nên nhận việc này).
- Class
OrderService
chỉ cần quan tâm tới logic nghiệp vụ của order, không cần biết làm thế nào để tạo ra entity Order
(class OrderFactory
sẽ làm việc đó), nó chỉ cần đưa thông tin mà OrderFactory
cần, và lịch sự nhờ OrderFactory
tạo cho nó 1 object Order
mới để phục vụ cho công việc của nó.
Các class trên đều được áp dụng SRP triệt để. Cậu sẽ nhận được rất nhiều benefit từ đó:
-
UserController
sẽ dễ implement hơn, dễ để test hơn, dễ hiểu hơn, dễ bảo trì hơn. Do nhiệm vụ của nó được định nghĩa rõ ràng và được giới hạn, cậu hoàn toàn biết cần test những gì từ UserController.
-
FacebookPost
hoàn toàn không được sử dụng để lấy dữ liệu từ database hay dùng để trả dữ liệu về client, nên nó giúp business logic hoàn toàn tách rời với view, hay database access layer.
Giờ, nếu DB của cậu có thêm 1 trường dữ liệu mới creation_time
, nhằm mục đích note lại thời điểm bản ghi được tạo ra, do nó không cần ở business logic, nên cậu chỉ cần thêm trường creation_time
ở FacebookPostDto
mà không lo sự thay đổi đó làm hỏng logic nào đó ở business logic.
- Cậu có thể yên tâm hơn trong việc sử dụng các class được thiết kế đơn nhiệm.
OrderService
sẽ có thể được kết tập vào các OrderController
(mục đích hiển thị), RefundOrderService
(chứa logic cách refund 1 đơn hàng - tin tớ đi, nó rất phức tạp để đưa vào trong OrderService
), CheckoutOrderService
(chứa logic tạo order mới - cũng phức tạp không kém), MonthlySaleReportService
(tổng hợp doanh số hàng tháng của 1 shop)… mà không lo OrderService
có làm điều gì bất ngờ (như validation 1 lần nữa và tung lỗi “Bad request”, hay lưu dữ liệu vào Item tables).
Đó là lý do tại sao cậu cần SRP cho từng class/object.
Application architechture
Ừ, cậu không nhầm đâu, nó có thể áp dụng trong cả kiến trúc ứng dụng đấy.
Về cơ bản, các layer/component/module của ứng dụng đều nên chỉ làm 1 đúng nhiệm vụ mà nó được thiết kế, và chỉ 1 nhiệm vụ mà thôi.
Validator layer nên chỉ làm công việc validate, không thay đổi/convert dữ liệu.
View/Presenter layer nên chỉ quan tâm tới logic hiển thị, không quan tâm tới business logic.
Nếu cậu đảm bảo được SRP cho từng layer/component/module, cậu chắc chắn sẽ có quãng thời gian dễ chịu khi làm việc với mã nguồn.
Bonus: everywhere else
SRP là nguyên lý universal không chỉ áp dụng trong phạm vi 1 ứng dụng phần mềm, nó còn có thể áp dụng rộng rãi cho toàn hệ thống:
- Micro-service: khi cậu thiết kế micro-service, các service của cậu nên chỉ xử lý 1 nhiệm vụ xác định. Ví dụ: order management service nên chỉ quan tâm tới quản lý order, còn các nhiệm vụ tính toán giá Shipping fee hay Payment nên được delegate sang cho Shipping Service và Payment service.
- Database nên chỉ chịu trách nhiệm chứa dữ liệu, không nên đẩy business logic vào trong trigger/procedure/function.
- Hệ thống batch: mỗi batch nên làm 1 nhiệm vụ xác định, 1 batch không làm 2 nhiệm vụ một lúc.
- 1 Redis server nên hoặc được sử dụng cho một trong các nhiệm vụ cache, hoặc nhiệm vụ làm Message queue, và không làm nhiều hơn 1 nhiệm vụ đó.
- Front-end nên chỉ quan tâm tới việc hiển thị, không nên thực hiện business logic.
Conclusion
Cậu có thể thấy cụ thể SRP được áp dụng như thế nào trong thế giới lập trình. Cá nhân tớ đánh giá đó là 1 nguyên lý vô cùng quan trọng. Nếu cậu master việc áp dụng nguyên lý này (không chỉ trong công việc, mà còn trong cuộc sống), tớ nghĩ cậu sẽ có 1 sự nghiệp rất khởi sắc đấy
Hope it helps!