اصول SOLID: چیزی که همه ازش اسم میبرن ولی کمتر کسی درست ازش استفاده میکنه
حدود ۵ دقیقه مطالعه
اصول SOLID احتمالاً یکی از پرتکرارترین موضوعاتی هست که تو مصاحبههای برنامهنویسی ازش سؤال میشه، ولی همین تکرار زیاد باعث شده خیلی وقتها به یه سری تعریف حفظی تبدیل بشه که کسی واقعاً نمیدونه کِی و چرا باید ازشون استفاده کنه. این پنج اصل، راهنمای طراحی کلاسها و ماژولها هستن که هدفشون کد قابل نگهداریتر و کمتر شکننده در برابر تغییره؛ نه یه سری قانون مقدس که باید همیشه و همهجا کورکورانه اجراشون کنی.
S — Single Responsibility Principle
این اصل میگه هر کلاس باید فقط یه دلیل برای تغییر کردن داشته باشه. مثال کلاسیکش یه کلاس Invoice هست که هم منطق محاسبهی قیمت رو داره، هم منطق ذخیرهسازی تو دیتابیس، و هم منطق چاپ فاکتور:
class Invoice:
def calculate_total(self):
# منطق محاسبهی قیمت
pass
def save_to_db(self):
# منطق ذخیرهسازی
pass
def print_invoice(self):
# منطق چاپ
pass
مشکل این طراحی اینه که سه تا دلیل کاملاً مستقل برای تغییر این کلاس وجود داره: تغییر منطق قیمتگذاری، تغییر نوع دیتابیس، و تغییر فرمت چاپ. اگه اینها رو جدا کنیم به Invoice، InvoiceRepository و InvoicePrinter، هر کدوم فقط با یه دلیل مشخص تغییر میکنن و تست کردنشون هم سادهتر میشه.
نکتهای که خیلی وقتها اشتباه فهمیده میشه اینه که SRP به معنی «هر کلاس فقط باید یه متد داشته باشه» نیست؛ منظور اینه که همهی متدهای یه کلاس باید حول یه مسئولیت واحد بچرخن.
O — Open/Closed Principle
این اصل میگه کد باید برای extension باز باشه ولی برای modification بسته. یعنی وقتی یه رفتار جدید اضافه میکنی، نباید مجبور بشی کد قدیمی و تستشده رو دستکاری کنی. مثال رایجش محاسبهی تخفیفه:
# طراحی بد: هر تخفیف جدید یعنی تغییر این تابع
def calculate_discount(customer_type, price):
if customer_type == "regular":
return price * 0.95
elif customer_type == "vip":
return price * 0.85
# هر مشتری جدید یعنی یه elif دیگه
# طراحی خوب: با polymorphism
class DiscountStrategy:
def apply(self, price):
raise NotImplementedError
class VipDiscount(DiscountStrategy):
def apply(self, price):
return price * 0.85
class RegularDiscount(DiscountStrategy):
def apply(self, price):
return price * 0.95
تو نسخهی دوم، اضافه کردن یه نوع تخفیف جدید یعنی فقط یه کلاس جدید نوشتن، بدون اینکه به تابع اصلی دست بزنی. این دقیقاً همون چیزیه که ریسک شکستن رفتار قدیمی رو کم میکنه.
L — Liskov Substitution Principle
این اصل میگه هر جا که یه کلاس پایه استفاده میشه، باید بشه بهجاش از هر کدوم از زیرکلاسهاش استفاده کرد، بدون اینکه رفتار برنامه خراب بشه. مثال معروفش مشکل مربع و مستطیله:
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # چون مربعه، عرض و ارتفاع باید برابر بمونن
def set_height(self, h):
self.width = h
self.height = h
از نظر ریاضی، مربع نوع خاصی از مستطیله، ولی از نظر رفتار برنامه، Square قرارداد Rectangle رو نقض میکنه؛ چون تغییر عرض بهطور غیرمنتظره ارتفاع رو هم تغییر میده. اگه یه تابع فرض کنه با set_width فقط عرض عوض میشه و ارتفاع دستنخورده میمونه، وقتی همون تابع با یه Square صدا زده بشه، رفتار غلطی میگیره. این مثال نشون میده که ارثبری باید بر اساس رفتار طراحی بشه، نه صرفاً بر اساس رابطهی مفهومی «این یه نوع از اونه».
I — Interface Segregation Principle
این اصل میگه هیچ کلاسی نباید مجبور بشه به متدهایی وابسته باشه که ازشون استفاده نمیکنه. تصور کن یه interface بزرگ برای Worker داری:
class Worker:
def work(self): raise NotImplementedError
def eat(self): raise NotImplementedError
اگه بخوای یه RobotWorker بسازی، مجبوری متد eat رو هم پیادهسازی کنی، در حالی که هیچ معنایی برای یه ربات نداره. راهحل اینه که این interface بزرگ رو به دو تا interface کوچیکتر بشکنیم: Workable و Eatable، و هر کلاس فقط اونی رو implement کنه که واقعاً بهش نیاز داره. این اصل خیلی وقتها تو زبانهایی مثل جاوا یا TypeScript که interface صریح دارن پررنگتره، ولی همون فلسفه تو پایتون هم با duck typing و ترکیب کلاسهای کوچیکتر قابل اعماله.
D — Dependency Inversion Principle
این اصل میگه ماژولهای سطح بالا نباید مستقیم به ماژولهای سطح پایین وابسته باشن؛ هر دو باید به یه abstraction وابسته باشن. مثال رایجش وابستگی مستقیم به یه دیتابیس خاصه:
# طراحی بد
class UserService:
def __init__(self):
self.db = MySQLDatabase() # وابستگی سخت به یه پیادهسازی خاص
# طراحی خوب
class UserService:
def __init__(self, db: DatabaseInterface):
self.db = db # هر چیزی که این interface رو implement کنه قابل تزریقه
با نسخهی دوم، میتونی بهراحتی تو تستها یه mock database تزریق کنی، یا بعداً از MySQL به PostgreSQL سوییچ کنی، بدون اینکه خط به خط UserService رو تغییر بدی. این همون ایدهای هست که پشت dependency injection تو فریمورکهایی مثل Spring یا NestJS قرار داره.
کی این اصول رو کنار بذاریم
نکتهای که خیلی وقتها نادیده گرفته میشه اینه که SOLID برای کدی طراحی شده که قراره رشد کنه و تغییر کنه. اگه داری یه اسکریپت یکبار مصرف مینویسی یا یه پروتوتایپ سریع میسازی، رعایت سفتوسخت این اصول فقط پیچیدگی اضافه میکنه بدون اینکه فایدهی واقعی داشته باشه. مثلاً درست کردن یه abstraction برای یه دیتابیسی که مطمئنی هیچوقت عوض نمیشه، فقط یه لایهی اضافهی بدون کاربرده.
قانونی که خودم بهش رسیدم اینه: این اصول رو وقتی جدی بگیر که کدت داره به یه نقطهای میرسه که هر تغییر کوچیک، چند جای دیگه رو هم میشکنه، یا وقتی داری چیزی میسازی که میدونی قراره تو آینده گسترش پیدا کنه. تا اون موقع، ساده نوشتن معمولاً بهتر از over-engineering کردنه.
