بلاگ

  • چرا پروژه‌ها شکست می‌خورند؟ از قوانین لمان تا Bounded Context در DDD

    چرا پروژه‌ها شکست می‌خورند؟ از قوانین لمان تا Bounded Context در DDD

    چرا کدهای تمیزمون بعد از یک سال به گند کشیده میشن؟ (از قوانین لمان تا معجزه Bounded Context در DDD)

    تا حالا براتون پیش اومده که یه پروژه رو با کلی ذوق و شوق، با تمیزترین کدها و جدیدترین فریم‌ورک‌ها شروع کنید، ولی بعد از یک سال کار به جایی برسه که اضافه کردن یه دکمه یا فیچر ساده بشه کابوس شبانه‌تون؟ جایی که هر جای کد رو دست می‌زنید، یه جای دیگه می‌ترکه!
    توی مهندسی نرم‌افزار به این وضعیت اسفناک میگن «گلوله بزرگ گل» (Big Ball of Mud) یا به زبان خودمون، باتلاق کدهای اسپاگتی.
    ولی واقعاً چرا اینطوری میشه؟ آیا همیشه تقصیر ما برنامه‌نویس‌هاست یا یه فرمول و قانون طبیعی پشت پرده وجود داره؟ توی این پست (که قسمت اول از سری مقاله‌های خودمونی DDD یا طراحی دامین محور هست)، می‌خوایم ریشه این فاجعه رو خیلی ساده بررسی کنیم و ببینیم چطور مهم‌ترین ابزار استراتژیک DDD یعنی Bounded Context میاد و پروژه‌مون رو نجات میده.

    ۱. دو مدل پیچیدگی داریم: کدومش تقصیر ماست؟

    برای اینکه بفهمیم چرا کدهامون به مرور زمان کثیف میشن، اول باید بدونیم با چه جور پیچیدگی‌هایی طرفیم. یه مهندس معروف به اسم فرد بروکس میاد پیچیدگی‌های نرم‌افزار رو به دو دسته خیلی باحال تقسیم می‌کنه:

    الف) پیچیدگی ذاتی (Essential Complexity)
    خلاصه‌ش میشه: پیچیدگیِ خودِ کسب‌وکار؛ یعنی همون چیزی که شرکت بابت حل کردنش به ما حقوق میده!
    این پیچیدگی توی ذاتِ دنیای واقعی وجود داره و اصلاً ربطی به کدنویسی نداره. مثلاً فرض کنید دارید یه سیستم بانکی می‌نویسید و قانون بانک اینه: «اگه روز تعطیل رسمی باشه، کاربر معمولی نمی‌تونه بیشتر از ۵۰ میلیون تومن کارت به کارت کنه، مگر اینکه مشتری ویژه باشه.»
    حتی اگه کامپیوتری در کار نبود و کارمند بانک می‌خواست با کاغذ و ماشین‌حساب هم این کار رو بکنه، باز هم این قانون وجود داشت. ما نمی‌تونیم پیچیدگی ذاتی رو حذف کنیم (مگر اینکه رئیس بانک قانون رو عوض کنه!)؛ ما فقط باید بفهمیمش و توی کد پیاده‌سازیش کنیم.

    ب) پیچیدگی عارضی یا تصادفی (Accidental Complexity)
    خلاصه‌ش میشه: دردسرهایی که خودمون با دست‌های خودمون درست می‌کنیم!
    این پیچیدگی توی صورت‌مسئله نبوده، بلکه ما برنامه‌نویس‌ها به خاطر انتخاب ابزارهای اشتباه، معماری غلط، عجله کردن یا کدنویسی بد به سیستم اضافه‌ش کردیم.
    مثلاً کلنجار رفتن با تنظیمات ORM،کش کردن دیتا توی Redis، یا از همه بدتر: اینکه بیایم همون قانون مهمِ سقف برداشت بانکی رو با کلی if/else تودرتو بنویسیم وسطِ Controller!
    بخش بزرگی از این پیچیدگی عارضی همون بدهی فنی (Technical Debt) خودمونه که اگه حواسمون نباشه، پروژه رو می‌بلعه.

    ۲. مخروط عدم قطعیت (Cone of Uncertainty): تله‌ای که روز اول توش می‌افتیم!

    حالا سوال اینجاست: مگه ما برنامه‌نویس‌های خوبی نیستیم؟ پس چرا پیچیدگی عارضی از دستمون در میره؟ جوابش توی یه مفهوم جالب به اسم مخروط عدم قطعیت قایم شده.
    این مفهوم یه حقیقت تلخ رو توی صورت ما می‌زنه: روز اولی که پروژه رو استارت می‌زنیم، ما کمترین شناخت ممکن رو از قوانین واقعی بیزینس (همون پیچیدگی ذاتی) داریم؛ اما دقیقاً در همین روز اول، مجبوریم مهم‌ترین تصمیمات معماری و ساختار دیتابیس رو بگیریم!

    فاجعه اینجوری رخ میده:

    اول پروژه: همه چیز مبهمه. نه ما لبه‌های تاریک بیزینس رو می‌شناسیم و نه حتی خودِ مدیر محصول و کارفرما! ولی چون باید کار رو شروع کنیم، میریم جدول‌های دیتابیس و کلاس‌ها رو بر اساس حدس و گمان می‌سازیم.

    وسط پروژه: چند ماه می‌گذره، ابهام‌ها کم میشه و تازه قوانین عجیب‌وغریبِ بیزینس خودشون رو نشون میدن. اینجاست که می‌فهمیم مدل اولیه‌مون کلاً غلط بوده!

    شروع وصله‌پینه: چون کدها به دیتابیس و زیرساخت گره خوردن و وقت نداریم از اول بنویسیمشون، شروع می‌کنیم به هک زدن و وصله‌پینه کردن (Workaround). و بوم! عدم قطعیتِ روز اول، تبدیل میشه به یه کوه از پیچیدگی عارضی!

    ۳. قوانین لمان (Lehman’s Laws): سرنوشت محتوم نرم‌افزارها

    دهه ۷۰ میلادی، یه دانشمند به اسم مانی لمان اومد روی نرم‌افزارهای واقعی تحقیق کرد و به ۸ تا قانون رسید. ۴ تا از این قوانین رسماً دارن زندگی روزمره ما برنامه‌نویس‌ها رو تعریف می‌کنن:

    قانون اول (تغییر دائمی): دنیای کسب‌وکار هیچ‌وقت ثابت نمی‌مونه. پس کد شما هم برای اینکه به درد بخوره، محکومه که دائماً تغییر کنه.

    قانون دوم (افزایش پیچیدگی): با هر فیچر جدیدی که به کد اضافه می‌کنید، سیستم به صورت طبیعی شلخته‌تر و پیچیده‌تر میشه؛ مگر اینکه آستین بالا بزنید و به صورت مداوم کدها رو تمیز و Refactor کنید.

    قانون چهارم (توهم اضافه کردن نیرو): وقتی کدبیس پیچیده و کثیف شد، اگه مدیر پروژه ۱۰ تا برنامه‌نویس جدید هم استخدام کنه، سرعت کار بیشتر نمیشه! چون وقت همه صرف توضیح دادن کدهای کثیف قبلی و هماهنگی با هم میشه.

    قانون هفتم (افت کیفیت): حتی اگه کدی بنویسید که امروز صفر تا باگ داشته باشه و دست بهش نزنید، دو سال دیگه از نظر کاربرها یه نرم‌افزار به درد نخور و بی‌کیفیته؛ چون نیازهای دنیا عوض شده ولی نرم‌افزار شما درجا زده.

    ۴. راه‌حل طلایی DDD: شکستن غول با Bounded Context

    خب، حالا چطوری جلوی قوانین بی‌رحم لمان رو بگیریم و نذاریم پروژه‌مون به باتلاق تبدیل بشه؟
    بزرگ‌ترین اشتباهی که توی طراحی سیستم‌ها می‌کنیم اینه که می‌خوایم «یک مدل واحد برای کل شرکت» بسازیم!
    بذارید با یه مثال از یه فروشگاه اینترنتی (مثل دیجی‌کالا) توضیح بدم:
    کلمه «کالا» (Product) توی همه جلسات شرکت تکرار میشه. اشتباه مرگبار اینه که بگیم: «خب، بیاید یه جدول و یه کلاس بزرگ به اسم Product بسازیم که همه بخش‌های شرکت از همون استفاده کنن تا دوباره‌کاری نشه!»
    چی میشه؟
    تیم ویترین سایت میاد عکس و نظرات کاربران رو به کلاس Product اضافه می‌کنه.
    تیم مالی میاد قیمت و فرمول مالیات و کد تخفیف رو بهش می‌چسبونه.
    تیم انبار میاد شماره قفسه و موجودی رو توش می‌ذاره.
    تیم ارسال (لجستیک) میاد وزن و ابعاد کارتن و شکستنی بودن رو بهش اضافه می‌کنه.
    بعد از یک سال، شما یه کلاس غول‌پیکر (God Object) دارید با ۱۵۰ تا فیلد! حالا اگه تیم انبار بخواد یه تغییر کوچیک توی کدِ قفسه‌بندی بده، یهو بخش محاسبه تخفیف توی سبد خرید از کار می‌افته!

    ورود Bounded Context (زمینه محدودشده)
    اریک اوانز (خالق DDD) میگه: کلمه‌ها توی کل شرکت معنی یکسانی ندارن!
    کلمه «کالا» توی ذهن مدیر انبار با کلمه «کالا» توی ذهن مدیر فروش دو تا چیز کاملاً متفاوته. پس به جای یه مدل غول‌پیکر، بیاید دور هر بخش از بیزینس یه خط قرمز بکشیم که بهش میگن Bounded Context:

    توی مرز کاتالوگ (Catalog Context): کالا فقط چیزیه که عکس، توضیحات و کامنت داره.

    توی مرز فروش (Sales Context): کالا چیزیه که قیمت و تخفیف داره.

    توی مرز ارسال (Shipping Context): اینجا اصلاً بهش نمی‌گیم کالا؛ اسمش رو می‌ذاریم Package (بسته پستی)! چون برای بچه‌های ارسال فقط وزن و ابعاد جعبه مهمه و اصلاً براشون مهم نیست قیمتِ داخل جعبه چنده یا چه رنگیه!
    با این کار چه اتفاقی افتاد؟
    به جای یه کلاس ترسناک ۱۵۰ فیلدی، سه تا کلاسِ خیلی کوچیک، تمیز و مستقل داریم.
    هر تیمی توی Bounded Context خودش و با دیتابیس خودش کار می‌کنه، بدون اینکه پای تیم دیگه رو لگد کنه.

    و نکته طلایی: مرز واقعی هر میکروسرویس دقیقاً همین Bounded Context است!

    جمع‌بندی

    خلاصه ماجرا اینه: تغییر کردنِ نیازمندی‌های پروژه یه قانون طبیعیه (قوانین لمان) و ما روز اول نمی‌دونیم قراره چی سرمون بیاد (مخروط عدم قطعیت). تنها راه زنده موندن اینه که سیستم رو به Bounded Contextهای کوچیک و مستقل بشکنیم و نذاریم پیچیدگیِ ابزارها و دیتابیس، منطقِ اصلی بیزینس ما رو کثیف کنه.