ویرگول
ورودثبت نام
میر مجتبی هاشمی جنتی
میر مجتبی هاشمی جنتیدانش آموخته مهندسی نرم افزار | فعال در صنعت | با اندکی تجربه
میر مجتبی هاشمی جنتی
میر مجتبی هاشمی جنتی
خواندن ۵ دقیقه·۳ روز پیش

کلاس‌های Rich و Anemic؛ وقتی کلاس فقط نگهدارنده دیتا نیست

سلام؛ اگر مدتی با برنامه‌نویسی شی‌گرا کار کرده باشیم، احتمالاً به کلاس‌هایی برخورد کردیم که فقط چند تا فیلد دارن و کلی getter و setter. این کلاس‌ها معمولاً هیچ تصمیمی نمی‌گیرن، هیچ رفتاری ندارن و صرفاً یه بسته برای جابه‌جایی دیتا هستن. اینجاست که ناخودآگاه این سؤال پیش میاد که آیا این چیزی که نوشتیم واقعاً یه «شیء» به حساب میاد یا نه؟ بحث کلاس‌های Anemic و Rich دقیقاً از همین نقطه شروع میشه.

Rich vs Anemic | OOP & DDD Concepts
Rich vs Anemic | OOP & DDD Concepts

در این مقاله، به این موضوع خواهیم پرداخت و اساساً بررسی میکنیم که ویژگی های این دو جریان چه تاثیری در کل سیستم خواهند داشت. بریم سراغ اصل مطلب.

ابتدا میخوام این موضوع رو باهم داشته باشیم که واژه های Rich و Anemic برای توصیف وضعیت یک کلاس به کاربرده میشه. اگر کلاس ما بسیار ساده و صرفا دارای property و getter setter باشه (همون موضوعی که در اول مقاله بیان شد) میگیم کلاسه Anemic هست ولی در مقابل اگر کلاس ما دارای منطق باشه و این منطق ها رو از طریق متد ها بیاییم و داخل کلاس بنویسیم، این کلاس میشه Rich. حالا بریم دقیق تر بررسی کنیم ببینیم یعنی چی این موضوع.

Anemic Model؛ کلاس‌هایی که کم‌خون شدن 😄
همون طور که گفتیم، Anemic Model به مدلی گفته میشه که توش کلاس‌ها تقریباً هیچ منطق خاصی ندارن. مسئولیتشون فقط نگهداری داده‌ست و تمام قوانین و تصمیم‌ها میرن توی سرویس‌ها. در ظاهر شاید تمیز به نظر بیاد، اما کم‌کم ساختار کد از شی‌گرایی فاصله می‌گیره.

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

class BankAccount { private int balance; public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } }

میاییم و میگیم، منطق برداشت پول جایی بیرون از خود حساب خواهیم نوشت:

class BankAccountService { public void withdraw(BankAccount account, int amount) { if (amount <= 0) { throw new IllegalArgumentException(); } if (account.getBalance() < amount) { throw new RuntimeException("Not enough balance"); } account.setBalance(account.getBalance() - amount); } }

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


Rich Model؛ وقتی شیء مسئول خودش است
در مقابل Anemic Model، رویکرد Rich Model قرار داره. توی این مدل، کلاس‌ها فقط نگهدارنده‌ی دیتا نیستن؛ بلکه رفتار و قوانین مربوط به خودشون رو هم می‌شناسن و اجرا می‌کنن. یعنی هر شیء مسئول اینه که اجازه نده به وضعیت نامعتبر بره.

همون مثال حساب بانکی رو این بار به شکل Rich ببینیم:

class BankAccount { private int balance; public BankAccount(int initialBalance) { if (initialBalance < 0) { throw new IllegalArgumentException(); } this.balance = initialBalance; } public void withdraw(int amount) { if (amount <= 0) { throw new IllegalArgumentException(); } if (balance < amount) { throw new RuntimeException("Not enough balance"); } balance -= amount; } public int getBalance() { return balance; } }

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


چرا با وجود این همه ایراد، Anemic Model هنوز استفاده میشه؟
واقعیت اینه که Anemic Model خیلی وقت‌ها انتخاب بدی نیست. فریم‌ورک‌هایی مثل Spring و JPA (من خودم جاوایی ام چون 😄) به‌طور ناخودآگاه ما رو به این سمت هل میدن. از طرفی برای پروژه‌های ساده‌ی CRUD، این مدل سریع‌تر جواب می‌ده و پیچیدگی ذهنی کمتری داره. اما مشکل معمولاً از جایی شروع میشه که پروژه رشد می‌کنه، قوانین بیشتر میشن و منطق دامنه پیچیده‌تر میشه. اون موقع سرویس‌ها چاق میشن، کلاس‌ها لاغر می‌مونن و تغییر دادن قوانین تبدیل به کار پرریسکی میشه. (همون طور که بالا هم گفتم، معمار ما باید اول کار، تصمیم درست رو بگیره و بر اساس استک و وضعیت کلی پروژه، بدونه که رویکرد ما در پروژه نسبت به کلاس هامون، چی خواهد بود)

Rich Model و ارتباطش با DDD
اگه با Domain-Driven Design آشنا باشیم، احتمالاً شنیدیم که میگن موجودیت‌ها باید «رفتار» داشته باشن. Rich Model دقیقاً با همین طرز فکر هم‌راستاست. توی این رویکرد، entityها مسئول حفظ invariantها هستن و اجازه نمیدن سیستم وارد وضعیت اشتباه بشه. اگر با کلید واژه های DDD آشنا نیستی، یه سر به اینجا بزن.

یرای مثال، فعال‌سازی یک کاربر بهتره تصمیم خود کاربر باشه، نه یه سرویس بیرونی که هر کاری دلش خواست با وضعیتش بکنه. بریم این رو یه کد برنیم و ببینیم چی به چی هست ...

این کلاس ما الان یک کلاس Anemic هست که منطق اون در یه سرویس بیرونی هست:

class User { private String email; private boolean active; // getter & setter }
class UserService { public void activate(User user) { if (user.isActive()) { throw new RuntimeException("Already active"); } user.setActive(true); } }

تغییر رویکرد میدیم و اون رو به Rich تبدیل میکنیم:

class User { private String email; private boolean active; public void activate() { if (active) { throw new RuntimeException("Already active"); } active = true; } }


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

در مقالات بعدی، بیشتر در مورد این مفاهیم صحبت خواهیم کرد ...

کلاسdomain driven design
۱
۰
میر مجتبی هاشمی جنتی
میر مجتبی هاشمی جنتی
دانش آموخته مهندسی نرم افزار | فعال در صنعت | با اندکی تجربه
شاید از این پست‌ها خوشتان بیاید