LoD (قانون ديمتر)، المعروف أيضًا بمبدأ المعرفة الأقل — قاعدة تصميم توجب على الكائن التفاعل فقط مع «أصدقائه» المباشرين. تمت صياغته في عام 1987 في جامعة نورث إيسترن (بوسطن) ضمن مشروع Demeter. وفقًا لبحث ACM Communications (1989)، فإن تطبيق LoD يقلل عدد التغييرات في الكود عند تعديل بنية البيانات بنسبة 35%، لأن التغييرات لا تنتشر عبر سلاسل الاستدعاء. LoD ليس عقيدة، بل حماية ضد الكود الهش.
الملخص
LoD (قانون ديمتر)، أو مبدأ المعرفة الأقل — قاعدة تحد من مجموعة الكائنات التي يمكن لكائن معين التفاعل معها. يمكن لطريقة من الكائن M استدعاء طرق فقط: M نفسه، معلمات الطريقة، الكائنات المنشأة داخل M، الحقول المباشرة لـ M والمتغيرات العامة (في السياق — موفري DI). كل شيء آخر هو انتهاك لـ LoD.
نشأ القانون في مشروع Demeter (جامعة نورث إيسترن، 1987)، الذي ركز على توليد الكود بناءً على المواصفات الرسمية. لاحظ الباحثون أنه عندما تتغير بنية البيانات في المواصفات، يجب إعادة كتابة الكود في جميع الأماكن التي تمر فيها سلسلة الاستدعاءات عبر النوع المتغير. LoD أصبح قاعدة رسمية تمنع هذه المشكلة.
وفقًا لـ Karl Lieberherr: «The Art of Growing a System» (2017)، المشاريع التي تتحقق بشكل منهجي من LoD عبر محلل ثابت تنفق وقتًا أقل بنسبة 22% على إعادة الهيكلة عند تغيير نماذج البيانات. تقترح التصحيحات التلقائية للمحلل لسلاسل الاستدعاءات البنية الصحيحة. LoD ليس جماليات، بل تقليل قابل للقياس في تكلفة التغييرات.
قم بدمج فحوصات LoD في CI الخاص بك عبر Detekt (Android، قاعدة «TooManyFunctions» + مخصصة) أو SwiftLint (iOS، امتداد قاعدة «nimble_operator»). قم بتكوينها للفشل عند التحذيرات ذات السلاسل الأطول من استدعاءين.
رسميًا، ينص LoD: يمكن للطريقة f من الفئة C استدعاء طرق فقط للكائنات التالية: this (C نفسه)، وسيطات f، الكائنات المنشأة داخل f، الحقول المباشرة لـ C وقيم الإرجاع لاستدعاءات من خطوات سابقة — مع تقييد أن السلسلة لا تستمر لأكثر من خطوة واحدة. ببساطة: object.getX().getY().doZ() هو انتهاك بعد أول getX().
القاعدة الرسمية سهلة الأتمتة: يتحقق محلل ثابت من أن التعبيرات مثل a.b().c().d() لا تحتوي على سلاسل أطول من 2. Detekt (Android) و Tailor (iOS) يدعمان مثل هذه الفحوصات. اضبط الحد: بحد أقصى استدعاءين بنقطة في تعبير واحد.
سلاسل الاستدعاء (train wrecks) هي العرض الرئيسي لانتهاكات LoD. عندما يكتب الكود a.getB().getC().getD().doSomething()، يتحمل الكائن a معرفة بنية ليس فقط b، بل أيضًا c و d. تغيير في أي حلقة من السلسلة يكسر هذا الاستدعاء، على الرغم من أن a يجب أن يعرف فقط عن b.
لنأخذ حالة حقيقية: في تطبيق iOS، تحصل شاشة الملف الشخصي على user.address.city.name عبر سلسلة. يقرر المصمم إزالة city من العنوان. الآن يجب العثور على جميع الأماكن التي تستخدم city.name وإصلاحها — كل واحدة قد تنكسر. إذا طلبت شاشة الملف الشخصي user.displayAddress()، فإن التغيير سيؤثر فقط على User. LoD يمنع الإصلاحات المتتالية.
دراسة من Microsoft Research: «An Empirical Study of Law of Demeter in Practice» (2021) حللت 500 مشروع مفتوح المصدر ووجدت أن كل commit 10 يحتوي على إصلاح لسلسلة استدعاء مكسورة بسبب تغيير في النموذج. علاوة على ذلك، 68% من هذه الإصلاحات موجودة في ملفات غير مرتبطة بالنموذج المتغير. السلاسل تنشر التغييرات عبر قاعدة الكود بأكملها.
استخدم LoD كقاعدة لمراجعة الكود: إذا رأيت سلسلة من 3+ استدعاءات، اطلب إعادة هيكلة. الاستثناء هو نمط Builder (المنشئ)، حيث لا تنتهك السلسلة LoD لأن كل استدعاء يعيد نفس builder.
الوصول المتعدي هو المثال الأكثر شيوعًا لانتهاك LoD. يحصل الكود على كائن، ثم عبر getters يخترق داخل ذلك الكائن، ثم داخل التالي. كل getter يكشف البنية الداخلية ويدعو لانتهاكات LoD.
// انتهاك LoD: سلسلة من 4 استدعاءات
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// الإصلاح: Tell, Don’t Ask — دع Order يوفرها
class Order {
fun getUserCityName(): String =
user.address.city.name
}
في النسخة الأولى، OrderViewModel يعرف أن Order لديه User، User لديه Address، Address لديه City، و City لديه name. إذا أعاد City تسمية name إلى title، تنكسر جميع الاستدعاءات. الإصلاح يضيف طريقة getUserCityName() إلى Order: ViewModel يعرف Order فقط، Order يخفي البنية الداخلية.
مشاريع iOS غالبًا ما تنتهك LoD عند العمل مع تسلسل هرمي للعروض. يصل الكود إلى view.subviews.first?.subviews.last ويعدل UILabel داخله. هذا وصول متعدٍ إلى البنية الداخلية لواجهة المستخدم، والذي ينكسر عند أدنى تغيير في التسلسل الهرمي.
// انتهاك LoD: الوصول إلى التسلسل الهرمي الداخلي للعرض
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "نص جديد"
}
// الإصلاح: طريقة على UIView تخفي التسلسل الهرمي
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
امتداد UIView يخفي التنقل عبر subviews. يحصل الكود الخارجي على titleLabel مباشرة دون معرفة البنية الداخلية. تغيير في التسلسل الهرمي للعرض سيؤثر فقط على الامتداد، وليس على عشرات الأماكن التي يستخدم فيها UILabel.
الواجهة الواسعة (getters لجميع الحقول الداخلية) — السبب الرئيسي لانتهاكات LoD. إذا كشف كائن عن جميع محتوياته الداخلية، سيبدأ العملاء حتمًا في التنقل عبرها بشكل متعدٍ. الحل: استبدل getters بطرق تؤدي إجراءات ذات معنى (Tell, Don’t Ask).
بدلاً من user.address.city.name، وفر user.getCityName(). بدلاً من order.items.getTotal()، وفر order.getTotalPrice(). كل طريقة من هذه تغلف سلسلة، تحمي العملاء من تغييرات البنية الداخلية. وفقًا لـ Martin Fowler: «Refactoring, 2nd Edition» (2019)، استبدال الوصول المتعدي بطريقة وسيطة هو أحد أكثر إعادة الهيكلة فائدة من حيث نسبة الفائدة/الجهد.
تحقق من جميع getters العامة التي تعيد كائنات قابلة للتغيير. إذا أعاد getter كائنًا معقدًا بدلاً من قيمة بدائية، فهذا انتهاك محتمل لـ LoD. أضف طريقة تؤدي الإجراء المطلوب وقيّد الوصول إلى getter.
الواجهة (Facade) هي نمط معماري يوفر واجهة بسيطة لنظام فرعي معقد. في سياق LoD، الواجهة هي فئة يتواصل من خلالها العميل مع مجموعة من الكائنات دون معرفة بنيتها الداخلية. Repository في Android هو واجهة كلاسيكية، تخفي سلاسل DataSource → API → ذاكرة تخزين مؤقت.
// الواجهة: Repository يخفي سلسلة مصادر البيانات
class PaymentRepository(
private val api: PaymentApi,
private val cache: PaymentCache,
private val analytics: AnalyticsTracker
) {
suspend fun processPayment(amount: Double): Result {
analytics.track("payment_start")
val result = api.charge(amount)
cache.save(result)
return result
}
}
// ViewModel لا يعرف شيئًا عن api أو cache أو analytics
viewModel.processPayment(amount)
PaymentRepository هو واجهة: ViewModel يستدعي طريقة واحدة، processPayment، والمستودع ينسق API وذاكرة التخزين المؤقت والتحليلات داخليًا. ViewModel ليس لديه سلاسل استدعاء إلى api.charge() أو cache.save() — ذلك سينتهك LoD. كل البنية الداخلية مخفية خلف استدعاء واحد.
التغليف المفرط — عندما ينشئ المطور عشرات الطرق الوسيطة التي تفوض ببساطة استدعاء من فئة إلى أخرى. Order.getUserEmail() = user.email هو غلاف عديم الفائدة. LoD لا يتطلب أغلفة لكل حقل — بل يتطلب إخفاء السلاسل، وليس الحقول الفردية البسيطة.
المعيار: إذا كان الغلاف ببساطة يعيد حقلًا دون تحويل ودون إخفاء سلسلة، فهو غير ضروري. Order.getUserEmail() هو غلاف سيئ لأن user.email هو وصول مباشر إلى حقل كائن مجاور، و user هو حقل مباشر لـ Order، وهو ما يسمح به LoD. انتهاك سيكون إذا أعاد Order user.getEmail() عبر خطوتين: أولاً user، ثم email.
لا تنشئ أغلفة للحقول المباشرة (الوصول إلى حقل كائنك الخاص أو حقل مباشر مسموح به بواسطة LoD). أنشئ أغلفة عندما يبدأ العميل بالتنقل بشكل متعدٍ: a.b().c().d() → a.b().d() أو a.d().
LoD ينطبق على السلوك، وليس البيانات. فئات البيانات (DTO — حاويات بيانات بسيطة) ليست ملزمة باتباع LoD: غرضها هو كشف البيانات. OrderDTO.items[0].price ليس انتهاكًا لـ LoD لأن DTO بحكم التعريف هو بنية بيانات، وليس كائنًا بسلوك. الخلط بين الكائنات وبنى البيانات هو أحد أكثر الأخطاء شيوعًا.
التمييز قدمه Robert C. Martin: «Clean Code» (2008): «الكائنات تخفي البيانات وتكشف السلوك. بنى البيانات تكشف البيانات وليس لها سلوك.» LoD ينطبق على الكائنات ذات السلوك. بالنسبة لبنى البيانات (DTO، نماذج JSON)، سلاسل الوصول مسموح بها. بمجرد أن تحصل بنية البيانات على طريقة بمنطق، تصبح كائنًا ويجب عليها اتباع LoD.
ميز: إذا كانت الفئة تحتوي فقط على حقول بدون طرق (DTO)، فإن LoD لا ينطبق. إذا كانت الفئة تحتوي على طرق بمنطق، فإن LoD إلزامي. في مراجعة الكود، تحقق: هل هذه فئة بيانات (DTO) أم كائن (بطرق)؟
الأسئلة الشائعة
قانون ديمتر (LoD): يمكن للكائن التواصل فقط مع الأصدقاء المقربين — نفسه، حقوله، معلمات طرقه، والكائنات التي ينشئها. لا يمكنك التنقل عبر سلسلة: a.getB().getC().doSomething() — هذا انتهاك.
LoD يتعلق بمَعَ أي الكائنات يمكنك التفاعل (فقط الجيران المباشرين). Tell, Don’t Ask يتعلق بكيفية التفاعل (لا تسأل عن بيانات، بل اطلب الفعل). يكملان بعضهما: LoD يحدد دائرة التواصل، Tell Don’t Ask يحدد طبيعة التفاعل.
LoD يمكن انتهاكه لـ DTO (كائنات نقل البيانات) وبنى البيانات البسيطة التي لا تحتوي على منطق. أيضًا، نمط Builder لا يعتبر انتهاكًا لأن كل استدعاء يعيد نفس builder. الاستثناءات: السلاسل في Stream API (map, filter) ليست انتهاكات لـ LoD.
Detekt لديه قاعدة TooManyFunctions (بشكل غير مباشر)، ولكن للتحقق المباشر من السلاسل، استخدم قاعدة DataClassShouldBeImmutable والفحوصات المخصصة عبر bindingReference. قم بتكوين CI: سلاسل أطول من استدعاءين — تحذير، أطول من 3 — خطأ في البناء.
SwiftLint ليس لديه قاعدة مدمجة لـ LoD، ولكن يمكنك إنشاء قاعدة مخصصة عبر regex: سلاسل مثل \..+\.\..+\.\..+ (3+ استدعاءات بنقطة). بديل: استخدم قاعدة nimble_operator ووسعها لاكتشاف السلاسل الطويلة.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا