Chúng ta có thế có nhiều cách khác nhau để đánh giá chất lượng code của một chương trình, nhưng một yêu cầu quan trọng luôn cần phải có đó là code chương trình phải có khả năng bảo trì được (maintainable). Những việc như căn thụt lề đúng cách, đặt tên biến gọn gàng, 100% được kiểm thử, v.v… sẽ chỉ là những yêu cầu thêm để cho phần code đó trở nên tốt hơn. Bất kì phần code nào không có khả năng bảo trì (maintainable) và không thể thích nghi với những yêu cầu thay đổi thì sẽ nhanh chóng bị lỗi thời. Chúng ta có thể không cần phải viết ra phần code hoàn hảo khi đang cố gắng tạo ra một nguyên mẫu (prototype), một bằng chứng của khái niệm hoặc một sản phẩm khả thi tối thiểu (MVP), nhưng trong hầu hết các trường hợp khác chúng ta nên luôn luôn viết ra code có khả năng bảo trì được (maintainable). Điều này nên được coi là một chất lượng cơ bản của công nghệ phần mềm và thiết kế.
Trong bài viết này tôi sẽ thảo luận về Nguyên tắc trách nhiệm duy nhất (Single Responsibility Principle) và một số kỹ thuật xoay quanh nó để có thể giúp bạn nâng cao chất lượng code chương trình của mình. Viết được code hoàn hảo là một nghệ thuật, nhưng một số nguyên tắc luôn luôn có thể giúp cho công việc của bạn phát triển theo hướng tạo nên phần mềm mạnh mẽ và dễ bảo trì.
Model là tất cả
Hầu hết mọi cuốn sách viết về một số framework MVC mới (MVP, MVVM, hoặc M** khác) thường có rất nhiều những đoạn code ví dụ tồi. Những ví dụ này cố gắng thể hiện những gì mà framework đó cung cấp. Nhưng chúng cũng kết thúc bằng cách đưa ra những lời khuyên tồi cho những người mới bắt đầu. Ví dụ như "chúng ta có ORM X này cho các model, templating engine Y cho các view và chúng ta sẽ có các controller để quản lý tất cả chúng", điều này không thu được gì khác hơn là các controller đồ sộ.
Mặc dù ở một khía cạnh tích cực nào đó, những ví dụ này minh họa cách dễ nhất để bạn có thể tiếp cận với framework của họ. Nhưng như vậy không có nghĩa là chúng sẽ dạy cho bạn cách thiết kế phần mềm đúng cách. Những người học dần dần sẽ nhận thấy sự phản tác dụng trong các dự án sau này của họ.
Các model chính là trái tim trong ứng dụng của bạn. Nếu bạn có các model tách ra khỏi phần còn lại của logic ứng dụng, thì việc bảo trì sẽ dễ dàng hơn nhiều, bất kể ứng dụng của bạn có phức tạp đến thế nào đi nữa. Ngay cả với những ứng dụng phức tạp, việc sử dụng model tốt cũng có thể tạo ra những code chương trình dễ hiểu hơn rất nhiều. Để đạt được điều này hãy bắt đầu bằng việc đảm bảo những model của bạn chỉ làm đúng những việc yêu cầu, đừng quá quan tâm đến toàn bộ ứng dụng. Hơn nữa nó cũng không liên quan đến những gì nằm bên dưới tầng lưu trữ dữ liệu: cho dù ứng dụng của bạn dựa vào cơ sở dữ liệu SQL hay là lưu trữ ở dạng file text.
Ở phần sau của bài viết này, bạn sẽ nhận ra để viết được code tốt đòi hỏi phải phân tách sự liên quan rất nhiều.
Tại các khóa học và thực tập ở TechMaster, học viên luôn được hướng dẫn phát triển phần mềm theo các nguyên tắc SOLID để cho ra sản phẩm phần mềm tốt và dễ bảo trì.
Nguyên tắc trách nhiệm duy nhất (Single Responsibility Principle)
Có thể bạn đã nghe nói về các nguyên tắc SOLID: trách nhiệm duy nhất (Single responsibility), đóng mở (Open-closed), thay thế của Liskov (Liskov substitution), phân chia interface (Interface segregation), đảo ngược sự phụ thuộc (dependency inversion). Chữ S đầu tiên mô tả về nguyên lý chịu trách nhiệm duy nhất (Single Responsibility Principle - SRP), và tầm quan trọng của nó không hề bị phóng đại. Tôi thậm chí còn cho rằng đó là điều cần thiết, tối quan trọng để viết ra code tốt. Trong thực tế, trong bất kỳ phần code được viết tồi nào thì bạn đều có thể tìm thấy một class làm nhiều hơn một nhiệm vụ của nó. Ví dụ như form1.cs hoặc index.php chứa vài nghìn dòng lệnh hoặc những thứ tương tự, chúng ta đều đã từng thấy hoặc tạo nên chúng.
Chúng ta hãy xem một ví dụ trong C# (ASP.NET MVC và Entity framework). Dù bạn không phải là một lập trình viên C#, thì bạn cũng sẽ dễ dàng hiểu được nếu đã có kinh nghiệm về lập trình hướng đối tượng.
public class OrderController
{
...
public ActionResult CreateForm()
{
/*
* View data preparations
*/
return View();
}
[HttpPost]
public ActionResult Create(OrderCreateRequest request)
{
if (!ModelState.IsValid)
{
/*
* View data preparations
*/
return View();
}
using (var context = new DataContext())
{
var order = new Order();
// Create order from request
context.Orders.Add(order);
// Reserve ordered goods
…(Huge logic here)...
context.SaveChanges();
//Send email with order details for customer
}
return RedirectToAction("Index");
}
... (many more methods like Create here)
}
Đây là một lớp OrderController thông thường, nó có phương thức Create. Trong các controller như thế này, tôi thường thấy lớp Order được sử dụng như là một request parameter. Nhưng tôi thích sử dụng những lớp thực hiện nhiệm vụ riêng biệt hơn (Single Responsibility Principle).
Chúng ta cần lưu ý là trong đoạn code trên controller biết quá nhiều về quá trình “đặt hàng” bao gồm cả lưu trữ đối tượng Order, và gửi email cho khách hàng, v.v… Hay nói đơn giản là có quá nhiều chức năng trong một lớp duy nhất. Mỗi khi có một thay đổi nhỏ thì lập trình viên cần phải thay đổi toàn bộ code của controller. Trong trường hợp những Controller khác cũng cần tạo ra các order, thường thì các lập trình viên sẽ copy-paste đoạn code đó. Các controller chỉ nên kiểm soát toàn bộ quá trình, và không thực sự chứa tất cả logic của quá trình đó.
Nhưng hôm nay là ngày chúng ta sẽ ngừng viết các controller đồ sộ kiểu như vậy!
Đầu tiên chúng ta sẽ trích xuất tất cả logic nghiệp vụ từ controller đó và chuyển nó vào một class OrderService:
public class OrderService
{
public void Create(OrderCreateRequest request)
{
// all actions for order creating here
}
}
public class OrderController
{
public OrderController()
{
this.service = new OrderService();
}
[HttpPost]
public ActionResult Create(OrderCreateRequest request)
{
if (!ModelState.IsValid)
{
/*
* View data preparations
*/
return View();
}
this.service.Create(request);
return RedirectToAction("Index");
}
}
Với đoạn code trên, controller giờ đây sẽ chỉ làm một nhiệm vụ duy nhất đó là quản lý tiến trình (process). Nó chỉ quan tâm đến các view, và các class OrderService và OrderRequest - ít nhất là tập các thông tin cần thiết để nó có thể làm công việc của mình, đó là quản lý các yêu cầu (request) và gửi các phản hồi (response).
Với cách làm này, bạn hiếm khi phải thay đổi code của controller. Những thành phần khác như các view, các đối tượng request và service vẫn có thể thay đổi khi chúng được gắn với các yêu cầu nghiệp vụ, mà không phải là các controller.
Đó là ý nghĩa của nguyên lý chịu trách nhiệm duy nhất (SRP) và có nhiều kỹ thuật để viết code đáp ứng nguyên lý này. Một ví dụ cho điều này là dependency injection (rất hữu ích để viết ra code có thể kiểm thử được).
Dependency Injection
Thật khó tưởng tượng một dự án lớn dựa trên nguyên lý chịu trách nhiệm duy nhất (Single Responsibility Principle) mà không có Dependency Injection. Chúng ta hãy cùng xem lại lớp OrderService một lần nữa:
public class OrderService
{
public void Create(...)
{
// Creating the order(and let’s forget about reserving here, it’s not important for following examples)
// Sending an email to client with order details
var smtp = new SMTP();
// Setting smtp.Host, UserName, Password and other parameters
smtp.Send();
}
}
Đoạn code này vẫn làm việc nhưng nó chưa hoàn hảo. Để có thể hiểu được phương thức create trong lớp OrderService hoạt động như thế nào, chúng ta bắt buộc cần phải hiểu về những sự phức tạp trong giao thức gửi nhận email SMTP. Và, một lần nữa, viêc copy-paste là cách duy nhất để tái sử dụng đoạn code chương trình SMTP bất cứ khi nào nó cần thiết. Nhưng với một chút tái cấu trúc (refactoring), điều này có thể thay đổi:
public class OrderService
{
private SmtpMailer mailer;
public OrderService()
{
this.mailer = new SmtpMailer();
}
public void Create(...)
{
// Creating the order
// Sending an email to client with order details
this.mailer.Send(...);
}
}
public class SmtpMailer
{
public void Send(string to, string subject, string body)
{
// SMTP stuff will be only here
}
}
Có vẻ đã tốt hơn rất nhiều! Nhưng lớp OrderService vẫn biết quá nhiều về việc gửi email. Điều gì xảy ra nếu chúng ta muốn thay đổi nó trong tương lai. Nếu chúng ta muốn gửi nội dung của email đó đến một file log đặc biệt thay vì thực sự gửi chúng trong môi trường phát triển của mình? Nếu chúng ta muốn sử dụng unit test với class OrderService của mình thì sao? Chúng ta hay tiếp tục tái cấu trúc bằng cách tạo ra một interface IMailer:
public interface IMailer
{
void Send(string to, string subject, string body);
}
SmtpMailer sẽ implement cái interface này. Ngoài ra, ứng dụng của chúng ta sẽ sử dụng một loC-container và chúng ta có thể cấu hình nó để IMailer được thực thi bởi class SmtpMailer. Lớp OderService có thể được thay đổi như sau:
public sealed class OrderService: IOrderService
{
private IOrderRepository repository;
private IMailer mailer;
public OrderService(IOrderRepository repository, IMailer mailer)
{
this.repository = repository;
this.mailer = mailer;
}
public void Create(...)
{
var order = new Order();
// fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.)
this.repository.Save(order);
this.mailer.Send(<orders user email>, <subject>, <body with order details>);
}
}
Bây giờ chúng ta đã tiến đến một nơi nào đó! Tôi nhân cơ hội này sẽ tạo một thay đổi khác. Lớp OrderService bây giờ dựa vào interface IOrderRepository để tương tác với thành phần lưu trữ tất cả các đơn hàng của chúng ta. Nó không còn quan tâm đến việc interface đó được thực thi như thế nào và công nghệ lưu trữ thực hiện nó. Bây giờ class OrderSevice chỉ chứa code thực hiện logic nghiệp vụ về đơn hàng.
Bằng cách này, nếu một tester tìm ra lỗi trong việc gửi email, thì lập trình viên sẽ biết tìm chính xác chỗ cần sửa là lớp SmtpMailer. Nếu có sai sót gì trong giảm giá sản phẩm, thì lập trình viên sẽ tìm đến code của lớp OrderService (trong trường hợp bạn đã hiểu rõ SRP, sau đó có thể là lớp DiscountService).
Event Driven Architecture
Tuy nhiên, tôi vẫn không thích phương thức Create trong lớp OrderService:
public void Create(...)
{
var order = new Order();
...
this.repository.Save(order);
this.mailer.Send(<orders user email>, <subject>, <body with order details>);
}
Việc gửi mail không phải là một việc chính trong quá trình đặt hàng. Thậm chí ứng dụng thất bại trong việc gửi email, thì đơn hàng vẫn phải được tạo ra chính xác. Ngoài ra, hãy tưởng tượng một tình huống bạn phải thêm một lựa chọn mới trong phần thiết lập của người dùng, cho phép họ tùy chọn có nhận một email sau khi đặt hàng thành công hay không. Để kết hợp điều này vào trong class OrderService, chúng ta sẽ cần giới thiệu một dependency, IUserParametersService. Bổ sung thêm phần localization vào trong hỗn hợp này, và bạn sẽ có thêm một dependency khác, ITranslator (để tạo ra nội dung email trong ngôn ngữ mà người dùng lựa chọn). Một số hành động khác là không cần thiết, đặc biệt là ý tưởng thêm nhiều dependencies và kết thúc với một constructor dài lòng thòng không hiển thị vừa một màn hình. Tôi tìm được một ví dụ tuyệt vời về điều này trong codebase của Magento (một CMS thương mại điện tử phổ biến được viết bằng PHP) trong một class mà có tới 32 dependencies!
Đôi khi rất khó để tìm ra cách tách logic này, và class của Magento có lẽ là một nạn nhân của một trong những trường hợp như vậy. Đó là lý do tại sao tôi thích cách hướng sự kiện (event-driven) hơn:
namespace <base namespace>.Events
{
[Serializable]
public class OrderCreated
{
private readonly Order order;
public OrderCreated(Order order)
{
this.order = order;
}
public Order GetOrder()
{
return this.order;
}
}
}
Bất cứ khi nào một đơn hàng (order) được tạo ra, thay vì gửi một email trực tiếp từ lớp OrderService, lớp sự kiện đặc biệt OrderCreated được tạo ra và một sự kiện được phát sinh. Một nơi nào đó trong ứng dụng các event handler sẽ được cấu hình. Một trong số chúng sẽ gửi một email tới khách hàng.
namespace <base namespace>.EventHandlers
{
public class OrderCreatedEmailSender : IEventHandler<OrderCreated>
{
public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator)
{
// this class depend on all stuff which it need to send an email.
}
public void Handle(OrderCreated event)
{
this.mailer.Send(...);
}
}
}
Lớp OrderCreated được đánh dấu Serializable vào mục đích này. Chúng ta có thể quản lý sự kiện này ngay lập tức, hoặc lưu trữ tuần tự trong một hàng đợi (Redis, ActiveMQ, hay cái gì khác) và xử lý nó trong process/thread riêng biệt từ một handling web requests. Trong bài viết này, tác giả giải thích chi tiết về kiến trúc event-driven là gì (xin vui lòng đừng quan tâm đến logic nghiệp vụ trong OrderController).
Một số người có thể lập luận rằng bây giờ nó trở nên rất khó hiểu về những gì đang xảy ra khi bạn tạo đơn hàng (order). Nhưng sự thật không phải như vậy. Nếu bạn cảm thấy như vậy, chỉ đơn giản là hãy tận dụng chức năng của IDE của mình. Bằng cách tìm tất cả những chỗ sử dụng lớp OrderCreated trong IDE, chúng ta có thể nhìn thấy tất cả các hành động liên quan đến sự kiện này.
Nhưng khi nào tôi nên sử dụng Dependency Injection và khi nào tôi nên sử dụng một cách tiếp cận hướng sự kiện (Event-driven)? Không phải luôn luôn dễ dàng để trả lời câu hỏi này, nhưng có một nguyên tắc đơn giản có thể giúp bạn đó là sử dụng Dependency Injection cho tất cả các hoạt động chính của bạn trong ứng dụng, và hướng tiếp cận Event-driven cho tất cả các hành động thứ cấp. Ví dụ, sử dụng Dependecy Injection với những thứ như việc tạo một đơn hàng (order) trong lớp OrderService với IOrderRepository, và ủy quyền việc gửi email, hoặc một việc gì đó không phải là phần quan trọng của việc tạo đơn hàng chính, tới một số event handler.
Kết luận
Chúng ta bắt đầu với một controller rất cồng kềnh, chỉ là một class, và kết thúc với một bộ sưu tập công phu của các class. Những lợi thế của các thay đổi này là khá rõ ràng từ những ví dụ. Tuy nhiên, vẫn còn rất nhiều cách để cải tiến những ví dụ này. Ví dụ, phương thức OrderService.Create có thể được chuyển đến một class riêng của nó: OrderCreator. Từ khi việc tạo order là một đơn vị độc lập của logic nghiệp vụ tuân theo Nguyên tắc trách nhiệm duy nhất (Single Responsibility Principle), thì khi nó có class riêng với tập các dependencies của riêng mình cũng là điều bình thường. Tương tự như vậy, các đơn hàng bị hủy bỏ (order removal và order cancellation) có thể được thực thi trong các class của riêng chúng.
Khi tôi viết code dạng một khối, tương tự như ví dụ đầu tiên trong bài viết này, bất kỳ thay đổi nhỏ nào của yêu cầu có thể dễ dàng dẫn đến nhiều thay đổi ở các phần khác của code. SRP giúp các lập trình viên viết code đó được tách riêng ra, nơi mà mỗi class có công việc riêng của nó. Nếu các đặc tả kỹ thuật của công việc này thay đổi, thì các lập trình viên chỉ cần thay đổi những class xác định. Sự thay đổi này ít có khả năng phá vỡ toàn bộ ứng dụng, và các class khác vẫn sẽ làm công việc của chúng như trước, tất nhiên trừ khi chúng đã bị phá vỡ ngay từ đầu.
Việc phát triển code bằng cách sử dụng các kỹ thuật này và nguyên tắc trách nhiệm duy nhất (Single Responsibility Principle) có thể dường như là một nhiệm vụ khó khăn, nhưng những nỗ lực này chắc chắn sẽ mang lại lợi ích to lớn khi dự án ngày càng lớn lên và tiếp tục phát triển.
Bản dịch của Đỗ Như Phương, lập trình viên iOS tại TechMaster
Nguồn: http://www.toptal.com/software/single-responsibility-principle
Bình luận