اصول SOLID یکی از پایهایترین مفاهیم در طراحی شیگرا (OOP) هست که توسط Robert C. Martin (Uncle Bob) معرفی شد. هدفش اینه که کد تمیزتر، قابلتوسعهتر و انعطافپذیرتر نوشته بشه.
اصول SOLID
1. Single Responsibility Principle (SRP) – اصل تکوظیفهای
هر کلاس باید فقط یک دلیل برای تغییر داشته باشه.
- یعنی هر کلاس فقط مسئول یک کار خاص باشه.
- این باعث میشه کلاسها کوچک، خوانا و تستپذیر بشن.
مثال:
کلاسی که هم وظیفه ذخیره داده در دیتابیس رو داره و هم تولید PDF میکنه، باید به دو کلاس جدا تقسیم بشه (DatabaseSaver و PdfGenerator).
2. Open/Closed Principle (OCP) – اصل باز/بسته
کلاسها باید برای گسترش باز باشن اما برای تغییر بسته باشن.
- یعنی به جای تغییر مستقیم کد یک کلاس، باید بتونیم رفتار جدید رو با ارثبری یا اینترفیس اضافه کنیم.
مثال:
برای محاسبه مالیات، بهجای تغییر کد کلاس اصلی، انواع مالیات جدید رو با کلاسهای جدا اضافه میکنیم.
3. Liskov Substitution Principle (LSP) – اصل جانشینی لیسکوف
هر کلاسی که از کلاس دیگری مشتق شده، باید بتونه جایگزین کلاس والد بشه بدون اینکه رفتار سیستم تغییر کنه.
- اگر یک تابع از کلاس پدر استفاده میکنه، باید بتونه بدون مشکل از کلاس فرزند هم استفاده کنه.
مثال:
اگر کلاس Bird
متد fly()
داره، کلاس Penguin
نباید از اون ارثبری کنه، چون نمیتونه پرواز کنه → نقض LSP.
4. Interface Segregation Principle (ISP) – اصل تفکیک رابطها
هیچ کلاسی نباید مجبور باشه متدهایی رو پیادهسازی کنه که بهشون نیاز نداره.
- اینترفیسها باید کوچک و تخصصی طراحی بشن.
مثال:
بهجای یک اینترفیس بزرگ مثل IMachine
(که شامل متدهای print, scan, fax هست)، چند اینترفیس کوچکتر مثل IPrinter
, IScanner
, IFax
داشته باشیم.
5. Dependency Inversion Principle (DIP) – اصل معکوسسازی وابستگی
ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشن؛ هر دو باید به انتزاع وابسته باشن.
- وابستگیها باید به اینترفیس یا abstraction باشه، نه به کلاسهای خاص.
مثال:
بهجای اینکه کلاس Report
مستقیم از کلاس SqlDatabase
استفاده کنه، باید به اینترفیس IDatabase
وابسته باشه، تا هر نوع دیتابیس (SQL, Mongo, File) قابل جایگزینی باشه.
نمونهی یکپارچه با TypeScript که هر پنج اصل SOLID رو در یک دامنهی سادهی «ثبت سفارش» نشان میدهد.
(کد ماژولار است؛ هر کلاس دقیقاً یک مسئولیت دارد، وابستگیها از طریق اینترفیسها تزریق میشوند، و با افزودن انواع جدید، بدون ویرایش کلاسهای موجود گسترش میدهیم.)
/******************************
* Domain: Order Checkout
* اصول پوششدادهشده:
* SRP: هر کلاس یک مسئولیت
* OCP: افزودن استراتژی تخفیف/پرداخت/اعلان بدون تغییر بقیه
* LSP: تمام پیادهسازیها قابلجایگزینی با اینترفیس والد
* ISP: رابطهای کوچک و هدفمند (ایمیل/اساماس)
* DIP: ماژول سطح بالا (OrderService) به انتزاعات وابسته است
******************************/
/***************
* 1) Abstractions (DIP هدف اصلی)
***************/
export interface DiscountPolicy {
/** مقدار نهایی پس از اعمال تخفیف را برمیگرداند */
apply(total: number): number;
}
export interface PaymentProcessor {
/** مبلغ را پردازش میکند؛ اگر موفق بود reject نمیکند */
pay(amount: number): Promise<void>;
}
export interface Notifier {
notify(message: string): Promise<void>;
}
/** ISP: رابطهای کوچک و هدفمند برای کانالها */
export interface EmailSender {
sendEmail(to: string, subject: string, html: string): Promise<void>;
}
export interface SmsSender {
sendSms(to: string, body: string): Promise<void>;
}
/** مخزن سفارشها (Persistence) */
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
/***************
* 2) Entities & Value Objects (SRP)
***************/
export class OrderItem {
constructor(
public readonly sku: string,
public readonly price: number,
public readonly qty: number
) {
if (qty <= 0) throw new Error('Quantity must be positive');
if (price < 0) throw new Error('Price cannot be negative');
}
get lineTotal(): number {
return this.price * this.qty;
}
}
export class Order {
private _paid = false;
constructor(
public readonly id: string,
public readonly customerEmail: string,
public readonly customerPhone: string,
private readonly items: OrderItem[]
) {
if (!id) throw new Error('Order id is required');
if (items.length === 0) throw new Error('Order must have at least one item');
}
get total(): number {
return this.items.reduce((sum, it) => sum + it.lineTotal, 0);
}
markPaid() {
this._paid = true;
}
get isPaid(): boolean {
return this._paid;
}
}
/***************
* 3) Discount policies (OCP: قابل گسترش بدون تغییر کدهای موجود)
***************/
export class NoDiscount implements DiscountPolicy {
apply(total: number): number {
return total;
}
}
export class PercentageDiscount implements DiscountPolicy {
constructor(private readonly percent: number /* 0..100 */) {
if (percent < 0 || percent > 100) throw new Error('Invalid percent');
}
apply(total: number): number {
return Math.max(0, total * (1 - this.percent / 100));
}
}
export class ThresholdFixedDiscount implements DiscountPolicy {
// مثلاً اگر بیشتر از 1,000 بود 100 تا کم کن
constructor(private readonly threshold: number, private readonly fixedOff: number) {}
apply(total: number): number {
return total >= this.threshold ? Math.max(0, total - this.fixedOff) : total;
}
}
/***************
* 4) Payment processors (LSP: هر کدام جای دیگری کار کند)
***************/
export class StripeProcessor implements PaymentProcessor {
constructor(private readonly apiKey: string) {}
async pay(amount: number): Promise<void> {
if (amount <= 0) throw new Error('Amount must be positive');
// فرض: تماس با Stripe SDK
// await stripe.charges.create({ amount: toCents(amount), ... })
await fakeIO('stripe:charge:' + amount);
}
}
export class CashOnDeliveryProcessor implements PaymentProcessor {
async pay(amount: number): Promise<void> {
if (amount <= 0) throw new Error('Amount must be positive');
// COD واقعی پرداخت آنلاین نمیکند؛ اما قرارداد LSP را نقض نمیکند:
// تعهد: اگر قابل قبول بود، reject نکن.
await fakeIO('cod:reserve:' + amount);
}
}
/***************
* 5) Notifiers (ISP: استفاده از کانالهای جدا، LSP: قابلجایگزینی)
***************/
export class EmailNotifier implements Notifier {
constructor(private readonly emailSender: EmailSender, private readonly to: string) {}
async notify(message: string): Promise<void> {
await this.emailSender.sendEmail(this.to, 'Order Update', `<p>${escapeHtml(message)}</p>`);
}
}
export class SmsNotifier implements Notifier {
constructor(private readonly smsSender: SmsSender, private readonly to: string) {}
async notify(message: string): Promise<void> {
await this.smsSender.sendSms(this.to, message);
}
}
/** پیادهسازیهای ساده برای کانالها (زیرساخت) */
export class ConsoleEmailSender implements EmailSender {
async sendEmail(to: string, subject: string, html: string): Promise<void> {
await fakeIO(`EMAIL to=${to} subject=${subject} html=${html}`);
}
}
export class ConsoleSmsSender implements SmsSender {
async sendSms(to: string, body: string): Promise<void> {
await fakeIO(`SMS to=${to} body=${body}`);
}
}
/***************
* 6) Repository (SRP: فقط Persistence)
***************/
export class InMemoryOrderRepository implements OrderRepository {
private readonly store = new Map<string, Order>();
async save(order: Order): Promise<void> {
this.store.set(order.id, order);
await fakeIO('repo:save:' + order.id);
}
async findById(id: string): Promise<Order | null> {
await fakeIO('repo:find:' + id);
return this.store.get(id) ?? null;
}
}
/***************
* 7) Application Service (OrderService)
* SRP: هماهنگی Use-case پرداخت
* DIP: فقط به انتزاعات وابسته است (DiscountPolicy, PaymentProcessor, Notifier, OrderRepository)
***************/
export class OrderService {
constructor(
private readonly repo: OrderRepository,
private readonly discount: DiscountPolicy,
private readonly payment: PaymentProcessor,
private readonly notifiers: Notifier[] // چند اعلان بهصورت ترکیبی
) {}
/** فرآیند پرداخت سفارش */
async checkout(orderId: string): Promise<{ payable: number }> {
const order = await this.repo.findById(orderId);
if (!order) throw new Error('Order not found');
const discounted = this.discount.apply(order.total);
await this.payment.pay(discounted);
order.markPaid();
await this.repo.save(order);
// اطلاعرسانی (ایمیل/اساماس/هر دو)
await Promise.all(
this.notifiers.map((n) => n.notify(`Order ${orderId} has been paid. Amount: ${discounted}`))
);
return { payable: discounted };
}
}
/***************
* 8) Composition Root (Dependency Injection)
***************/
async function demo() {
// ساخت دادهی نمونه
const order = new Order(
'ORD-1001',
'customer@example.com',
'+31123456789',
[new OrderItem('SKU-1', 200, 2), new OrderItem('SKU-2', 150, 1)]
);
// تزریق وابستگیها
const repo: OrderRepository = new InMemoryOrderRepository();
await repo.save(order);
// سیاستهای تخفیف (OCP: میتوانیم بدون تغییر OrderService، یکی را جایگزین کنیم)
const discount: DiscountPolicy = new PercentageDiscount(10);
// const discount: DiscountPolicy = new ThresholdFixedDiscount(500, 50);
// const discount: DiscountPolicy = new NoDiscount();
// پردازندهی پرداخت (LSP: میتوانیم Stripe را با COD عوض کنیم)
const payment: PaymentProcessor = new StripeProcessor('sk_test_123');
// const payment: PaymentProcessor = new CashOnDeliveryProcessor();
// اعلانها (ISP: هر کانالی را که لازم است اضافه میکنیم)
const emailSender = new ConsoleEmailSender();
const smsSender = new ConsoleSmsSender();
const notifiers: Notifier[] = [
new EmailNotifier(emailSender, order.customerEmail),
new SmsNotifier(smsSender, order.customerPhone),
];
const service = new OrderService(repo, discount, payment, notifiers);
const result = await service.checkout(order.id);
console.log('PAYABLE =', result.payable); // -> مبلغ نهایی پس از تخفیف
}
/***************
* Utilities (برای شبیهسازی I/O)
***************/
function fakeIO(label: string, ms = 50): Promise<void> {
return new Promise((resolve) => setTimeout(() => { console.log('[IO]', label); resolve(); }, ms));
}
function escapeHtml(s: string): string {
return s.replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]!));
}
// اجرای دمو
demo().catch((e) => console.error(e));
چطور هر اصل اینجا رعایت شده؟
SRP:
Order
, OrderItem
فقط منطق دامنه را دارند.
InMemoryOrderRepository
ذخیره/بازیابی.
OrderService
اورکستریشن یوزکیس checkout.
EmailNotifier
/SmsNotifier
فقط اعلان.
OCP:
با افزودن کلاسهای جدید مثل PercentageDiscount
یا ThresholdFixedDiscount
، بدون تغییر OrderService
رفتار را گسترش میدهیم.
برای پرداخت نیز با افزودن PayPalProcessor
(مثلاً) همینطور.
LSP:
هر PaymentProcessor
(Stripe، COD، …) میتواند جایگزین دیگری شود بدون شکستن قرارداد pay
.
هر Notifier
قابل جایگزینی است.
ISP:
کانالها به رابطهای کوچک EmailSender
و SmsSender
تفکیک شدهاند؛ نوتیفایرها فقط همان را که نیاز دارند میگیرند.
DIP:
OrderService
فقط به اینترفیسها (OrderRepository
, DiscountPolicy
, PaymentProcessor
, Notifier
) وابسته است؛ نه به کلاسهای پیادهسازی.
با رعایت SOLID:
- کد مقیاسپذیرتر میشه.
- تست و نگهداری سادهتر میشه.
- تغییرات آینده بدون شکستن کل سیستم راحتتر انجام میشه.
لینک مقاله در سایت : medium