ISP (Interface Segregation Principle) هو المبدأ الرابع من مبادئ SOLID، والذي ينص على: يجب ألا يعتمد العملاء على طرق لا يستخدمونها. صاغ هذا المبدأ روبرت مارتن في سياق تصميم الواجهات للأنظمة الموجهة بالكائنات. كما هو موضح في كتاب Clean Architecture (2017)، فإن مبدأ فصل الواجهات يتطلب إنشاء واجهات متخصصة ضيقة بدلاً من واجهة واحدة شاملة، مما يقلل الترابط ويبسط إجراء التغييرات.
الرئيسية
ISP (Interface Segregation Principle) هو مبدأ فصل الواجهات الذي يمنع إنشاء واجهات «سميكة» بطرق لا يستخدمها جميع العملاء. بدلاً من واجهة واحدة تحتوي على عشرات الطرق، يتم تصميم عدة واجهات صغيرة، كل منها لمجموعتها الخاصة من العملاء.
قدّم هذا المبدأ روبرت مارتن كحل لمشكلة «تلوث الواجهات»، عندما تُجبر فئة على تنفيذ طرق لا تحتاجها فقط لأنها معلنة في واجهة مشتركة. في اللغات المكتوبة بشكل ثابت، يؤدي هذا إلى تطبيقات فارغة أو إلقاء استثناءات — علامة مباشرة على انتهاك ISP.
ISP وSRP يكملان بعضهما البعض: SRP حول مسؤولية الفئة، ISP حول عقود الواجهات. يقول SRP «فئة واحدة — سبب واحد للتغيير»، ويقول ISP «واجهة واحدة — سيناريو عميل واحد». معاً يشكلان بنية معيارية حيث يكون لكل عنصر في النظام حدود واضحة.
الواجهة السميكة — واجهة تحتوي على طرق أكثر مما يحتاجه عميل معين. على سبيل المثال، واجهة Worker بطرق work وeat وsleep. يجب ألا ينفذ العامل الآلي eat وsleep، لكنه يُجبر على ذلك. الحل هو التقسيم إلى Workable وEatable وSleepable. كل عميل يحصل بالضبط على ما يحتاجه.
في تطوير التطبيقات المحمولة، توجد الواجهات السميكة في بروتوكولات المفوضين وDataSource. يمكن أن يحتوي بروتوكول واحد على طرق لسيناريوهين مختلفين (تحرير + عرض)، على الرغم من أن شاشة معينة تستخدم واحداً فقط منهما.
تنفيذ ISP يبدأ بتحليل عملاء كل واجهة. إذا استخدم عميلان مجموعتين مختلفتين من الطرق لواجهة واحدة — يجب تقسيم الواجهة. كل واجهة جديدة تجمع الطرق التي تُستدعى معاً ضمن سيناريو واحد.
آلية التقسيم: يتم تقسيم الواجهة الأصلية إلى عدة واجهات ضيقة، كل منها ترث الجزء المشترك (إن وجد). يتحول العملاء إلى الاعتماد على الواجهة الضيقة المطلوبة بدلاً من الواجهة العامة. الطبقات التي تنفذ الواجهة الأصلية الآن تنفذ فقط تلك الواجهات الضيقة التي تحتاجها فعلاً.
توضيح مهم: درجة التقسيم تحددها عدد العملاء وسيناريوهاتهم. ISP لا يتطلب أقصى تقسيم (واجهات دقيقة بطريقة واحدة لكل منها). هذا سيؤدي إلى تعقيد مفرط. الهدف هو إزالة اعتماد العملاء على الطرق غير الضرورية، وليس تقليل حجم كل واجهة.
العلامات الرئيسية لانتهاك ISP تشمل: طبقات تنفذ واجهة بطرق فارغة (تنفيذ وهمي)، إلقاء UnsupportedOperationException في التطبيقات، عدد كبير من المعاملات أو أنواع الإرجاع التي لا يستخدمها بعض العملاء، وتغييرات متكررة في الواجهة تؤثر فقط على بعض العملاء.
في تطوير Android، مثال نموذجي لانتهاك ISP هو واجهة OnItemClickListener، التي تتضمن طرقاً للنقر والنقر الطويل والتمرير. إذا كانت شاشة معينة تستخدم النقر فقط — تبقى الطرق المتبقية فارغة. الحل هو التقسيم إلى OnItemClickListener وOnItemLongClickListener وOnItemSwipeListener.
في تطوير iOS، يظهر انتهاك ISP في مفوضين UIKit: يحتوي بروتوكول واحد على طرق لحالات مختلفة للمكون. يتضمن UITableViewDelegate طرقاً للعرض والاختيار والتحرير وإجراء التمرير. غالباً ما ينفذ المطورون البروتوكول بأكمله بعشرات الطرق الفارغة. التقسيم إلى عدة بروتوكولات حسب مجموعات المسؤولية يحل المشكلة.
المشكلة ليست فقط في جماليات الكود. عندما تتغير الواجهة (تتم إضافة طريقة جديدة)، يجب تحديث جميع الطبقات المنفذة — حتى تلك التي لا تحتاج الطريقة الجديدة. في تطوير التطبيقات المحمولة مع عشرات الشاشات، يؤدي هذا إلى تغييرات متتالية. ISP يعزل كل عميل عن التغييرات التي لا تهمه.
انتهاك ISP الضمني يحدث من خلال معاملات التكوين. إذا كانت الطريقة تقبل كائناً بالعديد من الحقول، ويستخدم العميل 2-3 منها فقط — فهذه إشارة للتقسيم. البديل: عدة طرق متخصصة بمجموعة معاملات دنيا.
في تطوير Android، يتم انتهاك ISP عند استخدام SharedPreferencesManager واحد لقراءة وكتابة جميع إعدادات التطبيق. Fragment الذي يحتاج فقط لقراءة السمة يحصل على اعتماد على مدير عام بعشرات الطرق لأنواع مختلفة من البيانات. التقسيم إلى ThemePreferenceProvider وAuthPreferenceProvider وFeatureFlagProvider — تطبيق ISP على مستوى خدمات التكوين. كل مزود يحتوي بالضبط على الطرق التي يحتاجها عملاؤه.
لنفكر في مثال Android بواجهة للعمل مع البيانات. انتهاك 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 العامة. كل وحدة تصدر واجهات ضيقة لمستهلكيها، بدلاً من واجهة شاملة واحدة. هذا يسمح بتغيير التطبيق الداخلي للوحدة دون التأثير على المستهلكين الذين يستخدمون جزءاً فقط من وظائفها.
في مشاريع Android ذات Clean Architecture، يُطبق ISP على UseCases: كل UseCase هو واجهة منفصلة بطريقة invoke أو execute واحدة. العميل (ViewModel) يعتمد فقط على UseCase الذي يحتاجه، بدلاً من مستودع كامل. هذا يجعل التبعيات شفافة وقابلة للاختبار.
الأسئلة الشائعة
نعم، التقسيم المفرط ممكن. ISP لا يتطلب واجهة واحدة لكل طريقة. المعيار: هل يوجد عميل يحتاج فقط جزءاً من طرق الواجهة؟ إذا كان جميع العملاء يستخدمون جميع الطرق — لا تحتاج الواجهة للتقسيم. المستوى الأمثل للتقسيم يحدد بسيناريوهات الاستخدام الفعلية.
ISP على مستوى المعاملات يعني: لا يجب أن تقبل الدالة كائنات بعدد كبير من الحقول إذا كانت تستخدم جزءاً فقط منها. بدلاً من ذلك، يجب تمرير البيانات الضرورية فقط أو استخدام واجهات متخصصة (مثلاً، واجهة Renderable بدلاً من User كامل).
LSP حول الوراثة الصحيحة والتوافق السلوكي للأنواع الفرعية. ISP حول تصميم الواجهات: يجب ألا يعتمد العملاء على طرق لا يستخدمونها. LSP يجيب على السؤال «هل يمكن استخدام فئة فرعية بدلاً من الفئة الأساسية؟»، ISP يجيب «هل يحتاج العميل الواجهة بأكملها؟»
الواجهات الضيقة تبسط إنشاء كائنات mock: ينشئ الاختبار mock بطريقة أو طريقتين، وليس بعشرات. كلما قلت الطرق في الواجهة، كان من الأسهل محاكاة سلوكها. هذا يقلل العبء المعرفي على مطور الاختبار ويقلل احتمالية الأخطاء في منطق mock.
إذا كانت الواجهة مستقرة وجميع العملاء يستخدمون جميع الطرق — فإن التقسيم غير ضروري. مثال نموذجي: بروتوكولات UIKit التي صممتها Apple. تقسيمها محفوف بالمخاطر لأن UIKit يتوقع تنفيذ المفوض بالكامل. في مثل هذه الحالات، يُبرر انتهاك ISP باستقرار API.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.