🚀 قانون DRY؛ یکی از بدفهمیدهشدهترین قوانین در برنامهنویسی
هر برنامهنویسی خیلی زود با DRY آشنا میشود، و تقریباً همه آن را اشتباه یاد میگیرند.Don't Repeat Yourself.
دو قطعه کد مشابه دیدی؟ یک متد استخراج کن و کد تکراری را حذف کن.
من هم سالها همین کار را انجام میدادم و نتیجهاش بعضی از بدترین کدهایی بود که تاکنون مجبور به نگهداریشان شدهام:
🔹 یک Helper مشترک که هر اسپرینت یک پارامتر
bool جدید به آن اضافه میشد.🔹 یک Base Class که هیچکس جرئت تغییرش را نداشت، چون شش قابلیت کاملاً نامرتبط از آن ارثبری کرده بودند.
🔹 یک ماژول «مشترک» که دو بخش مستقل سیستم به آن وابسته بودند و در نتیجه هیچکدام نمیتوانستند بدون تأثیر روی دیگری تغییر کنند.
همه اینها با یک تلاش کاملاً بیضرر برای تکرار نکردن کد شروع شدند.
🎯 ءDRY واقعاً چه میگوید؟
این همان بخشی است که بیشتر افراد از آن عبور میکنند.
تعریف اصلی DRY که توسط Andy Hunt و Dave Thomas در کتاب The Pragmatic Programmer
ارائه شد، اصلاً درباره کد نیست:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
«هر بخش از دانش باید در یک سیستم، تنها یک نمایش یکتا، شفاف و مرجع داشته باشد.»
موضوع اصلی دانش (Knowledge) است.
یک حقیقت درباره دامنه کسبوکار شما، مانند یک قانون مالیاتی یا فرمت شماره فاکتور، باید فقط در یک مکان وجود داشته باشد.
وقتی آن حقیقت تغییر کرد، باید فقط یک بار آن را تغییر دهید؛ نه اینکه به دنبال هفت نسخه مختلف آن در سیستم بگردید.
❌ اشتباه رایج: حذف تکرار کد، نه حذف تکرار دانش
دو قطعه کد میتوانند کاملاً مشابه باشند اما دانش کاملاً متفاوتی را نمایش دهند.
فرض کنید دو نوع آدرس را اعتبارسنجی میکنید:
🔹 آدرس ارسال مشتری
🔹 آدرس انبار
امروز قوانین هر دو یکسان هستند:
public bool IsValid(Address address) =>
!string.IsNullOrWhiteSpace(address.Street) &&
!string.IsNullOrWhiteSpace(address.City) &&
!string.IsNullOrWhiteSpace(address.PostalCode);
واکنش طبیعی DRY این است که یک Validator مشترک استخراج کنیم و از هر دو جا آن را فراخوانی کنیم.
اما اینها دو مفهوم متفاوت هستند که فقط همین هفته قوانین یکسانی دارند.
روزی که انبار به یک Loading Dock Code نیاز پیدا کند، دوباره به همان متد مشترک برمیگردید و یک Flag جدید اضافه میکنید تا مصرفکننده قبلی همچنان کار کند:
public bool IsValid(Address address, bool requireDockCode = false) =>
!string.IsNullOrWhiteSpace(address.Street) &&
!string.IsNullOrWhiteSpace(address.City) &&
!string.IsNullOrWhiteSpace(address.PostalCode) &&
(!requireDockCode || !string.IsNullOrWhiteSpace(address.DockCode));
همین پارامتر
bool نشانه خطر است.اولین باری که یک متد مشترک مجبور میشود Flag بگیرد تا برای یک مصرفکننده رفتار متفاوتی داشته باشد، یعنی شما با تکرار مواجه نبودید.
شما فقط دو چیز مشابه را به زور به هم چسباندهاید.
یک سال بعد، امضای متد سه Flag دیگر هم خواهد داشت؛ هر کدام نشانهای از اینکه این دو مفهوم هیچوقت واقعاً یکسان نبودهاند.
💸 هزینه Abstraction اشتباه از هزینه Duplication بیشتر است
تکرار کد معمولاً بسیار ارزانتر از یک Abstraction اشتباه است.
کپیپیست:
✅ قابل مشاهده است
✅ محلی است
✅ هر نسخه میتواند مستقل تکامل پیدا کند
اما Abstraction اشتباه:
❌ پنهان است
❌ سراسری است
❌ همه مصرفکنندهها را به یک شکل خاص وابسته میکند
ءFlagها اضافه میشوند، پیچیدگی افزایش پیدا میکند و در نهایت از تغییر متدی که دیگر درکش نمیکنید میترسید.
من زمان بسیار بیشتری را صرف حذف Abstractionهای بد کردهام تا زمانی که از ایجاد آنها صرفهجویی کرده باشم.
این همان هزینه پنهان Coupling است که قبلاً درباره آن صحبت کردهام و DRY افراطی یکی از رایجترین راههای ورود آن به سیستم است.
🏗 جایی که بیشترین آسیب را میزند: مرزهای سیستم
داخل یک کلاس، یک Helper بد فقط آزاردهنده است.
اما در مرز بین ماژولها، تبدیل به یک آسیب ساختاری میشود.
فرض کنید یک Modular Monolith داریم که شامل دو ماژول است:
🔹 Billing
🔹 Shipping
هر دو یک مفهوم Order دارند.
یک توسعهدهنده با نیت خیر متوجه میشود این دو کلاس فیلدهای مشابهی دارند و آنها را به یک Type مشترک منتقل میکند:
// Shared.Orders, referenced by both Billing and Shipping
public class Order
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public decimal Total { get; set; }
// ...whatever either module happens to need
}