
در دنیای توسعه نرمافزار، از اشتباهات رایجی که میتواند معماری سیستم را به مرور زمان شکننده و غیرقابل نگهداری کند و بعدا بدهی فنی آن هم از نوع شدید را بوجود آورد، تله موجودیت (Entity Trap) است. زمانی در این تله می افتیم که برای طراحی سیستم، بهجای مدلکردن رفتارها (Behavior) و منطق کسبوکار (Business Logic)، صرفاً روی ساختار دادهها (Data Structure) و موجودیتهای پایگاه داده تمرکز شود. به بیان دیگر وقتی دادهها جای منطق کسبوکار را میگیرند در تلهای به نام موجودیت افتادهایم.
نتیجه تله موجودیت، یک سیستم یا محصول یا پروژه کوتاه مدت است! در کوتاهمدت کار میکند اما آنچنان شکننده که با رشد پروژه، نیازمندی جدید مشتری، تغییر قوانین کسبوکار یا حتی ضرورت بهینه سازی و استفاده از تکنولوژی جدید، پروژه به کابوس تبدیل میشود. در ادامه به چرایی و راهکارهای مقابله با این تله میپردازیم.
قبل از اینکه تله موجودیت را معرفی کنم، برای مرور بهتر موجودیت را تعریف کنیم. همانطور که میدانید در معماری نرمافزار، موجودیت (Entity) به یک شیء یا مدلی گفته میشود که:
تصور کنید تیم توسعه، تمام منطق کسبوکار را مستقیماً داخل موجودیتها یا لایه داده مینویسد. مثلا کلاسی که مربوط به مشتری یا حساب بانکی است، متدی برای انتقال حساب داشته باشد.
class BankAccount { private: int id; std::string owner; double balance; public: // انتقال وجه مستقیماً در موجودیت! void transfer(BankAccount& targetAccount, double amount) { if (this->balance < amount) { throw std::runtime_error("Insufficient funds"); } this->balance -= amount; targetAccount.balance += amount; } // سایر متدها };
این نحوه توسعه چه مشکلی را ایجاد میکند؟
موجودیت فراتر از فانکشن یا کلاس یا ماژول است. سرویس یا پروژه هم میتواند در لایه های مختلف از همین جهت بررسی و مورد نقد و کنکاش قرار گیرد. به زبان ساده هرگاه هر موجودیتی در بستر یک موجودیت بزرگتر تبدیل به God Objects یا یک موجودیت همه کاره با مسئولیتهای زیادی تبدیل شود در دام موجودیت افتاده ایم. مسئولیت پذیری زیاد و نامتوازن توسط یک موجودیت، توسعه، تست، نگهداری، مقیاسپذیری و تحملپذیری خطای آنها را بسیار بزرگ و دشوار میکند.
قبل از ادامه توضیحات و در ادامه مثال ایجاد تله موجودیت به مثال اصلاحی آن توجه کنید.
class BankAccount { private: int id; std::string owner; double balance; public: // فقط متدهای دسترسی ساده double getBalance() const { return balance; } void setBalance(double newBalance) { balance = newBalance; } // ... }; class MoneyTransferService { public: static void transfer(BankAccount& source, BankAccount& target, double amount) { if (source.getBalance() < amount) { throw std::runtime_error("Insufficient funds"); } source.setBalance(source.getBalance() - amount); target.setBalance(target.getBalance() + amount); // امکان اضافه کردن قوانین جدید: // - کارمزد انتقال // - اعتبارسنجی AML // - لاگ تراکنش } };
برای رهایی از تله راهکارهای زیر توصیه میشود:
۱. تفکیک رفتار (Behavior) از حالت (State)
بهجای قراردادن تمام منطق داخل موجودیت، آن را به سرویسهای حوزه (Domain Services) منتقل کنید. یا تعریف کلاس و ماژول های دیگر. مشابه مثال ذکر شده. البته قطعا مثال گفته شده یک مثال ابتدایی و ناقص است و تنها برای توضیح کلی مطلب گفته شده است.
۲. استفاده از الگوی Domain-Driven Design (DDD)
در DDD، موجودیتها فقط داده را نگه میدارند و رفتارها در Aggregate Roots یا Domain Services تعریف میشوند. میتوان گفت موجودیتها را باید Anemic (فاقد منطق) نگه دارید (مگر برای رفتارهای ساده مثل Validation).
۳. معماری لایهبندی شده
بدهی فنی مثل بدهی مالی (وام) است. همچنان که برای جلو زدن از تورم یا راه اندازی کسب و کار و... راه گریزی نیست. بدهی فنی هم برای شروع پروژه و توسعه محصول نیاز است. اما آنچه مهم است پرداخت به موقع آن است که مستلزم شناخت آن است. یعنی این مهم است که اگر راهی سریع به جای راه اصولی انتخاب میشود با شناخت و پذیرش عواقب آن و برنامه عملیاتی برای جبران به موقع آن باشد. تله موجودیت از یک معماری اشتباه می آید که خیلی از مواقع بدلیل تصمیمات سریع رخ میدهد. اگر سیستم شما پر از موجودیتهای چاق و سنگین است که همه کار میکنند و سنگین شده اند، احتمالاً در تله موجودیت افتادهاید! زمان بازبینی معماری است. جدا از مباحث فنی مانند تست پذیری و مقیاس پذیری، تله موجودیت در سطح سرویس یا پروژه علاوه بر مشکلات گفته شده در زمینه مدیریت تیم ها و نفرات نیز به شدت از چابکی و انعطاف می کاهد.