+13

SOLID - Đơn nhiệm (P2)

Trong bài trước tôi đã trình bày một vài ví dụ lấy từ mã nguồn của Microsoft về việc vi phạm Đơn nhiệm (và kể cả một số lỗi coding convention thông thường). Một trong những lý do là thiết kế cũng như kiến trúc mà chúng ta tường dùng không đảm bảo được sự tuân thủ. Điều này thực ra cũng không có gì to tát. Các nguyên tắc vốn không thể tuân thủ 100% (đơn giản như việc đánh răng mỗi buổi sáng, chúng ta đều có cheat day trong quá khứ, trong hiện tại và trong cả tương lai). Các kiến trúc cũng như thiết kế phổ biến còn tồn tại vì chúng vẫn rất tuyệt vời và giúp ích chúng ta trong mọi trường hợp (dù đôi khi cũng khiến công việc của chúng ta lộn xộn một chút). Áp dụng các kiến trúc ấy thì mã nguồn của chúng ta dù sao cũng được xây dựng tốt hơn, dễ quản lý hơn. Kể cả Clean Architecture, dù cố gắng giải quyết vấn đề của Onion Architecture hay n-tiers, thì nó cũng có những vấn đề của nó, vì trên đời vốn không có gì hoàn hảo, kể cả người đàn ông đầu tiên được chúa tạo ra cũng thiếu cái xương sườn.

Bài này, tôi muốn bàn về nguyên do vì sao các lập trình viên, dù chúng ta đều hiểu Đơn nhiệm, nhưng lại khó khăn trong việc thực hành nó. Lý do đầu tiên thì chắc là thói cẩu thả và tùy tiện. Cái đó thì chúng ta không bàn tới làm gì vì cẩu thả và tùy tiện hủy hoại chúng ta trong mọi hoạt động, không riêng gì lập trình. Tôi muốn nói đến vấn đề mấu chốt mà các lập trình viên nhầm lẫn, dẫn đến thực hành sai, cũng như diễn giải sai đơn nhiệm:

Nhầm lẫn giữa TRÁCH NHIỆM và HÀNH VI

Qua các kênh vlog hay blog, tôi thường thấy các bạn diễn giải đơn nhiệm kiểu: Hãy nhìn vào class này:

public class Account
{
  public number Withdraw(number amount);
  
  public number GetBalance();
}

Class này đang làm hai việc: Rút tiền và Lấy số dư. Theo Đơn nhiệm, chúng ta phải tách class này ra:

public class AccountWithdrawal
{
  public number Withdraw(number amount);
}

public class AccountBalance
{
  public number GetBalance();
}

Đó là một sự nhầm lẫn hoàn toàn. Một class có trách nhiệm duy nhất, nhưng không ai cấm nó có nhiều hành vi. Một class Account có trách nhiệm quản lý tài khoản và có các hành vi Rút tiền, Gửi tiền, Đọc số dư là hoàn toàn bình thường và đúng đắn. Các ngôn ngữ OOP đều có tính năng đa thừa kế, tức là nó có thể implement nhiều interface. Cách diễn giải như trên không thể giải thích được tính năng này. Đến đây các bạn cũng hãy cũng cố lại chút kiến thức về class và interface nhé. Class có vai diễn của nó, tức nó có trách nhiệm. Còn interface là đặc tả hành vi. Hãy quay lại các class Service trong Phần 1, và là class phổ biến trong n-tiers hay Onion Architecture, một AccountService thường sẽ thừa kế một interface IAccountService. Đó là vấn đề. Trách nhiệm bị loại bỏ và chúng ta chỉ quan tâm hành vi mà AccountService cung cấp. Vì thể, các class Service thường không giữ được tính đơn nhiệm và trở thành một siêu class sau vài năm phát triển. Nhưng chúng ta cũng không tách IAccountService thành IHasBlance, ICanWithdraw, thật vô nghĩa vì các interface đó sẽ chẳng được sử dụng đâu khác, và có thể khiến danh sách thừa kế của service dài dằng dặc.

Hãy xem đoạn code sau:

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 {...}

Các bạn có thể thấy class Calculator làm 2 việc là Cộng và Trừ, đó là hành vi của một cái máy tính, gộp thành trách nhiệm của nó là tính toán. Một class có 1 trách nhiệm và có nhiều hành vi. Nhưng đối với Operator thì khác. AddOperator không được làm phép trừ, nó chỉ có 1 hành vi và 1 trách nhiệm: Cộng hai số.

Hãy xét ví dụ về mẫu Facade trong một hệ thống ecommerce:

public class CommerceFacade
{
  public IOrderProcessor OrderProcessor;
  
  public IKartManager KartManager;
  
  public ICustomer Customer;
}

Liệu class CommerceFacade có kiêm nhiệm quá nhiều khi nó có thể gọi các hành vi của order, kart và customer? Chúng ta thiết kế CommerceFacace không phải để nó thực hiện các hành vi của order, kart, hay customer, mà là một cổng để truy cập các dịch vụ cung cấp trải nghiệm mua bán của khách hàng. Trách nhiệm này giúp giảm tính phức tạp của hệ thống khi lập trình viên tiếp xúc với - thay vì tìm kiếm các thành phần tương ứng trong hệ thống, lập trình viên chỉ việc sử dụng Facade và hiểu rằng có đủ các công cụ cần thiết tại đây. Các thành phần trong Facace rõ ràng cũng phải thể hiện tính đơn nhiệm, nên các lập trình viên sẽ không sử dụng nhầm lẫn chúng. Định nghĩa về trách nhiệm của Facade không bao gồm trách nhiệm của thành viên, nó chỉ cố gắng trở thành túi khôn của lập trình viên, giống cái túi của Doreamon, dù vạn năng nhưng trách nhiệm của nó chỉ có 1, hành vi của nó cũng chỉ có 1. Việc orderProcessor, kartManager, Customer cần sửa đổi không kéo theo việc sửa đổi CommerceFacade. Nhưng CommerceFacade sẽ thất bại nếu bạn thêm UrlProvider vào nó. Việc xử lý Url không phải trách nhiệm hay lý do mà CommerceFacade sinh ra.

Một class làm nhiều việc là cách phát biểu gây nhầm lẫn. Và cách nghĩ đơn nhiệm có nghĩa chỉ làm một việc là thiếu sót. Trách nhiệm của Tổng thống là điều hành đất nước những rõ ràng ông ta phải làm trăm công nghìn việc.

Sự nhầm lẫn trên cộng với sự đa dạng tình huống trong thực tế, cách mô tả và thiết kế khác nhau đối với từng lập trình viên có thể khiến các bạn rối bời. Tại sao thêm hàm này vào class này là sai trong khi vào class kia thì đúng? Tại sao chỗ này có đến 2, 3 class tham gia vào việc này mà chỗ kia chỉ cần 1 class? Trách nhiệm của một siêu anh hùng là giải cứu thế giới, vậy việc ăn mặc dị hợm có nằm trong nhiệm vụ của cậu ta không?


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.