Trong bài viết trước, tôi đã chia sẻ một số ví dụ từ mã nguồn của Microsoft về việc vi phạm nguyên tắc Đơn nhiệm. Một trong những lý do dẫn đến điều này là thiết kế và kiến trúc mà chúng ta thường sử dụng không đảm bảo sự tuân thủ. Tuy nhiên, điều này không phải là vấn đề quá nghiêm trọng. Các nguyên tắc không thể luôn được tuân thủ 100% (giống như việc đánh răng mỗi sáng, chúng ta đều có những "cheat day" trong quá khứ, hiện tại, và tương lai). Những kiến trúc và thiết kế phổ biến vẫn được sử dụng bởi vì chúng mang lại giá trị to lớn và giúp ích cho chúng ta trong nhiều tình huống, mặc dù đôi lúc cũng khiến công việc của chúng ta trở nên hỗn độn hơn.
Tuy nhiên, việc áp dụng những kiến trúc này thật sự giúp mã nguồn của chúng ta trở nên tốt hơn và dễ quản lý hơn. Ngay cả Clean Architecture, mặc dù cố gắng giải quyết những vấn đề của Onion Architecture hay n-tiers, cũng có những nhược điểm riêng. Điều này chỉ ra rằng trên đời không có gì là hoàn hảo, ngay cả người đàn ông đầu tiên mà Chúa tạo ra cũng thiếu đi một chiếc xương sườn.
Trong bài viết này, tôi muốn bàn luận về lý do tại sao các lập trình viên, mặc dù đã hiểu nguyên tắc Đơn nhiệm, vẫn gặp khó khăn trong việc thực hành nó. Nguyên nhân đầu tiên có thể là sự cẩu thả và tùy tiện. Điều này không cần bàn đến, vì cẩu thả và tùy tiện có thể hủy hoại mọi hoạt động, không chỉ riêng lập trình. Tôi muốn nói đến một vấn đề mấu chốt mà các lập trình viên hay nhầm lẫn, dẫn đến việc thực hành sai và diễn giải sai về Đơn nhiệm.
Nhầm lẫn giữa TRÁCH NHIỆM và HÀNH VI
Qua các kênh vlog và blog, tôi thường thấy nhiều người diễn giải nguyên tắc Đơn nhiệm bằng cách nhìn vào một lớp (class) cụ thể. Ví dụ:
csharp
public class Account
{
public number Withdraw(number amount);
public number GetBalance();
}
Nhiều người cho rằng lớp này đang thực hiện hai nhiệm vụ: rút tiền và xem số dư. Theo nguyên tắc Đơn nhiệm, chúng ta nên tách lớp này ra:
csharp
public class AccountWithdrawal
{
public number Withdraw(number amount);
}
public class AccountBalance
{
public number GetBalance();
}
Tuy nhiên, đó là một sự nhầm lẫn hoàn toàn. Một lớp có thể có trách nhiệm duy nhất, nhưng không có nghĩa là nó không thể có nhiều hành vi. Lớp Account
đảm nhận trách nhiệm quản lý tài khoản và có các hành vi như rút tiền, gửi tiền và xem số dư hoàn toàn là hợp lý. Các ngôn ngữ lập trình hướng đối tượng đều hỗ trợ tính năng đa thừa kế, tức là một lớp có thể triển khai nhiều interface. Cách diễn giải chặt chẽ trên không thể giải thích tính năng này.
Một lớp có vai trò riêng của nó, tức là nó có trách nhiệm. Trong khi đó, interface lại là mô tả hành vi. Nếu chúng ta quay lại với các lớp dịch vụ trong phần 1, như AccountService
, thì nó thường sẽ kế thừa từ một interface IAccountService
. Đây cũng chính là vấn đề khi mà trách nhiệm thường bị lờ đi, và chúng ta chỉ quan tâm đến hành vi mà AccountService
cung cấp. Do đó, các lớp dịch vụ thường không giữ được tính Đơn nhiệm và trở thành một lớp siêu trong một thời gian dài phát triển.
Ví dụ khác, hãy xem đoạn mã sau:
csharp
public class Calculator
{
private readonly OperatorFactory operatorFactory;
public number Add(number a, number b);
{
return Calc(a, b, 'add');
}
public number Sub(number a, number b)
{
return Calc(a, b, 'sub');
}
private number Calc(number a, number b, string operatorType)
{
var operator = operatorFactory.CreateOperator(operatorType);
return operator.Execute(a, b);
}
}
public class OperatorFactory
{
public IOperator CreateOperator(string type)
{
switch (type)
{
case 'add':
return new AddOperator();
case 'sub':
return new SubOperator();
default:
throw new Exception('Operator type {type} is not implemented');
}
}
}
public interface IOperator
{
number Execute(number a, number b);
}
public class AddOperator : IOperator {...}
public class SubOperator : IOperator {...}
Trong đoạn mã trên, bạn thấy rằng lớp Calculator
thực hiện hai hành vi là cộng và trừ. Đây là hành vi của một thiết bị tính toán, nhưng vai trò của nó là tính toán tổng thể. Một lớp có thể có 1 trách nhiệm và nhiều hành vi. Ngược lại, đối với Operator
, lớp AddOperator
chỉ được phép thực hiện phép cộng, giữ vai trò và trách nhiệm của riêng mình: thực hiện phép cộng hai số.
Bên cạnh đó, hãy xét ví dụ về mẫu Facade trong một hệ thống thương mại điện tử:
csharp
public class CommerceFacade
{
public IOrderProcessor OrderProcessor;
public IKartManager KartManager;
public ICustomer Customer;
}
Liệu lớp CommerceFacade
có đảm nhiệm quá nhiều khi gọi các hành vi của order
, kart
, và customer
? Thiết kế CommerceFacade
không phải để nó thực hiện các hành vi trực tiếp mà chính là để cung cấp một cổng truy cập đến các dịch vụ khác, nhằm đơn giản hóa trải nghiệm mua sắm của khách hàng. Trách nhiệm của nó là giảm độ phức tạp của hệ thống khi lập trình viên tương tác với nó. Thay vì tìm kiếm từng thành phần, lập trình viên chỉ cần sử dụng Facade và biết rằng đã có tất cả công cụ cần thiết ở đó.
Các thành phần trong Facade cần thể hiện tính Đơn nhiệm, điều này giúp lập trình viên tránh nhầm lẫn giữa chúng. Định nghĩa trách nhiệm của Facade không bao gồm trách nhiệm của các thành viên. Mục đích của nó là trở thành "túi khôn" cho lập trình viên, giống như cái túi của Doreamon, với nhiều công cụ nhưng chỉ có một trách nhiệm duy nhất. Điều này có nghĩa là việc thay đổi orderProcessor
, kartManager
, hoặc customer
không dẫn đến sự thay đổi trong CommerceFacade
. Tuy nhiên, CommerceFacade
sẽ thất bại nếu bạn thêm UrlProvider
vào nó, vì xử lý URL không phải là trách nhiệm chính của Facade.
Tóm lại, một lớp thực hiện nhiều việc không nhất thiết là xấu. Sự hiểu lầm rằng Đơn nhiệm có nghĩa là chỉ làm một việc là một hạn chế. Trách nhiệm của Tổng thống là điều hành đất nước, nhưng ông ấy cũng thực hiện nhiều nhiệm vụ khác. Sự nhầm lẫn này, cộng với tình huống đa dạng và những cách mô tả thiết kế khác nhau giữa các lập trình viên, có thể khiến mọi người cảm thấy bối rối. Tại sao thêm một hàm vào lớp này là sai trong khi đối với lớp khác thì đúng? Tại sao một số nơi cần đến 2, 3 lớp mà nơi khác chỉ cần 1 lớp? Trách nhiệm của một siêu anh hùng là cứu thế giới, vậy thì ăn mặc kỳ lạ có phải là công việc của họ không?
source: viblo