SOLID – Nguyên tắc 4: Chia nhỏ interface – Interface segregation principle (ISP)

Chúng ta tiếp tục tìm hiểu bộ nguyên tắc lập trình SOLID – là các nguyên tắc giúp chúng ta thiết kế chương trình và viết code tốt hơn: code trong sáng và rành mạch, dễ bảo trì, dễ mở rộng trong tương lai. SOLID gồm 5 nguyên tắc lập trình sau đây:

  1. Single Responsibility principle.
  2. Open-Closed principle.
  3. Liskov substitution principle.
  4. Interface segregation principle.
  5. Dependency inversion principle.

Chúng ta đã tìm hiểu được 3 nguyên tắc đầu tiên trong bộ nguyên tắc này. Hôm nay, chúng ta sẽ tiếp tục phân tích và tìm hiểu nguyên tắc thứ tư trong SOLID: Interface segregation principle – Tách nhỏ interface.

Trước khi bắt đầu

Có một vấn đề mà mọi người nên chú ý, đó là nguyên lí này áp dụng trên interface chứ không phải áp dụng cho class nhé.

Nhắc lại một chút kiến thức về interface, theo định nghĩa thì: Interface là một lớp rỗng chỉ chứa khai báo về tên phương thức không có khai báo về thuộc tính hay thứ gì khác và các phương thức này cũng là rỗng. Bởi vậy bất kỳ lớp nào sử dụng lớp interface đều phải định nghĩa tất cả các phương thức đã khai báo ở interface.

Trong một hệ thống, để ứng dụng được linh hoạt và có thể được phát triển đồng thời, thì các module khác nhau nên giao tiếp với nhau thông qua các interface, tức là ta chỉ cần quan tâm tới “bề mặt” mà interface đã cung cấp mà không cần quan tâm tới implement cụ thể bên dưới là như thế nào.

Ý nghĩa khác biệt cơ bản khi thực hiện kế thừa class và implement interface là: class con sẽ dùng kế thừa để tận dụng lại những gì đã có của class cha, trong khi đó một class implement interface để “cá nhân hoá” các cài đặt (implement) của riêng nó.

Nguyên tắc thứ 4 này nói gì?

Phát biểu:  Nếu Interface quá lớn thì nên tách thành các interface nhỏ hơn, với nhiều mục đích cụ thể.

Nguyên tắc này khác trực quan trong thực tế, bạn hãy hình dung về việc mua một cái vali để chứa đồ đi chơi xa. Bạn đến cửa hàng thì có hai lựa chọn phù hợp với túi tiền của bạn như hình bên dưới: một cái vali to, hoặc là nhiều cái vali nhỏ. Bạn nghĩ rằng vali càng to thì càng chứa được nhiều đồ, như vậy thì càng tốt, và bạn sắm ngay một chiếc thật to.

1469504874073_6676564
Figure 1 – Cùng một giá tiền, bạn sẽ mua 1 vali to, hay nhiều vali nhỏ?

Lần đi chơi thứ nhất, bạn đi 10 ngày, do đó cần mang theo rất nhiều đồ, và cái vali to chứa đủ đồ cần thiết cho 10 ngày. Quá tuyệt vời – bạn nghĩ thầm. Và rồi công việc bận rộn hơn, bạn không còn có nhiều thời gian để đi chơi dài ngày nữa, những lần đi chơi chỉ còn tầm 3 tới 5 ngày, vali thì quá to còn đồ dùng thì lại ít. Và lúc đó, bạn ước rằng mình đã mua 2 cái vali nhỏ thay vì 1 cái to. Bởi vì nếu cần mang nhiều đồ, bạn vẫn có thể mang theo 2 vali, nhưng bạn không thể “cắt nhỏ” cái vali bự ra thành nhiều phần nhỏ hơn được.

Đó là một ví dụ cho việc chúng ta nên tách nhỏ interface, trong lập trình thì mọi việc cũng khá là tương tự như thế.

Áp dụng nguyên tắc này trong lập trình thế nào?

Hẳn là bạn còn nhớ ví dụ về “Student” ở các bài trước, hình dung như sau: sinh viên cần học nhiều môn, và mọi sinh viên (sinh viên chính quy, sinh viên hệ tài năng, …) đều học những môn giống nhau. Điểm khác nhau duy nhất là sinh viên hệ cử nhân tài năng phải học thêm một môn là tiếng Pháp.

Chúng ta có nhận xét sau: Việc học của mỗi người sẽ khác nhau và tuỳ thuộc vào mỗi sinh viên, nhưng tất cả đều học, do vậy dùng interface là hợp lí. Lúc thiết kế, bạn nghĩ tới việc thiết kế một interface là InfStudy có các function học, do chỉ có duy nhất một sự khác biệt nên bạn nhét hết tất cả môn học vào cùng một interface này luôn:

interface InfStudy
{
   function studyEnglish();
   function studyMath();
   function studyFrench();
}

