ISP (Interface Segregation Principle) — چهارمین اصل SOLID است که بیان میکند: مشتریان نباید به روشهایی که استفاده نمیکنند وابسته باشند. این اصل توسط رابرت مارتین در زمینه طراحی رابطها برای سیستمهای شیءگرا فرموله شده است. همانطور که در کتاب Clean Architecture (2017) شرح داده شده، اصل جداسازی رابطها نیازمند ایجاد رابطهای تخصصی باریک به جای یک رابط جهانی است که وابستگی را کاهش داده و اعمال تغییرات را سادهتر میکند.
نکات اصلی
ISP (Interface Segregation Principle) — اصل جداسازی رابطها که ایجاد رابطهای «حجیم» با روشهایی که توسط همه مشتریان استفاده نمیشوند را منع میکند. به جای یک رابط با ده روش، چندین رابط کوچک طراحی میشوند که هر کدام برای گروه خاصی از مشتریان است.
این اصل توسط رابرت مارتین به عنوان راهحل مشکل «آلودگی رابطها» معرفی شد، زمانی که یک کلاس مجبور است روشهایی را پیادهسازی کند که به آنها نیاز ندارد، فقط به این دلیل که در یک رابط مشترک اعلام شدهاند. در زبانهای با تایپ ایستا، این منجر به پیادهسازیهای خالی یا پرتاب استثنا میشود — نشانه مستقیم نقض ISP.
ISP و SRP مکمل یکدیگر هستند: SRP درباره مسئولیت کلاس است، ISP درباره قراردادهای رابطها. SRP میگوید «یک کلاس — یک دلیل برای تغییر»، ISP میگوید «یک رابط — یک سناریوی مشتری». آنها با هم معماری ماژولاری را تشکیل میدهند که در آن هر عنصر سیستم مرزهای مشخصی دارد.
Fat Interface — رابطی که حاوی روشهای بیشتری نسبت به نیاز یک مشتری خاص است. به عنوان مثال، رابط Worker با روشهای work, eat, sleep. یک ربات کارگر نباید eat و sleep را پیادهسازی کند، اما مجبور است. راهحل — تقسیم به Workable, Eatable, Sleepable. هر مشتری دقیقاً آنچه نیاز دارد دریافت میکند.
در توسعه موبایل، رابطهای حجیم در پروتکلهای نماینده و DataSource یافت میشوند. یک پروتکل میتواند شامل روشهایی برای دو سناریوی مختلف (ویرایش + نمایش) باشد، در حالی که یک صفحه خاص تنها از یکی از آنها استفاده میکند.
پیادهسازی ISP با تحلیل مشتریان هر رابط آغاز میشود. اگر دو مشتری از مجموعه روشهای متفاوتی از یک رابط استفاده میکنند — رابط باید تقسیم شود. هر رابط جدید روشهایی را گروهبندی میکند که در یک سناریو با هم فراخوانی میشوند.
مکانیسم تقسیم: رابط اصلی به چندین رابط باریک تقسیم میشود که هر کدام بخش مشترک (در صورت وجود) را به ارث میبرند. مشتریان به وابستگی به رابط باریک مورد نیاز خود به جای رابط عمومی تغییر میکنند. کلاسهایی که رابط اصلی را پیادهسازی میکردند، اکنون فقط آن رابطهای باریکی را پیادهسازی میکنند که واقعاً به آنها نیاز دارند.
نکته مهم: درجه تقسیم با تعداد مشتریان و سناریوهای آنها تعیین میشود. ISP حداکثر تقسیم (ریز رابطهای تکمتدی) را الزامی نمیکند. این منجر به پیچیدگی بیش از حد میشود. هدف — حذف وابستگی مشتریان به روشهای غیرضروری است، نه به حداقل رساندن اندازه هر رابط.
نشانههای اصلی نقض ISP شامل موارد زیر است: کلاسهایی که رابط را با روشهای خالی پیادهسازی میکنند (پیادهسازی ساختگی)، پرتاب استثنای UnsupportedOperationException در پیادهسازیها، تعداد زیاد پارامترها یا انواع بازگشتی که توسط بخشی از مشتریان استفاده نمیشوند، و تغییرات مکرر رابط که تنها بر بخشی از مشتریان تأثیر میگذارد.
در توسعه اندروید، نمونه معمول نقض ISP — رابط OnItemClickListener که شامل روشهایی برای کلیک، کلیک طولانی و سوایپ است. اگر یک صفحه خاص فقط از کلیک استفاده کند — روشهای باقیمانده خالی میمانند. راهحل — تقسیم به OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
در توسعه iOS، نقض ISP در نمایندگان UIKit ظاهر میشود: یک پروتکل شامل روشهایی برای حالتهای مختلف یک کامپوننت است. UITableViewDelegate شامل روشهایی برای نمایش، انتخاب، ویرایش و عملیات سوایپ است. اغلب توسعهدهندگان کل پروتکل را با ده روش خالی پیادهسازی میکنند. تقسیم به چندین پروتکل بر اساس گروههای مسئولیت مشکل را حل میکند.
مشکل تنها در زیباییشناسی کد نیست. هنگامی که یک رابط تغییر میکند (یک روش جدید اضافه میشود)، تمام کلاسهای پیادهساز باید بهروز شوند — حتی آنهایی که به روش جدید نیاز ندارند. در توسعه موبایل با دهها صفحه، این منجر به تغییرات آبشاری میشود. ISP هر مشتری را از تغییراتی که به او مربوط نیست جدا میکند.
نقض تلویحی ISP از طریق پارامترهای پیکربندی رخ میدهد. اگر یک روش یک شی با تعداد زیادی فیلد دریافت کند، در حالی که مشتری فقط از 2-3 تای آنها استفاده میکند — این سیگنالی برای تقسیم است. جایگزین: چندین روش تخصصی با حداقل مجموعه پارامترها.
در توسعه اندروید، ISP هنگام استفاده از یک SharedPreferencesManager برای خواندن و نوشتن تمام تنظیمات برنامه نقض میشود. Fragment که فقط نیاز به خواندن تم دارد، وابستگی به یک مدیر جهانی با دهها روش برای انواع مختلف داده دریافت میکند. تقسیم به ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — کاربرد ISP در سطح سرویسهای پیکربندی. هر ارائهدهنده دقیقاً روشهایی را که مشتریانش نیاز دارند شامل میشود.
بیایید نمونه اندروید را با یک رابط برای کار با داده بررسی کنیم. نقض ISP — یک رابط برای تمام عملیات CRUD، در حالی که همه مشتریان به تمام عملیات نیاز ندارند.
// نقض ISP: رابط حجیم
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// پس از اعمال ISP: رابطهای باریک
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel به روشهای نوشتن وابسته نیست
class ReadOnlyViewModel(
private val reader: UserReader
)
نمونه iOS با تقسیم پروتکلها برای کار با رسانه:
// نقض ISP: یک پروتکل برای کار با رسانه
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// پس از ISP: تقسیم به پروتکلهای مبنی بر مسئولیت
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 به روشهای بارگیری وابسته نیست
class PlayerViewModel {
private let player: MediaPlayer
}
نتیجه عملی: ISP از مشتریان در برابر تغییرات در بخشهای غیرمرتبط رابط محافظت میکند. تقسیم UserRepository به UserReader و UserWriter به این معنی است که تغییرات در save بر ReadOnlyViewModel تأثیر نمیگذارد و بالعکس. هر مشتری از عملکردی که استفاده نمیکند جدا شده و هنگام بهبود سایر بخشهای سیستم نیازی به تغییر ندارد.
ISP و SRP — یک جفت طبیعی. SRP تعیین میکند که یک کلاس باید یک دلیل برای تغییر داشته باشد. ISP همان منطق را به رابطها اعمال میکند: یک رابط باید یک سناریوی مشتری را خدمترسانی کند. یک کلاس میتواند چندین رابط باریک را پیادهسازی کند (هر کدام مربوط به یک مسئولیت است) که از یک رابط حجیم با چندین مسئولیت تمیزتر است.
ISP و OCP نیز مرتبط هستند: گسترش رابطهای باریک آسانتر است. افزودن یک روش جدید به یک رابط باریک فقط بر مشتریان آن تأثیر میگذارد. افزودن روش به یک رابط حجیم بر همه مشتریان تأثیر میگذارد — به طور بالقوه OCP را نقض میکند اگر مشتریان مجبور به تغییر پیادهسازی خود شوند.
ISP و DIP با هم کار میکنند: DIP نیازمند وابستگی به انتزاعهاست. ISP این انتزاعها را باریک و متمرکز میکند. وابستگی به یک رابط گسترده هنوز وابستگی به انتزاع است، اما یک انتزاع «بد» از دیدگاه ISP. چهار اصل (SRP, OCP, ISP, DIP) «هرم ماژولاریتی» را تشکیل میدهند: SRP و ISP مرزها را تعیین میکنند، OCP و DIP — روشهای گسترش و اتصال.
معماری کامپوننتی در پروژههای موبایل (ماژولها، ویژگیها، لایهها) از ISP در سطح API عمومی بهره میبرد. هر ماژول به جای یک نمای کلی، رابطهای باریکی را برای مصرفکنندگان خود صادر میکند. این امکان تغییر پیادهسازی داخلی ماژول را بدون تأثیر بر مصرفکنندگانی که فقط از بخشی از عملکرد آن استفاده میکنند، فراهم میکند.
در پروژههای اندروید با Clean Architecture، ISP در UseCase اعمال میشود: هر UseCase یک رابط جداگانه با یک روش invoke یا execute است. مشتری (ViewModel) فقط به UseCase مورد نیاز خود وابسته است، نه به کل مخزن. این وابستگیها را شفاف و قابل آزمایش میکند.
سوالات متداول
بله، تقسیم بیش از حد ممکن است. ISP یک رابط برای هر روش را الزامی نمیکند. معیار: آیا مشتریای وجود دارد که فقط به بخشی از روشهای رابط نیاز داشته باشد؟ اگر همه مشتریان از همه روشها استفاده میکنند — رابط نیازی به تقسیم ندارد. سطح بهینه تقسیم توسط سناریوهای واقعی استفاده تعیین میشود.
ISP در سطح پارامترها به این معنی است: یک تابع نباید اشیایی با تعداد زیادی فیلد دریافت کند اگر فقط از بخشی از آنها استفاده میکند. در عوض، باید فقط دادههای لازم را منتقل کرد یا از رابطهای تخصصی استفاده کرد (مثلاً رابط Renderable به جای User کامل).
LSP درباره وراثت صحیح و سازگاری رفتاری زیرنوعهاست. ISP درباره طراحی رابطهاست: مشتریان نباید به روشهایی که استفاده نمیکنند وابسته باشند. LSP به این سوال پاسخ میدهد «آیا میتوان از زیرکلاس به جای کلاس پایه استفاده کرد؟»، ISP — «آیا مشتری به کل رابط نیاز دارد؟».
رابطهای باریک ایجاد اشیاء ساختگی (mock) را سادهتر میکنند: آزمایش یک mock با یک یا دو روش ایجاد میکند، نه با ده روش. هر چه روشهای کمتری در رابط باشد، شبیهسازی رفتار آن آسانتر است. این بار شناختی توسعهدهنده آزمایش را کاهش داده و احتمال خطا در منطق mock را کاهش میدهد.
اگر رابط پایدار است و همه مشتریان از همه روشها استفاده میکنند — تقسیم اضافی است. نمونه معمول: پروتکلهای UIKit که توسط Apple طراحی شدهاند. تقسیم آنها مخاطرهآمیز است زیرا UIKit انتظار پیادهسازی کامل نماینده را دارد. در چنین مواردی، نقض ISP با پایداری API توجیه میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید