ahmad hasanzadeh branding logoahmad hasanzadeh branding logo
    خانهپروژه هامقالاتدرباره منتکنولوژی هاارتباط با من
ahmad hasanzadeh branding logoahmad hasanzadeh branding logo

ممنون که سر زدی ッ

© ۱۴۰۵

احمد حسن زاده. تمامی حقوق محفوظ است.

راهنمای کاربردی اصول SOLID برای بهبود طراحی و کیفیت کد

راهنمای کاربردی اصول SOLID برای بهبود طراحی و کیفیت کد

راهنمای عملی اصول SOLID با مثال‌های واقعی کد قبل و بعد از رفکتور، برای نوشتن کدی قابل نگهداری و مقیاس‌پذیر.

اصول SOLID یکی از پرتکرارترین مفاهیمی هستند که در مصاحبه‌های فنی، کدریویوها و بحث‌های معماری شنیده می‌شوند، اما خیلی از توسعه‌دهنده‌ها فقط تعریف کتابی آن‌ها را حفظ کرده‌اند بدون این‌که واقعاً بدانند این پنج قانون در کد روزمره چه شکلی پیدا می‌کنند. این مقاله دقیقاً همین شکاف را پر می‌کند: هر اصل را با یک مثال واقعی از کد بد (Violation) و نسخه‌ی اصلاح‌شده‌ی آن بررسی می‌کنیم تا بدانید دقیقاً کِی و چرا باید از هرکدام استفاده کنید.

SOLID مخفف پنج اصل طراحی شیءگرا است که در دهه‌ی ۲۰۰۰ توسط رابرت مارتین (Uncle Bob) گردآوری و تثبیت شد:

  • S – Single Responsibility Principle (اصل تک مسئولیتی)
  • O – Open/Closed Principle (اصل باز/بسته)
  • L – Liskov Substitution Principle (اصل جایگزینی لیسکاف)
  • I – Interface Segregation Principle (اصل تفکیک اینترفیس)
  • D – Dependency Inversion Principle (اصل وارونگی وابستگی)