Các loại sinh viên khác nhau đều sẽ implement interface này:

class NormalStudent implements InfStudy
{
   function studyEnglish(){
      // study this way
   }

   function studyMath(){
      // study this way
   }

   function studyFrench(){
      //do không cần học nên không implement gì cả
      return NULL;    
   }
}


class AdvancedStudent implements InfStudy
{
   function studyEnglish(){
      // study that way
   }

   function studyMath(){
      // study that way
   }

   function studyFrench(){
      // study that way
   }
}

Thiết kế như thế này cho tới hiện tại là ổn, mọi thứ chạy tốt. Lớp NormalStudent phải implement hàm studyFrench() cho đúng cú pháp mặc dù bên trong không cài đặt gì cả, tuy nhiên chỉ có vấn đề nhỏ này thôi nên cũng tạm chấp nhận. Thời gian trôi đi và yêu cầu được mở rộng: sinh viên hệ chính quy phải học thêm môn triết học, trong khi hệ cử nhân tài năng được miễn. Chúng ta có nên đặt hàm này vào interface InfStudy luôn không?

Chúng ta nhận ra rằng, bởi vì sự khác biệt ngày càng nhiều, rất có thể sau này sẽ phát sinh nhiều môn học riêng cho từng hệ sinh viên nữa, do đó chúng ta nên có cách giải quyết triệt để hơn, bởi vì nếu cứ viết chung vào interface InfStudy thì sẽ phát sinh việc phải implement nhiều hàm không cần thiết. Do vậy, chúng ta quyết định tách việc học thành các interface cụ thể khác nhau: phần học chung cho cả 2 hệ, phần học riêng cho cử nhân tài năng, phần học riêng cho chính quy. Triển khai như sau:

interface InfCommonStudy
{
   function studyEnglish();
   function studyMath();
}

interface InfAdvancedStudy
{
   function studyFrench();
}

interface InfNormalStudy
{
   function studyPhilosophy();
}

Như vậy, đối với từng đối tượng sinh viên cụ thể, mà ta sẽ implement những interface tương ứng:

class NormalStudent implements InfStudy, InfNormalStudy
{
   function studyEnglish(){
      // study this way
   }

   function studyMath(){
      // study this way
   }

   function studyPhilosophy(){
      //study this way
   }
}



class AdvancedStudent implements InfStudy, InfAdvancedStudy
{
   function studyEnglish(){
      // study that way
   }

   function studyMath(){
      // study that way
   }

   function studyFrench(){
      // study that way
   }
}

Bạn thấy đó, với thiết kế này, chúng ta không còn phải lo lắng tới việc phải implement những hàm không cần thiết, cũng sẽ dễ dàng kiểm soát được việc mở rộng hơn. Trong tương lai, nếu phát sinh thêm nhiều môn học riêng, hoặc có những loại sinh viên mới có những môn học chuyên biệt khác, việc triển khai cũng dễ dàng và tường minh hơn rất nhiều.

Nhận xét và kết

Bạn có thể dùng class hoặc interface trong lập trình hướng đối tượng, hãy chú ý tới ý nghĩa của 2 loại đối tượng này để có cách sử dụng phù hợp. Trong phần bàn luận ở đầu bài có nhắc tới, bạn có thể xem lại nếu quên.

 Áp dụng nguyên tắc ISP này có thể giúp tránh được những implement dư thừa, đồng thời dễ dàng mở rộng yêu cầu hơn, tuy nhiên tách các interface quá nhỏ cũng dẫn tới việc khó để kiểm soát ứng dụng, do vậy cũng nên cân nhắc cẩn thận trước khi thực hiện điều này.

Violation of ISP
Figure 2 – Phân loại rác là cần thiết, thế nhưng chia nhỏ thùng rác tới mức người ta không biết nên bỏ rác vào đâu lại là thảm hoạ 🙂

Nếu có thắc mắc hay góp ý gì thì đừng ngại comment chia sẻ nhé. Xin cảm ơn!

12 thoughts on “SOLID – Nguyên tắc 4: Chia nhỏ interface – Interface segregation principle (ISP)

  1. Pingback: [ SOLID là gì ] Tìm hiểu SOLID để trở thành Dev chất! – Webbynat

  2. Pingback: [ SOLID là gì ] Nguyên tắc 1: Đơn nhiệm – Single Responsibility principle (SRP). – Webbynat

  3. Pingback: [ SOLID là gì ] Nguyên tắc 2: Đóng và Mở – Open / Closed principle (OCP) – Webbynat

  4. Pingback: [ SOLID là gì ] Nguyên tắc 3: Tính khả dĩ thay thế – Liskov substitution principle (LSP) – Webbynat

  5. Pingback: [ SOLID là gì ] Nguyên tắc 5: Tính tương thích động – Dependency Inversion principle (DIP) – Webbynat

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s