Giới thiệu
Đây là đây là bài viết thứ tư trong series “SOLID cho thanh niên code cứng”. Ở bài viết này, mình sẽ nói về Interface Segregation Principle – Nguyên lý phân tách interface.
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Nội dung nguyên lý:
Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể
Giải thích nguyên lý
Trước khi giải thích nguyên lý, mình nhắc lại khái niệm interface một chút cho các bạn mất căn bản nhé.
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 các phương thức đã khai báo ở lớp interface.
(Xem thêm tại: http://viecbonus.com/blog/interface-va-asbtract-class-trong-php-la-gi/)
Để thiết kế một hệ thống linh hoạt, dễ thay đổi, các module của hệ thống nên giao tiếp với nhau thông qua interface. Mỗi module sẽ gọi chức năng của module khác thông qua interface mà không cần quan tâm tới implementation bên dưới. Như đã nói ở trên, do interface chỉ chứa khai báo rỗng về method, khi một class implement một interface, class đó phải implement toàn bộ các method được khai báo trong interface đó.
Điều này tương đương với việc nếu ta tạo ra 1 interface bự (hơn 100 method chẳng hạn), mỗi class sẽ phải implement toàn bộ 100 method đó, kể những method không bao giờ sử dụng đến. Nếu áp dụng ISP, ta sẽ chia interface này ra thành nhiều interface nhỏ, các class chỉ cần implement những interface có chức năng mà chúng cần, không cần phải implement những chức năng thừa nữa.
Ví dụ minh họa
Đây là nguyên lý dễ hiểu nhất trong SOLID, các bạn chỉ đọc code một chút là hiểu ngay! Giả sử ta muốn viết một chương trình giới thiệu thuộc tính của các loài động vật. Động vật nào cũng có thể ăn, uống, ngủ, ta thiết kế interface IAnimal như sau:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IAnimal { | |
void Eat(); | |
void Drink(); | |
void Sleep(); | |
} | |
public class Dog : IAnimal { | |
public void Eat () {} | |
public void Drink () {} | |
public void Sleep () {} | |
} | |
public class Cat : IAnimal { | |
public void Eat () {} | |
public void Drink () {} | |
public void Sleep () {} | |
} |
Khi ta muốn thêm 1 số loài động vật mới và tính năng vào, ta phải thêm có thêm method vào trong interface như: bơi lội, bay, săn mồi, … Điều này làm interface phình to ra. Khi một loài động vật kế thừa interface, nó phải implement luôn cả những hàm không dùng đến.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IAnimal { | |
void Eat(); | |
... | |
void Swim(); | |
void Fly(); | |
} | |
public class Fish : IAnimal { | |
public void Eat () {} | |
... | |
public void Swim() {} | |
public void Fly() { throw new Exception("Cá éo biết bay"); } | |
} | |
public class Bird : IAnimal { | |
public void Eat () {} | |
... | |
public void Swim() { throw new Exception("Chim éo biết bơi"); } | |
public void Fly() | |
} |
Trong thực tế cũng vậy, khi cần thêm chức năng mới, ta thường thêm method vào interface, làm cho interface ngày càng phình to ra. Khi thêm method vào interface IAnimal, những class cũ như Dog, Cat đều phải implement những method mới nên mất thời gian. Giải pháp trong tình huống này là tách interface IAnimal ra thành các interface nhỏ như sau:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IAnimal { | |
void Eat(); | |
void Drink(); | |
void Sleep(); | |
} | |
public interface IBird { | |
void Fly(); | |
} | |
public interface IFish { | |
void Swim(); | |
} | |
// Các class chỉ cần kế thừa những interface có chúc năng chúng cần | |
public class Dog : IAnimal { | |
public void Eat() {} | |
public void Drink() {} | |
public void Sleep() {} | |
} | |
public class FlappyBird: IAnimal, IBird { | |
public void Eat() {} | |
public void Drink() {} | |
public void Sleep() {} | |
public void Fly() {} | |
} | |
public class FormosaFish: IAnimal, IBird { | |
public void Eat() {} | |
public void Drink() {} | |
public void Sleep() {} | |
public void Swim() {} | |
} |
Để có thể phân tách một interface lớn thành các interface nhỏ một cách hợp lý, các bạn nên xem lại bài viết đầu tiên của series về Single Responsibility Principle. Tuy nhiên, đôi khi việc tách ra nhiều interface có thể làm tăng số lượng interface, tăng số lượng class, ta cần cân nhắc lợi hại trước khi áp dụng nhé.
Lưu ý và kết luận
Áp dụng nguyên lý ISP sẽ làm hệ thống linh hoạt hơn, đồng thời giảm thiểu code thừa (do phải implement những tính năng không cần thiết). Tuy nhiên, trong thực tế vẫn có nhiều trường hợp bất khả kháng mà chúng ta phải tạo 1 interface lớn. Ví dụ: Trong một ứng dụng ASP.NET, nếu muốn implement chức năng đăng nhập/phân quyền cho user, ta phải implement iinterface MembershipProvider. Interface này khá là bự với hơn 27 method (Giờ có khi còn nhiều hơn).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public override bool ChangePassword(string username, string oldPassword, string newPassword) | |
public override bool DeleteUser(string username, bool deleteAllRelatedData) | |
public override bool EnablePasswordRetrieval() | |
public override int MinRequiredPasswordLength() | |
// Thực sự ta chỉ cần hàm này | |
public override bool ValidateUser(string username, string password) |
Có lẽ mục đích của Microsoft khi design interface này là để cover toàn bộ các chức năng cần có của việc đăng nhập/phân quyền. Tuy nhiên, do interface này lớn, nếu ta chỉ muốn làm phân quyền đơn giản thì việc implement 27 method này là hoàn toàn mất thời gian và không cần thiết.
Ở bài viết sau, mình sẽ giới thiệu về Dependency Inversion Principle – nguyên lý cuối cùng trong SOLID. Có thắc mắc hay góp ý gì các bạn cứ thoải mái nêu ra trong phần comment, mình sẽ cố gắng giải đáp.
Một số tài liệu để tham khảo thêm:
- http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
- http://code.tutsplus.com/tutorials/solid-part-3-liskov-substitution-interface-segregation-principles–net-36710
- http://www.codeproject.com/Tips/766045/Interface-Segregation-Principle-ISP-of-SOLID-in-Cs
- http://prasadhonrao.com/solid-principles-interface-segregation-principle-isp/
Dường như anh bị nhầm rồi kìa. Lớp FlappyBird và lớp FormosaFish đều thực thi 2 interface thì phải.
public class FlappyBird: IAnimal, IBird
{}
và
public class FormosaFish: IAnimal, IFish
{}
LikeLiked by 3 people
1 class imp được 2 interface hả bạn 😀 trước giờ mình toàn khai báo chỉ có 1 implement, khai báo 2 thì nó thông báo err
LikeLike
một class nhiều interface thoải mái, làm sao mà lỗi được hả bạn 😀
LikeLiked by 1 person
Cẩn thận bạn nhầm giữa implement interface và inherit class. Vì C# cả 2 thằng này đều là sau dấu “:”.
LikeLiked by 1 person
1 class có thể implement multi interface được nhưng chỉ có thể kế thừa
1 class thôi bạn ơi
LikeLike
Interface thoải mái, chỉ ko đã kế thừa đc thôi
LikeLike
Mình thắc mắc lúc thực thi class thì sao. Chẳng hạn mình muốn vừa gọi được phương thức Sleep() và Fly() thì khai báo kiểu gì. IAnimal * flappyBird = FlappyBird(); Hay IBird * flappyBird = FlappyBird(); đều không được
LikeLike
nên để interface bird extend interface animal . lúc dùng thì dùng bird là dc.
LikeLike
Anh nên cho interface IBird và Ifish extend interface IAnimal thì mới đúng OOP. Khi class FormosaFish implement IFish thì nó đã có sẵn phương thức của IAnimal và IFish, không cần implement 2 cái interface như trên: 1 cái là tổng quát của cái còn lại.
LikeLike
Nếu cá rô bốt thì sao kế thừa được bạn
LikeLike