این اصول مختص هیچ زبان یا فریم‌ورک خاصی نیستند؛ چه در بک‌اند با NestJS کار کنید، چه در فرانت‌اند با React و TypeScript، منطق پشت این قوانین یکسان است. در مثال‌های این مقاله از TypeScript استفاده شده چون خواناترین شکل ممکن برای نمایش تایپ‌ها و قراردادهاست، اما مفهوم هر اصل کاملاً قابل انتقال به زبان‌های دیگر (Java، C#، PHP، Python) است.

نمای کلی پنج اصل SOLID در طراحی نرم‌افزار

۱. اصل تک مسئولیتی (Single Responsibility Principle)

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

کد ناقض این اصل:

invoice.service.bad.tsts
class Invoice {
  constructor(private items: { price: number; qty: number }[]) {}
 
  calculateTotal(): number {
    return this.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  }
 
  saveToDatabase(): void {
    // اتصال مستقیم به دیتابیس داخل همین کلاس
    console.log('Saving invoice to database...');
  }
 
  sendEmailNotification(email: string): void {
    // منطق ارسال ایمیل هم اینجا قرار گرفته
    console.log(`Sending invoice email to ${email}`);
  }
}

نسخه‌ی اصلاح‌شده با تفکیک مسئولیت‌ها:

invoice.service.good.tsts
class Invoice {
  constructor(private items: { price: number; qty: number }[]) {}
 
  calculateTotal(): number {
    return this.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  }
}
 
class InvoiceRepository {
  save(invoice: Invoice): void {
    console.log('Saving invoice to database...');
  }
}
 
class InvoiceNotifier {
  sendEmail(email: string, invoice: Invoice): void {
    console.log(`Sending invoice email to ${email}`);
  }
}

حالا هر کلاس فقط یک وظیفه دارد و تغییر در منطق ایمیل، هیچ تأثیری روی محاسبه‌ی فاکتور یا لایه‌ی ذخیره‌سازی نمی‌گذارد.

۲. اصل باز/بسته (Open/Closed Principle)

یک ماژول باید برای توسعه باز و برای تغییر بسته باشد. یعنی وقتی رفتار جدیدی نیاز دارید، باید بتوانید آن را با افزودن کد جدید اضافه کنید، نه با دستکاری کد قبلی که ممکن است چیزی را بشکند.

کد ناقض این اصل، جایی که برای هر روش پرداخت جدید باید همین تابع را ویرایش کنیم:

payment.bad.tsts
class PaymentProcessor {
  process(type: 'card' | 'wallet' | 'crypto', amount: number): void {
    if (type === 'card') {
      console.log(`Charging ${amount} via card`);
    } else if (type === 'wallet') {
      console.log(`Charging ${amount} via wallet`);
    } else if (type === 'crypto') {
      console.log(`Charging ${amount} via crypto`);
    }
    // برای هر روش پرداخت جدید، این تابع باید دوباره تغییر کند
  }
}

نسخه‌ی اصلاح‌شده با استفاده از انتزاع (Abstraction) و پلی‌مورفیسم:

payment.good.tsts
interface PaymentMethod {
  charge(amount: number): void;
}
 
class CardPayment implements PaymentMethod {
  charge(amount: number): void {
    console.log(`Charging ${amount} via card`);
  }
}
 
class WalletPayment implements PaymentMethod {
  charge(amount: number): void {
    console.log(`Charging ${amount} via wallet`);
  }
}
 
class CryptoPayment implements PaymentMethod {
  charge(amount: number): void {
    console.log(`Charging ${amount} via crypto`);
  }
}
 
class PaymentProcessor {
  process(method: PaymentMethod, amount: number): void {
    method.charge(amount);
  }
}
 
// افزودن روش پرداخت جدید بدون لمس PaymentProcessor:
class BankTransferPayment implements PaymentMethod {
  charge(amount: number): void {
    console.log(`Charging ${amount} via bank transfer`);
  }
}

الان برای اضافه کردن یک روش پرداخت جدید، فقط یک کلاس جدید می‌سازیم؛ کد قبلی دست‌نخورده باقی می‌ماند و ریسک شکستن رفتار موجود به صفر می‌رسد.

۳. اصل جایگزینی لیسکاف (Liskov Substitution Principle)

هر کلاس فرزند باید بتواند جای کلاس والد خودش را بدون بر هم زدن صحت برنامه بگیرد. اگر کدی که با کلاس والد کار می‌کند، وقتی زیرکلاس جایگزین آن می‌شود دچار رفتار غیرمنتظره یا خطا شود، این اصل نقض شده است.

مثال کلاسیک نقض این اصل با سلسله‌مراتب Rectangle/Square:

shape.bad.tsts
class Rectangle {
  constructor(protected width: number, protected height: number) {}
 
  setWidth(width: number) { this.width = width; }
  setHeight(height: number) { this.height = height; }
  getArea(): number { return this.width * this.height; }
}
 
class Square extends Rectangle {
  setWidth(width: number) {
    this.width = width;
    this.height = width; // رفتار غیرمنتظره برای کدی که Rectangle انتظار دارد
  }
  setHeight(height: number) {
    this.width = height;
    this.height = height;
  }
}
 
function resizeAndCheck(rect: Rectangle) {
  rect.setWidth(5);
  rect.setHeight(10);
  console.assert(rect.getArea() === 50); // برای Square این تست شکست می‌خورد
}

نسخه‌ی اصلاح‌شده با جدا کردن انتزاع مشترک به‌جای وراثت اجباری:

shape.good.tsts
interface Shape {
  getArea(): number;
}
 
class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  getArea(): number { return this.width * this.height; }
}
 
class Square implements Shape {
  constructor(private side: number) {}
  getArea(): number { return this.side * this.side; }
}
 
function printArea(shape: Shape) {
  console.log(shape.getArea());
}

به‌جای این‌که Square را زیرکلاس اجباریِ Rectangle کنیم، هر دو را پیاده‌کننده‌ی یک اینترفیس مشترک (Shape) قرار دادیم؛ هیچ فرضیات نادرستی درباره‌ی رفتار مشترک باقی نمی‌ماند.

۴. اصل تفکیک اینترفیس (Interface Segregation Principle)

کلاینت‌ها نباید مجبور شوند به متدهایی وابسته باشند که استفاده نمی‌کنند. یک اینترفیس بزرگ و همه‌کاره، کلاس‌های پیاده‌کننده را مجبور می‌کند متدهایی را پیاده‌سازی کنند که برایشان بی‌معنی است.

کد ناقض این اصل:

worker.bad.tsts
interface Worker {
  work(): void;
  eat(): void;
}
 
class HumanWorker implements Worker {
  work(): void { console.log('Working...'); }
  eat(): void { console.log('Eating lunch...'); }
}
 
class RobotWorker implements Worker {
  work(): void { console.log('Working...'); }
  eat(): void {
    throw new Error('Robots do not eat!'); // پیاده‌سازی بی‌معنی و اجباری
  }
}

نسخه‌ی اصلاح‌شده با شکستن اینترفیس به بخش‌های کوچک‌تر و متمرکز:

worker.good.tsts
interface Workable {
  work(): void;
}
 
interface Eatable {
  eat(): void;
}
 
class HumanWorker implements Workable, Eatable {
  work(): void { console.log('Working...'); }
  eat(): void { console.log('Eating lunch...'); }
}
 
class RobotWorker implements Workable {
  work(): void { console.log('Working...'); }
  // نیازی به پیاده‌سازی eat() نیست
}

هر کلاس فقط قراردادهایی را پیاده‌سازی می‌کند که به آن‌ها نیاز دارد؛ نه بیشتر.

۵. اصل وارونگی وابستگی (Dependency Inversion Principle)

ماژول‌های سطح بالا نباید مستقیماً به ماژول‌های سطح پایین وابسته باشند؛ هر دو باید به انتزاع (Interface) وابسته باشند. این اصل پایه‌ی اصلی الگوهایی مثل Dependency Injection در فریم‌ورک‌هایی مانند NestJS و Angular است.

کد ناقض این اصل، جایی که سرویس سطح بالا مستقیماً به یک پیاده‌سازی خاص از دیتابیس وابسته است:

user.bad.tsts
class MySQLUserRepository {
  save(user: { name: string }): void {
    console.log(`Saving ${user.name} to MySQL`);
  }
}
 
class UserService {
  private repository = new MySQLUserRepository(); // وابستگی مستقیم به پیاده‌سازی
 
  createUser(name: string): void {
    this.repository.save({ name });
  }
}

نسخه‌ی اصلاح‌شده با وارونگی وابستگی از طریق تزریق وابستگی:

user.good.tsts
interface UserRepository {
  save(user: { name: string }): void;
}
 
class MySQLUserRepository implements UserRepository {
  save(user: { name: string }): void {
    console.log(`Saving ${user.name} to MySQL`);
  }
}
 
class MongoUserRepository implements UserRepository {
  save(user: { name: string }): void {
    console.log(`Saving ${user.name} to MongoDB`);
  }
}
 
class UserService {
  constructor(private repository: UserRepository) {} // وابستگی به انتزاع
 
  createUser(name: string): void {
    this.repository.save({ name });
  }
}
 
// جایگزین کردن پیاده‌سازی دیتابیس، بدون تغییر UserService:
const service = new UserService(new MongoUserRepository());

با این تغییر، UserService دیگر نمی‌داند دیتابیس زیرین MySQL است یا Mongo؛ فقط با یک قرارداد (Interface) کار می‌کند و تست‌نویسی و جایگزینی پیاده‌سازی‌ها بسیار ساده‌تر می‌شود.

نمودار جریان وابستگی قبل و بعد از اعمال Dependency Inversion

چه زمانی باید SOLID را دقیق رعایت کرد و چه زمانی نه

اعمال کورکورانه‌ی هر پنج اصل روی یک اسکریپت کوچک یا یک پروژه‌ی MVP، معمولاً بیش از حد پیچیده و زمان‌بر می‌شود. SOLID برای کدی طراحی شده که قرار است در طول زمان رشد کند، توسط چند نفر نگهداری شود و دائم در حال تغییر باشد. قانون عملی این است: وقتی یک کلاس یا تابع شروع به بزرگ شدن می‌کند، وقتی هر تغییر کوچک چند فایل نامرتبط را تحت تأثیر قرار می‌دهد، یا وقتی نوشتن تست واحد برایش سخت می‌شود، وقت آن رسیده که این اصول را پیاده کنید.

جمع‌بندی

SOLID یک چک‌لیست دستوری نیست که باید همیشه و در هر شرایطی کورکورانه دنبال شود، بلکه مجموعه‌ای از راهنماهاست که وقتی کد در حال رشد و پیچیده شدن است، مسیر را برایتان روشن می‌کنند. شروع خوب این است که با اصل تک مسئولیتی (S) و اصل وارونگی وابستگی (D) کار را آغاز کنید، چون این دو بیشترین تأثیر را در کاهش کوپلینگ و افزایش قابلیت تست کد دارند؛ سه اصل دیگر معمولاً به‌طور طبیعی وقتی معماری کد تمیزتر می‌شود، خودشان را نشان می‌دهند.

منبع

Codenight – توضیح اصول SOLID

ادمین

نویسنده:

ادمین
دقیقه:12
انتشار :۱۴۰۵/۷/۵
آپدیت :۱۴۰۵/۷/۵

دسته بندی ها

بک‌اندفرانت‌اند

تگ ها

#SOLID#Clean Code#OOP#Software Architecture#Design Principles#TypeScript