ISP (Interface Segregation Principle) là nguyên tắc SOLID thứ tư, khẳng định: khách hàng không nên phụ thuộc vào các phương thức mà họ không sử dụng. Nguyên tắc được Robert Martin xây dựng trong bối cảnh thiết kế giao diện cho các hệ thống hướng đối tượng. Như được mô tả trong cuốn sách Clean Architecture (2017), nguyên tắc phân tách giao diện yêu cầu tạo các giao diện chuyên biệt hẹp thay vì một giao diện phổ quát duy nhất, giúp giảm sự kết nối và đơn giản hóa việc thực hiện thay đổi.
Những điểm chính
ISP (Interface Segregation Principle) là nguyên tắc phân tách giao diện cấm tạo các giao diện “dày” với các phương thức không được tất cả khách hàng sử dụng. Thay vì một giao diện với hàng tá phương thức, nhiều giao diện nhỏ được thiết kế, mỗi giao diện cho nhóm khách hàng riêng của nó.
Nguyên tắc được Robert Martin giới thiệu như một giải pháp cho vấn đề “ô nhiễm giao diện”, khi một lớp buộc phải triển khai các phương thức nó không cần chỉ vì chúng được khai báo trong một giao diện chung. Trong các ngôn ngữ kiểu tĩnh, điều này dẫn đến triển khai rỗng hoặc ném ngoại lệ — một dấu hiệu trực tiếp của vi phạm ISP.
ISP và SRP bổ sung cho nhau: SRP về trách nhiệm của lớp, ISP về hợp đồng giao diện. SRP nói “một lớp — một lý do để thay đổi”, ISP nói “một giao diện — một kịch bản khách hàng”. Cùng nhau, chúng tạo thành một kiến trúc mô-đun nơi mỗi phần tử của hệ thống có ranh giới rõ ràng.
Fat Interface — một giao diện chứa nhiều phương thức hơn một khách hàng cụ thể cần. Ví dụ, giao diện Worker với các phương thức work, eat, sleep. Một robot công nhân không nên triển khai eat và sleep, nhưng buộc phải làm vậy. Giải pháp là chia thành Workable, Eatable, Sleepable. Mỗi khách hàng nhận chính xác những gì họ cần.
Trong phát triển di động, các giao diện dày được tìm thấy trong các giao thức ủy quyền và DataSource. Một giao thức có thể chứa các phương thức cho hai kịch bản khác nhau (chỉnh sửa + hiển thị), mặc dù một màn hình cụ thể chỉ sử dụng một trong số chúng.
Triển khai ISP bắt đầu bằng việc phân tích khách hàng của mỗi giao diện. Nếu hai khách hàng sử dụng các bộ phương thức khác nhau của cùng một giao diện — giao diện đó nên được chia tách. Mỗi giao diện mới nhóm các phương thức được gọi cùng nhau trong một kịch bản.
Cơ chế chia tách: giao diện gốc được chia thành nhiều giao diện hẹp, mỗi giao diện kế thừa phần chung (nếu có). Khách hàng chuyển sang phụ thuộc vào giao diện hẹp cần thiết thay vì giao diện chung. Các lớp triển khai giao diện gốc giờ chỉ triển khai những giao diện hẹp mà chúng thực sự cần.
Một làm rõ quan trọng: mức độ chia tách được xác định bởi số lượng khách hàng và kịch bản của họ. ISP không yêu cầu chia tách tối đa (vi giao diện với một phương thức mỗi cái). Điều này sẽ dẫn đến độ phức tạp quá mức. Mục tiêu là loại bỏ sự phụ thuộc của khách hàng vào các phương thức không cần thiết, chứ không phải giảm thiểu kích thước của mỗi giao diện.
Các dấu hiệu chính của vi phạm ISP bao gồm: các lớp triển khai giao diện với phương thức rỗng (triển khai giả), ném UnsupportedOperationException trong triển khai, số lượng lớn tham số hoặc kiểu trả về không được một số khách hàng sử dụng, và thay đổi giao diện thường xuyên chỉ ảnh hưởng đến một số khách hàng.
Trong phát triển Android, một ví dụ điển hình của vi phạm ISP là giao diện OnItemClickListener, bao gồm các phương thức cho nhấp, nhấp dài và vuốt. Nếu một màn hình cụ thể chỉ sử dụng nhấp — các phương thức còn lại bị bỏ trống. Giải pháp là chia thành OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
Trong phát triển iOS, vi phạm ISP thể hiện trong các ủy quyền UIKit: một giao thức chứa các phương thức cho các trạng thái khác nhau của thành phần. UITableViewDelegate bao gồm các phương thức cho hiển thị, chọn, chỉnh sửa và hành động vuốt. Các nhà phát triển thường triển khai toàn bộ giao thức với hàng tá phương thức rỗng. Chia thành nhiều giao thức theo nhóm trách nhiệm giải quyết vấn đề.
Vấn đề không chỉ là thẩm mỹ mã. Khi một giao diện thay đổi (một phương thức mới được thêm vào), tất cả các lớp triển khai phải được cập nhật — ngay cả những lớp không cần phương thức mới. Trong phát triển di động với hàng chục màn hình, điều này dẫn đến các thay đổi dây chuyền. ISP cô lập mỗi khách hàng khỏi những thay đổi không liên quan đến họ.
Vi phạm ISP ngầm xảy ra thông qua các tham số cấu hình. Nếu một phương thức chấp nhận một đối tượng với nhiều trường và khách hàng chỉ sử dụng 2-3 trong số đó — đây là tín hiệu để chia tách. Giải pháp thay thế: nhiều phương thức chuyên biệt với bộ tham số tối thiểu.
Trong phát triển Android, ISP bị vi phạm khi sử dụng một SharedPreferencesManager duy nhất để đọc và ghi tất cả cài đặt ứng dụng. Một Fragment chỉ cần đọc chủ đề lại nhận được sự phụ thuộc vào một trình quản lý toàn cục với hàng tá phương thức cho các loại dữ liệu khác nhau. Chia thành ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — áp dụng ISP ở cấp độ dịch vụ cấu hình. Mỗi nhà cung cấp chứa chính xác các phương thức mà khách hàng của nó cần.
Hãy xem xét một ví dụ Android với giao diện làm việc với dữ liệu. Vi phạm ISP — một giao diện cho tất cả các thao tác CRUD, mặc dù không phải tất cả khách hàng đều cần tất cả các thao tác.
// Vi phạm ISP: giao diện dày
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Sau khi áp dụng ISP: các giao diện hẹp
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel không phụ thuộc vào phương thức ghi
class ReadOnlyViewModel(
private val reader: UserReader
)
Một ví dụ iOS với phân tách giao thức để làm việc với phương tiện:
// Vi phạm ISP: một giao thức cho toàn bộ công việc với phương tiện
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// Sau ISP: phân tách thành các giao thức theo trách nhiệm
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModel không phụ thuộc vào phương thức tải xuống
class PlayerViewModel {
private let player: MediaPlayer
}
Kết luận thực tế: ISP bảo vệ khách hàng khỏi các thay đổi trong các phần không liên quan của giao diện. Chia UserRepository thành UserReader và UserWriter có nghĩa là các thay đổi trong save không ảnh hưởng đến ReadOnlyViewModel và ngược lại. Mỗi khách hàng được cô lập khỏi chức năng họ không sử dụng và không yêu cầu thay đổi khi các phần khác của hệ thống được sửa đổi.
ISP và SRP — một cặp tự nhiên. SRP định nghĩa rằng một lớp nên có một lý do để thay đổi. ISP áp dụng cùng logic cho các giao diện: một giao diện nên phục vụ một kịch bản khách hàng. Một lớp có thể triển khai nhiều giao diện hẹp (mỗi giao diện tương ứng với một trách nhiệm), điều này sạch hơn so với một giao diện dày với nhiều trách nhiệm.
ISP và OCP cũng có liên quan: các giao diện hẹp dễ mở rộng hơn. Thêm một phương thức mới vào giao diện hẹp chỉ ảnh hưởng đến khách hàng của nó. Thêm một phương thức vào giao diện dày ảnh hưởng đến tất cả khách hàng — có khả năng vi phạm OCP nếu khách hàng buộc phải thay đổi triển khai của họ.
ISP và DIP làm việc cùng nhau: DIP yêu cầu sự phụ thuộc vào các trừu tượng. ISP làm cho các trừu tượng này trở nên hẹp và tập trung. Phụ thuộc vào một giao diện rộng vẫn là sự phụ thuộc vào một trừu tượng, nhưng là một trừu tượng “xấu” từ góc nhìn của ISP. Bốn nguyên tắc (SRP, OCP, ISP, DIP) tạo thành “kim tự tháp mô-đun”: SRP và ISP xác định ranh giới, OCP và DIP xác định cách mở rộng và kết nối.
Kiến trúc thành phần trong các dự án di động (mô-đun, tính năng, lớp) được hưởng lợi từ ISP ở cấp độ API công khai. Mỗi mô-đun xuất các giao diện hẹp cho người tiêu dùng của nó, thay vì một mặt tiền chung duy nhất. Điều này cho phép thay đổi triển khai nội bộ của mô-đun mà không ảnh hưởng đến người tiêu dùng chỉ sử dụng một phần chức năng của nó.
Trong các dự án Android với Clean Architecture, ISP được áp dụng cho UseCases: mỗi UseCase là một giao diện riêng biệt với một phương thức invoke hoặc execute duy nhất. Khách hàng (ViewModel) chỉ phụ thuộc vào UseCase mà nó cần, thay vì toàn bộ kho dữ liệu. Điều này làm cho các phụ thuộc trở nên minh bạch và có thể kiểm tra được.
Các câu hỏi thường gặp
Có, chia tách quá mức là có thể. ISP không yêu cầu một giao diện cho mỗi phương thức. Tiêu chí là: có khách hàng nào chỉ cần một phần phương thức của giao diện không? Nếu tất cả khách hàng sử dụng tất cả các phương thức — giao diện không cần chia tách. Mức độ chia tách tối ưu được xác định bởi các kịch bản sử dụng thực tế.
ISP ở cấp độ tham số có nghĩa: một hàm không nên chấp nhận các đối tượng với nhiều trường nếu nó chỉ sử dụng một phần trong số chúng. Thay vào đó, chỉ nên truyền dữ liệu cần thiết hoặc sử dụng các giao diện chuyên biệt (ví dụ, giao diện Renderable thay vì User đầy đủ).
LSP về kế thừa chính xác và tương thích hành vi của kiểu con. ISP về thiết kế giao diện: khách hàng không nên phụ thuộc vào các phương thức họ không sử dụng. LSP trả lời câu hỏi “có thể sử dụng lớp con thay cho lớp cơ sở không?”, ISP trả lời “khách hàng có cần toàn bộ giao diện không?”
Các giao diện hẹp đơn giản hóa việc tạo đối tượng mock: bài kiểm tra tạo một mock với một hoặc hai phương thức, không phải hàng tá. Càng ít phương thức trong một giao diện, càng dễ dàng giả lập hành vi của nó. Điều này giảm tải nhận thức cho nhà phát triển kiểm thử và giảm khả năng xảy ra lỗi trong logic mock.
Nếu giao diện ổn định và tất cả khách hàng sử dụng tất cả các phương thức — việc chia tách là dư thừa. Một ví dụ điển hình: các giao thức UIKit do Apple thiết kế. Chia chúng là rủi ro vì UIKit mong đợi triển khai đầy đủ của ủy quyền. Trong những trường hợp như vậy, vi phạm ISP được biện minh bởi sự ổn định của API.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm