LoD (Law of Demeter)، همچنین به عنوان اصل کمترین آگاهی شناخته میشود — قاعده طراحی که به شی دستور میدهد فقط با «دوستان» مستقیم تعامل کند. در سال ۱۹۸۷ در دانشگاه نورثایسترن (بوستون) در چارچوب پروژه Demeter فرموله شد. طبق تحقیق ACM Communications (1989)، استفاده از LoD تعداد تغییرات در کد هنگام تغییر ساختار داده را ۳۵٪ کاهش میدهد، زیرا تغییرات در طول زنجیرههای فراخوانی منتشر نمیشوند. LoD — جزم نیست، بلکه محافظی در برابر کد شکننده است.
نکات اصلی
LoD (Law of Demeter) یا اصل کمترین آگاهی — قاعدهای که دایره اشیایی را که یک شی خاص میتواند با آنها تعامل کند محدود میکند. متد شی M میتواند فقط متدهای موارد زیر را فراخوانی کند: خود M، پارامترهای متد، اشیای ایجاد شده در داخل M، فیلدهای مستقیم M و متغیرهای سراسری (در زمینه — ارائهدهندگان DI). بقیه موارد — نقض LoD است.
قانون در پروژه Demeter (دانشگاه نورثایسترن، ۱۹۸۷) که به تولید کد بر اساس مشخصات رسمی میپرداخت، پدید آمد. محققان متوجه شدند: وقتی ساختار داده در مشخصات تغییر میکرد، باید کد را در همه جاهایی که زنجیره دسترسی از نوع تغییر یافته عبور میکرد، بازنویسی میکردند. LoD به یک قاعده رسمی برای جلوگیری از این مشکل تبدیل شد.
طبق Karl Lieberherr: «The Art of Growing a System» (2017)، پروژههایی که به طور سیستماتیک LoD را از طریق تحلیلگر ایستا بررسی میکنند، ۲۲٪ زمان کمتری برای بازسازی هنگام تغییر مدلهای داده صرف میکنند. تحلیلگر auto-fix زنجیرههای فراخوانی معماری صحیح را پیشنهاد میکند. 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() زنجیرههای طولانیتر از ۲ وجود نداشته باشد. Detekt (Android) و Tailor (iOS) چنین بررسیهایی را پشتیبانی میکنند. آستانه را تنظیم کنید: حداکثر ۲ فراخوانی از طریق نقطه در یک عبارت.
زنجیرههای فراخوانی (chain calls, 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) ۵۰۰ پروژه منبعباز را تحلیل کرد و نشان داد: در هر دهمین commit اصلاح زنجیره فراخوانی شکسته شده به دلیل تغییر مدل رخ میدهد. در عین حال ۶۸٪ از این اصلاحات — در فایلهای غیرمرتبط با مدل تغییر یافته است. زنجیرهها تغییرات را در سراسر پایگاه کد منتشر میکنند.
از LoD به عنوان قانون بازبینی کد استفاده کنید: اگر زنجیره ۳+ فراخوانی میبینید — بازسازی را درخواست کنید. استثنا — Builder (سازنده)، که در آن زنجیره LoD را نقض نمیکند، زیرا هر فراخوانی همان builder را برمیگرداند.
دسترسی گذری — رایجترین مثال نقض LoD. کد شی را دریافت میکند، سپس از طریق getterها به داخل این شی نفوذ میکند، سپس به داخل شی بعدی. هر getter ساختار داخلی را آشکار میکند و به نقض LoD دعوت میکند.
// نقض LoD: زنجیره ۴ فراخوانی
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 اغلب هنگام کار با سلسلهمراتب view LoD را نقض میکنند. کد به view.subviews.first?.subviews.last دسترسی پیدا میکند و UILabel داخل را تغییر میدهد. این — دسترسی گذری به ساختار داخلی UI است که با کوچکترین تغییر سلسلهمراتب میشکند.
// نقض LoD: دسترسی به سلسلهمراتب داخلی view
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 را مستقیماً دریافت میکند، بدون دانستن ساختار داخلی. تغییر سلسلهمراتب view فقط افزونه را تحت تأثیر قرار میدهد، نه دهها جایی که از این UILabel استفاده میشود.
رابط گسترده (getter برای تمام فیلدهای داخلی) — دلیل اصلی نقض LoD. اگر شی تمام درون خود را آشکار کند، مشتریان ناگزیر شروع به حرکت گذری روی آنها میکنند. راهحل: getterها را با متدهایی که اقدامات معنادار انجام میدهند جایگزین کنید (Tell, Don't Ask).
به جای user.address.city.name، user.getCityName() را ارائه دهید. به جای order.items.getTotal()، order.getTotalPrice() را ارائه دهید. هر چنین متدی — محصورسازی زنجیرهای است که مشتریان را از تغییرات ساختار داخلی محافظت میکند. طبق Martin Fowler: «Refactoring, 2nd Edition» (2019)، جایگزینی دسترسی گذری با متد واسطه — یکی از مفیدترین بازسازیها از نظر نسبت سود به تلاش است.
تمامی getterهای عمومی که اشیای تغییرپذیر برمیگردانند را بررسی کنید. اگر getter یک شی پیچیده برمیگرداند نه یک نوع اولیه — این یک نقض بالقوه LoD است. متدی را اضافه کنید که اقدام مورد نیاز را انجام دهد و دسترسی به getter را محدود کنید.
facade (Facade) — الگوی معماری که یک رابط ساده به یک زیرسیستم پیچیده ارائه میدهد. در زمینه LoD، Facade کلاسی است که مشتری از طریق آن با گروهی از اشیا ارتباط برقرار میکند بدون اینکه ساختار داخلی آنها را بداند. Repository در Android — Facade کلاسیک که زنجیره DataSource → API → cache را پنهان میکند.
// Facade: 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 — Facade: ViewModel یک متد processPayment را فراخوانی میکند و مخزن API، cache و تحلیل را در درون خود هماهنگ میکند. ViewModel زنجیرههای فراخوانی به api.charge() یا cache.save() ندارد — این LoD را نقض میکرد. تمام ساختار داخلی پشت یک فراخوانی پنهان شده است.
Wrapperهای اضافی — زمانی که برنامهنویس دهها متد واسطه ایجاد میکند که صرفاً فراخوانی را از یک کلاس به کلاس دیگر منتقل میکنند. Order.getUserEmail() = user.email — wrapper بیفایده. LoD برای هر فیلدی wrapper نمیخواهد — میخواهد زنجیرهها را پنهان کند، نه فیلدهای ساده جداگانه را.
معیار: اگر wrapper صرفاً یک فیلد را بدون تبدیل و بدون پنهانسازی زنجیره برمیگرداند — لازم نیست. Order.getUserEmail() — wrapper بد، زیرا user.email دسترسی مستقیم به فیلد شی همسایه است و user فیلد مستقیم Order است که توسط LoD مجاز است. نقض زمانی بود که Order user.getEmail() را در دو مرحله برمیگرداند: اول user، سپس email.
برای فیلدهای مستقیم wrapper ایجاد نکنید (دسترسی به فیلد شی خود یا فیلد مستقیم — توسط LoD مجاز است). زمانی که مشتری شروع به حرکت گذری میکند wrapper ایجاد کنید: a.b().c().d() → a.b().d() یا a.d().
LoD برای رفتار اعمال میشود، نه برای دادهها. Data class (DTO — کانتینرهای ساده داده) ملزم به رعایت LoD نیستند: هدف آنها آشکار کردن دادهها است. OrderDTO.items[0].price — نقض LoD نیست، زیرا DTO طبق تعریف یک ساختار داده است، نه یک شی با رفتار. اشتباه گرفتن اشیا و ساختارهای داده — یکی از رایجترین خطاها است.
تمایز را Robert C. Martin: «Clean Code» (2008) بیان کرده است: «اشیاء دادهها را پنهان میکنند و رفتار را آشکار میکنند. ساختارهای داده دادهها را آشکار میکنند و رفتاری ندارند». LoD به اشیاء با رفتار مربوط میشود. برای ساختارهای داده (DTO، مدلهای JSON) زنجیرههای دسترسی مجاز هستند. به محض اینکه یک ساختار داده متدی با منطق پیدا میکند — به شی تبدیل میشود و باید LoD را رعایت کند.
تمایز قائل شوید: اگر کلاس فقط شامل فیلدهای بدون متد است (DTO) — LoD برای آن اعمال نمیشود. اگر کلاس شامل متدهایی با منطق است — LoD الزامی است. در بازبینی کد بررسی کنید: این data class (DTO) است یا شی (با متدها)؟
سوالات متداول
قانون دمیتر (LoD): شی فقط میتواند با دوستان نزدیک ارتباط برقرار کند — خودش، فیلدهایش، پارامترهای متدهایش و اشیایی که خودش ایجاد کرده است. نمیتوان از طریق زنجیره رفت: a.getB().getC().doSomething() — این نقض است.
LoD — درباره این است که به کدام اشیا میتوان دسترسی داشت (فقط همسایگان مستقیم). Tell, Don't Ask — درباره این است که چگونه دسترسی داشت (داده نخواه، بلکه بگو انجام دهد). آنها مکمل یکدیگرند: LoD دایره ارتباط را محدود میکند، Tell Don't Ask — نحوه دسترسی را.
LoD را میتوان برای DTO (Data Transfer Objects) و ساختارهای داده ساده بدون منطق نقض کرد. همچنین Builder نقض محسوب نمیشود، زیرا هر فراخوانی همان builder را برمیگرداند. استثناها: زنجیرهها در Stream API (map, filter) — نقض LoD نیست.
Detekt دارای قانون TooManyFunctions (غیرمستقیم) است، اما برای بررسی مستقیم زنجیرهها از قانون DataClassShouldBeImmutable و بررسیهای سفارشی از طریق bindingReference استفاده کنید. CI را تنظیم کنید: زنجیرههای طولانیتر از ۲ فراخوانی — هشدار، طولانیتر از ۳ — خطای ساخت.
SwiftLint قانون داخلی برای LoD ندارد، اما میتوان یک قانون سفارشی از طریق regex ایجاد کرد: زنجیرههای به شکل \..+\.\..+\.\..+ (۳+ فراخوانی از طریق نقطه). جایگزین: از قانون nimble_operator استفاده کنید و آن را برای تشخیص زنجیرههای طولانی گسترش دهید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید