وقتی یک پروژه Laravel را شروع میکنید، معمولاً ساختن Route، نوشتن Controller، تعریف Model و ذخیره اطلاعات در دیتابیس کار دشواری نیست. اما با بزرگتر شدن پروژه، شرایط تغییر میکند. ممکن است یک Controller صدها خط کد داشته باشد، اضافه کردن درگاه پرداخت جدید مستلزم تغییر چندین کلاس شود یا تغییر یک سرویس باعث خراب شدن بخشهای دیگر برنامه شود.
در چنین شرایطی، صرفاً بلد بودن Laravel کافی نیست؛ باید بتوانیم کدها را به شکلی طراحی کنیم که توسعه، نگهداری و تست آنها آسانتر باشد.
یکی از مجموعه اصول مهم در طراحی شیگرا، SOLID است. این نام از حروف اول پنج اصل طراحی نرمافزار تشکیل شده است که به ما کمک میکنند مسئولیت کلاسها را بهتر تقسیم کنیم، وابستگیها را کاهش دهیم و قابلیت توسعه برنامه را افزایش دهیم.
در این مقاله هر پنج اصل SOLID را با مثالهای عملی در پروژههای Laravel بررسی میکنیم. برای مثالها از سناریوی یک فروشگاه اینترنتی استفاده خواهیم کرد تا ببینیم این اصول در کد واقعی چه کاربردی دارند.
SOLID چیست و چرا در Laravel اهمیت دارد؟
SOLID مجموعهای از پنج اصل طراحی شیگراست:
| حرف | نام اصل | مفهوم اصلی |
|---|---|---|
| S | Single Responsibility Principle | هر کلاس مسئولیت مشخصی داشته باشد |
| O | Open/Closed Principle | توسعه قابلیتها بدون تغییرات غیرضروری در کد موجود |
| L | Liskov Substitution Principle | پیادهسازیهای جایگزین باید قابل استفاده بهجای نوع پایه باشند |
| I | Interface Segregation Principle | Interfaceها باید متناسب با نیاز مصرفکنندگان طراحی شوند |
| D | Dependency Inversion Principle | وابستگی به انتزاعها، نه جزئیات پیادهسازی |
این اصول به Laravel محدود نیستند و در زبانها و فریمورکهای مختلف قابل استفادهاند. بااینحال، امکاناتی مانند Service Container، Dependency Injection، Service Provider و Interfaceها در Laravel پیادهسازی آنها را آسانتر میکنند.
هدف SOLID این نیست که تعداد کلاسها را افزایش دهیم یا برای هر عملیات ساده یک Interface بسازیم. هدف، طراحی کدی است که در برابر تغییرات متداول پروژه انعطافپذیر باشد.
پیشنیازهای یادگیری SOLID در Laravel
برای استفاده از مثالهای این مقاله بهتر است با موارد زیر آشنا باشید:
- کلاسها و متدها در PHP
- وراثت و Interfaceها
- Controller و Model در Laravel
- Dependency Injection
- آشنایی مقدماتی با Service Layer
اگر با تزریق وابستگی آشنایی ندارید، ابتدا مقاله «Dependency Injection در Laravel» را مطالعه کنید؛ زیرا اصل پنجم SOLID ارتباط مستقیمی با این مفهوم دارد.
۱. اصل Single Responsibility Principle یا SRP
اصل مسئولیت واحد بیان میکند:
هر کلاس باید یک مسئولیت مشخص داشته باشد و تنها یک دلیل اصلی برای تغییر داشته باشد.
منظور از مسئولیت واحد این نیست که هر کلاس فقط یک متد داشته باشد. ممکن است یک کلاس چندین متد داشته باشد، اما همه آنها باید در راستای یک مسئولیت منسجم باشند.
مثال اشتباه: انجام چند وظیفه در یک کلاس
فرض کنید در فروشگاه اینترنتی، کلاسی برای ثبت سفارش نوشتهایم:
<?php
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Mail;
class OrderService
{
public function createOrder(array $data): Order
{
$order = Order::create($data);
$total = $order->items()->sum('price');
$order->update([
'total' => $total,
]);
Mail::raw(
'Your order has been created.',
function ($message) use ($order) {
$message->to($order->user->email)
->subject('Order Created');
}
);
return $order;
}
}
این کلاس چند مسئولیت متفاوت را انجام میدهد:
- ایجاد سفارش
- محاسبه مبلغ سفارش
- ارسال ایمیل اطلاعرسانی
در نتیجه، اگر روش محاسبه قیمت تغییر کند یا سیستم ارسال ایمیل عوض شود، کلاس OrderService نیز باید تغییر کند.
این وابستگیها نگهداری کد را دشوارتر میکنند.
نکته: کد بالا صرفاً برای نشان دادن مشکل طراحی است. در پروژه واقعی، محاسبه مبلغ سفارش باید با منطق صحیح قیمتگذاری، تخفیف و اقلام سفارش انجام شود و نباید بدون توجه به ساختار دادهها از روی یک ستون فرضی محاسبه شود.
راهکار: تقسیم مسئولیتها
میتوانیم مسئولیت ارسال ایمیل را به کلاس مستقلی منتقل کنیم.
فایل app/Services/OrderNotificationService.php:
<?php
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Mail;
class OrderNotificationService
{
public function sendOrderCreatedEmail(
Order $order
): void {
Mail::raw(
'Your order has been created.',
function ($message) use ($order) {
$message->to($order->user->email)
->subject('Order Created');
}
);
}
}
حالا کلاس مدیریت سفارش میتواند بر عملیات مربوط به سفارش تمرکز کند:
<?php
namespace App\Services;
use App\Models\Order;
class OrderService
{
public function __construct(
protected OrderNotificationService $notifications
) {}
public function createOrder(array $data): Order
{
$order = Order::create($data);
$this->notifications
->sendOrderCreatedEmail($order);
return $order;
}
}
در این ساختار، مسئولیت ارسال ایمیل به کلاس جداگانهای سپرده شده است.
البته این مثال هنوز ساده است. در پروژه واقعی ممکن است بخواهیم ارسال اعلان را با Notificationهای خود Laravel، رویدادها یا صفها انجام دهیم. انتخاب روش به نیازهای برنامه بستگی دارد.
SRP در Controllerهای Laravel
یکی از نمونههای رایج نقض SRP، Controllerهای بسیار بزرگ است.
برای مثال، اگر متد store() در یک Controller هم اعتبارسنجی انجام دهد، هم سفارش بسازد، هم مبلغ را محاسبه کند، هم پرداخت را مدیریت کند و هم پیامک بفرستد، بهتر است مسئولیتها را بررسی کنیم.
تقسیم مسئولیتها میتواند شامل موارد زیر باشد:
- اعتبارسنجی درخواست با Form Request
- اجرای منطق کسبوکار در یک Service
- مدیریت ارسال اعلان در Notification یا سرویس مناسب
- مدیریت تراکنش دیتابیس در محل مناسب
قرار نیست همه این موارد حتماً به کلاسهای جداگانه تبدیل شوند. معیار اصلی، پیچیدگی واقعی کد و مسئولیتهای متفاوت آن است.
چه زمانی از SRP استفاده کنیم؟
وقتی تغییر در یک بخش از کلاس معمولاً به دلیل مستقلی از سایر بخشها اتفاق میافتد، یا کلاس برای انجام چند وظیفه نامرتبط طراحی شده است، بررسی SRP میتواند به بازطراحی مناسب منجر شود.
۲. اصل Open/Closed Principle یا OCP
اصل بازبودن برای توسعه و بستهبودن برای تغییر میگوید:
کد باید برای افزودن قابلیتهای جدید قابل توسعه باشد، بدون اینکه برای هر قابلیت جدید مجبور شویم منطق اصلی و پایدار آن را تغییر دهیم.
این اصل به معنای ممنوعیت مطلق تغییر کد موجود نیست. هدف این است که در بخشهایی که احتمال توسعه دارند، طراحی مناسبی برای افزودن رفتار جدید داشته باشیم.
مثال اشتباه: شرطهای متعدد برای روش پرداخت
فرض کنید فروشگاه ما از یک روش پرداخت استفاده میکند:
<?php
namespace App\Services;
class PaymentService
{
public function pay(
string $gateway,
int $amount
): bool {
if ($gateway === 'zarinpal') {
// اتصال به زرینپال
return true;
}
if ($gateway === 'sadad') {
// اتصال به سداد
return true;
}
if ($gateway === 'behpardakht') {
// اتصال به بهپرداخت
return true;
}
throw new \InvalidArgumentException(
'Unsupported payment gateway.'
);
}
}
هر بار که بخواهیم درگاه جدیدی اضافه کنیم، باید همین کلاس را تغییر دهیم.
اگر تعداد درگاهها زیاد شود، شرطها افزایش پیدا میکنند و تست کردن هر تغییر دشوارتر میشود.
راهکار: استفاده از قرارداد مشترک
ابتدا یک Interface برای درگاه پرداخت تعریف میکنیم.
فایل app/Contracts/PaymentGateway.php:
<?php
namespace App\Contracts;
interface PaymentGateway
{
public function pay(int $amount): bool;
}
سپس هر درگاه، این قرارداد را پیادهسازی میکند.
فایل app/Services/Payment/ZarinpalGateway.php:
<?php
namespace App\Services\Payment;
use App\Contracts\PaymentGateway;
class ZarinpalGateway implements PaymentGateway
{
public function pay(int $amount): bool
{
// ارسال درخواست واقعی به درگاه زرینپال
return true;
}
}
فایل app/Services/Payment/SadadGateway.php:
<?php
namespace App\Services\Payment;
use App\Contracts\PaymentGateway;
class SadadGateway implements PaymentGateway
{
public function pay(int $amount): bool
{
// ارسال درخواست واقعی به درگاه سداد
return true;
}
}
در مثال آموزشی بالا، بدنه متدها شبیهسازی شده است؛ درگاه واقعی باید بر اساس مستندات ارائهدهنده، درخواست، امضا یا رمزنگاری، پاسخ و وضعیت تراکنش را بهدرستی مدیریت کند.
اکنون میتوانیم کلاس دیگری برای اجرای فرایند پرداخت بنویسیم:
<?php
namespace App\Services;
use App\Contracts\PaymentGateway;
class CheckoutService
{
public function __construct(
protected PaymentGateway $gateway
) {}
public function checkout(int $amount): bool
{
return $this->gateway->pay($amount);
}
}
کلاس CheckoutService به جزئیات زرینپال یا سداد وابسته نیست؛ بلکه با قرارداد PaymentGateway کار میکند.
برای انتخاب درگاه مورد نظر، میتوانیم از Service Container و Binding استفاده کنیم. اگر فقط یک پیادهسازی پیشفرض داریم، اتصال آن در AppServiceProvider میتواند کافی باشد:
use App\Contracts\PaymentGateway;
use App\Services\Payment\ZarinpalGateway;
public function register(): void
{
$this->app->bind(
PaymentGateway::class,
ZarinpalGateway::class
);
}
اگر کاربر بتواند بین چند درگاه انتخاب کند، یک Binding ثابت بهتنهایی برای انتخاب پویا کافی نیست. در آن حالت، میتوان از Factory یا یک Registry برای انتخاب پیادهسازی مناسب استفاده کرد.
مزیت این ساختار چیست؟
با اضافه کردن یک درگاه جدید، میتوانیم کلاس جدیدی بسازیم که PaymentGateway را پیادهسازی کند. منطق اصلی CheckoutService برای هر درگاه جدید نیازی به تغییر ندارد.
این ساختار همچنین امکان تست کردن فرایند پرداخت با یک پیادهسازی آزمایشی را فراهم میکند.
چه زمانی از OCP استفاده کنیم؟
در بخشهایی که احتمال اضافه شدن پیادهسازیهای جدید وجود دارد، مانند روشهای پرداخت، روشهای ارسال کالا، تخفیفها یا ارائهدهندگان سرویس خارجی، طراحی قابل توسعه میتواند مفید باشد.
برای یک قابلیت ساده با تنها یک حالت ثابت، نیازی نیست از ابتدا چندین Interface و Factory بسازیم.
۳. اصل Liskov Substitution Principle یا LSP
اصل جایگزینی لیسکوف بیان میکند که:
اگر یک کلاس، نوعی از کلاس پایه یا قراردادی مشخص را پیادهسازی میکند، باید بتوان از آن در جایگاه نوع پایه استفاده کرد، بدون اینکه رفتار مورد انتظار برنامه به شکل نامعتبر تغییر کند.
این اصل صرفاً به این معنا نیست که دو کلاس متدهای یکسانی داشته باشند. پیادهسازی جایگزین باید به قرارداد رفتاری پایبند بماند.
مثال اشتباه: پیادهسازیای که قرارداد را نقض میکند
فرض کنید در فروشگاه اینترنتی، قراردادی برای محاسبه هزینه ارسال تعریف کردهایم:
<?php
namespace App\Contracts;
interface ShippingCalculator
{
public function calculate(int $weight): int;
}
قرار است تمام کلاسهایی که این Interface را پیادهسازی میکنند، با دریافت وزن کالا، هزینه ارسال را برگردانند.
یک پیادهسازی ساده:
<?php
namespace App\Services\Shipping;
use App\Contracts\ShippingCalculator;
class StandardShippingCalculator
implements ShippingCalculator
{
public function calculate(int $weight): int
{
return $weight * 1000;
}
}
اکنون تصور کنید پیادهسازی دیگری میسازیم:
<?php
namespace App\Services\Shipping;
use App\Contracts\ShippingCalculator;
class FreeShippingCalculator
implements ShippingCalculator
{
public function calculate(int $weight): int
{
throw new \RuntimeException(
'Shipping is not available.'
);
}
}
این کلاس نام «ارسال رایگان» را دارد، اما متد آن بهجای بازگرداندن هزینه ارسال، خطا ایجاد میکند. اگر کد مصرفکننده انتظار داشته باشد که متد یک عدد برگرداند، این پیادهسازی رفتار مورد انتظار را نقض میکند.
راهکار: حفظ قرارداد رفتاری
اگر منظور از ارسال رایگان، هزینه صفر است، پیادهسازی باید همان را برگرداند:
<?php
namespace App\Services\Shipping;
use App\Contracts\ShippingCalculator;
class FreeShippingCalculator
implements ShippingCalculator
{
public function calculate(int $weight): int
{
return 0;
}
}
در این صورت، هر دو پیادهسازی با قرارداد ShippingCalculator سازگارند و کد مصرفکننده میتواند بدون دانستن جزئیات کلاس، متد calculate() را فراخوانی کند.
اگر بعضی روشهای ارسال واقعاً در شرایط خاص قابل استفاده نیستند، بهتر است این محدودیت را در طراحی قرارداد و فرایند انتخاب روش ارسال منعکس کنیم؛ برای مثال، با یک متد برای بررسی قابلیت ارسال یا با تعریف یک قرارداد متفاوت.
نکات مهم در رعایت LSP
- متد بازنویسیشده نباید بدون دلیل، ورودیهای معتبر قرارداد پایه را رد کند.
- خروجی باید با نوع و معنای مورد انتظار قرارداد سازگار باشد.
- پیادهسازی جایگزین نباید شرایط و محدودیتهای غیرمنتظرهای برای مصرفکننده ایجاد کند.
- خطاها و استثناها باید با قرارداد رفتاری سیستم سازگار باشند.
LSP در طراحی درگاههای پرداخت، سیستم ارسال، ذخیرهسازی فایل و سرویسهای خارجی اهمیت زیادی دارد؛ زیرا در این بخشها معمولاً چند پیادهسازی مختلف برای یک قرارداد وجود دارد.
۴. اصل Interface Segregation Principle یا ISP
اصل تفکیک Interface میگوید:
کلاسها نباید مجبور باشند به متدهایی وابسته شوند که به آنها نیازی ندارند.
به بیان ساده، بهتر است چند Interface کوچک و مشخص داشته باشیم تا یک Interface بزرگ با تعداد زیادی عملیات نامرتبط.
مثال اشتباه: Interface بیش از حد بزرگ
فرض کنید برای مدیریت فایلها، Interface زیر را طراحی کردهایم:
<?php
namespace App\Contracts;
interface FileManager
{
public function upload(string $path): string;
public function download(string $path): string;
public function delete(string $path): bool;
public function generateThumbnail(string $path): string;
}
این قرارداد چهار مسئولیت متفاوت را در یک Interface قرار داده است.
حالا فرض کنید یک سرویس فقط باید فایلها را دانلود کند. اگر بخواهیم آن را به FileManager وابسته کنیم، مجبور میشود قراردادی را بشناسد که شامل آپلود، حذف و تولید تصویر بندانگشتی نیز هست.
این موضوع بهخصوص زمانی مشکلساز میشود که پیادهسازیهای مختلف، همه این قابلیتها را پشتیبانی نکنند.
راهکار: تقسیم Interfaceها بر اساس قابلیت
میتوانیم قراردادها را به بخشهای کوچکتر تقسیم کنیم.
فایل app/Contracts/FileUploader.php:
<?php
namespace App\Contracts;
interface FileUploader
{
public function upload(string $path): string;
}
فایل app/Contracts/FileDownloader.php:
<?php
namespace App\Contracts;
interface FileDownloader
{
public function download(string $path): string;
}
فایل app/Contracts/FileDeleter.php:
<?php
namespace App\Contracts;
interface FileDeleter
{
public function delete(string $path): bool;
}
اکنون کلاسی که فقط به دانلود نیاز دارد، میتواند تنها به FileDownloader وابسته شود.
<?php
namespace App\Services;
use App\Contracts\FileDownloader;
class DocumentService
{
public function __construct(
protected FileDownloader $downloader
) {}
public function downloadDocument(string $path): string
{
return $this->downloader->download($path);
}
}
در این ساختار، DocumentService فقط قابلیت مورد نیاز خود را میشناسد.
البته اگر یک پیادهسازی همه قابلیتها را دارد، میتواند چند Interface را همزمان پیادهسازی کند. هدف ISP این نیست که حتماً هر Interface در یک کلاس جداگانه پیادهسازی شود؛ هدف، کوچک و معنادار بودن قراردادهاست.
ISP در پروژههای Laravel
این اصل میتواند در طراحی موارد زیر مفید باشد:
- سرویسهای فایل و رسانه
- ارائهدهندگان پرداخت
- سرویسهای گزارشگیری
- سیستم اعلانها
- سرویسهای مرتبط با APIهای خارجی
برای مثال، اگر یک سرویس گزارشگیری فقط به خروجی CSV نیاز دارد، نباید به قراردادی وابسته شود که عملیات PDF، Excel و ارسال ایمیل را نیز الزامی میکند.
چه زمانی از ISP استفاده کنیم؟
وقتی یک Interface متدهای زیادی دارد و پیادهسازیهای آن مجبورند برخی متدها را بیاستفاده، خالی یا با خطای «پشتیبانی نمیشود» پیادهسازی کنند، زمان مناسبی برای بررسی ISP است.
۵. اصل Dependency Inversion Principle یا DIP
اصل وارونگی وابستگی بیان میکند:
ماژولهای سطح بالا نباید به جزئیات سطح پایین وابسته باشند؛ هر دو باید به انتزاعها وابسته باشند.
برای درک بهتر این اصل، باید تفاوت میان دو مفهوم را بشناسیم:
- ماژول سطح بالا: بخشی که منطق اصلی کسبوکار را اجرا میکند.
- جزئیات سطح پایین: بخشی که کار مشخصی مانند ارسال پیامک، ذخیره فایل یا اتصال به یک سرویس خارجی را انجام میدهد.
در یک فروشگاه اینترنتی، منطق ثبت سفارش نباید مستقیماً به یک ارائهدهنده خاص پیامک وابسته باشد.
مثال اشتباه: وابستگی مستقیم به یک سرویس
<?php
namespace App\Services;
class OrderNotificationService
{
public function send(string $phone): void
{
$sms = new ThirdPartySmsService();
$sms->send(
$phone,
'Your order has been created.'
);
}
}
در این مثال، کلاس OrderNotificationService خودش نمونهای از ThirdPartySmsService ایجاد میکند.
اگر ارائهدهنده پیامک تغییر کند یا بخواهیم در تست از سرویس آزمایشی استفاده کنیم، باید کد کلاس اصلی را تغییر دهیم.
راهکار: وابستگی به Interface
ابتدا یک قرارداد برای ارسال پیامک تعریف میکنیم.
فایل app/Contracts/SmsSender.php:
<?php
namespace App\Contracts;
interface SmsSender
{
public function send(
string $phone,
string $message
): void;
}
سپس کلاس اصلی را به این قرارداد وابسته میکنیم:
<?php
namespace App\Services;
use App\Contracts\SmsSender;
class OrderNotificationService
{
public function __construct(
protected SmsSender $smsSender
) {}
public function send(string $phone): void
{
$this->smsSender->send(
$phone,
'Your order has been created.'
);
}
}
اکنون کلاس اصلی به یک Interface وابسته است، نه به پیادهسازی مشخص سرویس پیامک.
هر کلاس دیگری که قرارداد SmsSender را پیادهسازی کند، میتواند برای این سرویس استفاده شود؛ البته به شرطی که رفتار مورد انتظار قرارداد را رعایت کند.
اتصال Interface به پیادهسازی در Laravel
برای اینکه Laravel بداند کدام کلاس را باید به SmsSender تزریق کند، میتوانیم در app/Providers/AppServiceProvider.php آن را ثبت کنیم:
<?php
namespace App\Providers;
use App\Contracts\SmsSender;
use App\Services\LogSmsSender;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
SmsSender::class,
LogSmsSender::class
);
}
public function boot(): void
{
//
}
}
در این مثال، LogSmsSender باید SmsSender را پیادهسازی کرده باشد. این کلاس میتواند برای محیط توسعه یا آزمایش، اطلاعات پیام را در لاگ ثبت کند.
در محیط عملیاتی، میتوانید پیادهسازی واقعی سرویس پیامک را جایگزین کنید؛ به شرط آنکه قرارداد مورد نیاز را رعایت کند.
تفاوت DIP و Dependency Injection
این دو مفهوم با یکدیگر مرتبطاند، اما یکسان نیستند.
Dependency Inversion Principle یک اصل طراحی است که توصیه میکند وابستگیها به انتزاعها متکی باشند.
در مقابل، Dependency Injection روشی برای تأمین وابستگیهای یک کلاس از بیرون آن است.
برای مثال، تزریق مستقیم ThirdPartySmsService به یک کلاس میتواند Dependency Injection باشد، اما لزوماً اصل DIP را بهطور کامل رعایت نمیکند؛ زیرا کلاس همچنان به یک پیادهسازی مشخص وابسته است.
ترکیب Interface، Dependency Injection و Service Container در Laravel راهی متداول برای پیادهسازی این اصل است.
مقایسه پنج اصل SOLID
جدول زیر کمک میکند تشخیص دهید هر اصل کدام نوع مشکل طراحی را هدف قرار میدهد.
| اصل | سؤال کلیدی | نشانه مشکل |
|---|---|---|
| SRP | آیا کلاس مسئولیتهای نامرتبط دارد؟ | تغییرات مختلف، یک کلاس را درگیر میکنند |
| OCP | آیا هر قابلیت جدید مستلزم تغییر منطق اصلی است؟ | شرطهای متعدد برای انواع رفتار |
| LSP | آیا پیادهسازی جایگزین، قرارداد را رعایت میکند؟ | کلاس جایگزین رفتار غیرمنتظره دارد |
| ISP | آیا کلاس به متدهای غیرضروری وابسته است؟ | Interface بزرگ و پیادهسازیهای ناقص |
| DIP | آیا منطق اصلی به جزئیات مشخص وابسته است؟ | تعویض سرویس خارجی نیازمند تغییر کلاس اصلی است |
چگونه اصول SOLID را در یک پروژه واقعی Laravel اجرا کنیم؟
فرض کنید یک فروشگاه اینترنتی دارید که ثبت سفارش، پرداخت، ارسال کالا و اطلاعرسانی را انجام میدهد.
برای استفاده منطقی از SOLID میتوانید مراحل زیر را دنبال کنید.
مرحله اول: مسئولیتهای کلاسها را مشخص کنید
ابتدا بررسی کنید هر کلاس چه وظیفهای دارد. اگر یک Controller همزمان منطق پرداخت، محاسبه تخفیف، ثبت سفارش و ارسال پیامک را اجرا میکند، مسئولیتها را بررسی و در صورت نیاز جدا کنید.
مرحله دوم: نقاط قابل توسعه را شناسایی کنید
همه بخشهای برنامه به یک اندازه نیازمند انتزاع نیستند. روی قسمتهایی تمرکز کنید که احتمال تغییر یا اضافه شدن پیادهسازی در آنها بیشتر است؛ مانند درگاه پرداخت یا ارائهدهندگان پیامک.
مرحله سوم: قراردادهای مناسب تعریف کنید
برای قابلیتهایی که واقعاً به پیادهسازیهای جایگزین نیاز دارند، Interface تعریف کنید. قرارداد باید رفتار مورد انتظار را مشخص کند، نه اینکه صرفاً برای هر کلاس یک Interface ساخته شود.
مرحله چهارم: وابستگیها را تزریق کنید
بهجای ساختن مستقیم سرویسهای خارجی در کلاسهای اصلی، آنها را از طریق سازنده یا روش مناسب دیگر تزریق کنید.
مرحله پنجم: از Service Container استفاده کنید
در صورت نیاز، Interface را به پیادهسازی مناسب متصل کنید. Laravel بسیاری از کلاسهای معمولی را بهصورت خودکار ایجاد میکند، بنابراین لازم نیست همه کلاسها را دستی ثبت کنید.
مرحله ششم: تست بنویسید
برای کلاسهای اصلی، تستهایی بنویسید که رفتار مورد انتظار را بررسی کنند. استفاده از Fake یا Mock میتواند به تست مستقل از سرویسهای خارجی کمک کند.
مرحله هفتم: از پیچیدگی غیرضروری جلوگیری کنید
اگر یک بخش ساده است و احتمال توسعه خاصی ندارد، ممکن است ساختن چند Interface، Factory و Service برای آن ارزش چندانی نداشته باشد. اصول SOLID ابزار تصمیمگیریاند، نه مجموعهای از قوانین برای افزایش تعداد فایلها.
اشتباهات رایج در استفاده از SOLID
۱. ساختن Interface برای تمام کلاسها
داشتن Interface برای هر کلاس الزام نیست. Interface زمانی ارزشمند است که قرارداد مستقلی برای مصرفکننده تعریف کند یا نیاز به پیادهسازیهای جایگزین وجود داشته باشد.
۲. تبدیل هر متد به یک Service مستقل
تقسیم مسئولیتها به معنای ساختن کلاسهای بسیار کوچک و پراکنده نیست. کلاسها باید مسئولیت منسجم و قابل توضیح داشته باشند.
۳. اشتباه گرفتن SRP با یک متد در هر کلاس
یک کلاس میتواند چند متد داشته باشد و همچنان SRP را رعایت کند. معیار اصلی، انسجام مسئولیتهاست.
۴. استفاده از Interface بدون رعایت LSP
اگر دو کلاس Interface یکسانی دارند، اما یکی از آنها رفتار مورد انتظار قرارداد را رعایت نمیکند، طراحی همچنان مشکل دارد.
۵. پیادهسازی بیش از حد پیچیده از ابتدا
لازم نیست قبل از مشخص شدن نیازهای واقعی، همه بخشهای برنامه را برای دهها پیادهسازی احتمالی آماده کنیم. بهتر است ساختار را متناسب با نیاز فعلی و مسیر منطقی توسعه طراحی کنیم.
۶. تصور اینکه SOLID بهتنهایی امنیت یا عملکرد را تضمین میکند
رعایت SOLID به کیفیت طراحی کمک میکند، اما جایگزین اعتبارسنجی ورودی، مجوزدهی، مدیریت تراکنش، بررسی خطاهای امنیتی و بهینهسازی عملکرد نیست.
ارتباط SOLID با Service Layer و Repository Pattern
SOLID در کنار الگوهای معماری دیگر معنا پیدا میکند.
Service Layer کمک میکند منطق کسبوکار از بخشهایی مانند Controller جدا شود. Repository Pattern میتواند دسترسی به دادهها را از منطق مصرفکننده جدا کند. Dependency Injection نیز نحوه تأمین وابستگیها را مدیریت میکند.
SOLID چارچوبی برای ارزیابی طراحی این اجزاست؛ مثلاً بررسی میکنیم آیا Service مسئولیت بیش از حد دارد، آیا Repository از طریق یک قرارداد قابل جایگزینی است و آیا کلاسهای اصلی به جزئیات ذخیرهسازی وابسته شدهاند یا خیر.
البته استفاده از Repository برای همه پروژههای Laravel ضروری نیست. در بسیاری از پروژهها، Eloquent بهتنهایی پاسخگوی نیازهاست. انتخاب معماری باید بر اساس پیچیدگی و نیاز واقعی برنامه انجام شود.
جمعبندی
اصول SOLID به ما کمک میکنند پروژههای Laravel را طوری طراحی کنیم که تغییر، توسعه و تست آنها سادهتر باشد.
- SRP: هر کلاس مسئولیت منسجم و مشخصی داشته باشد.
- OCP: قابلیتهای جدید را با کمترین تغییر غیرضروری در منطق پایدار اضافه کنیم.
- LSP: پیادهسازیهای جایگزین، قرارداد رفتاری را رعایت کنند.
- ISP: Interfaceها را متناسب با نیاز مصرفکنندگان طراحی کنیم.
- DIP: منطق اصلی را به انتزاعها وابسته کنیم، نه به جزئیات پیادهسازی.
برای شروع، لازم نیست تمام پروژه را یکباره بازنویسی کنید. یک Controller پیچیده یا یک سرویس وابسته به ارائهدهنده خارجی را انتخاب کنید، مسئولیتهای آن را بررسی کنید و سپس با توجه به نیاز واقعی، ساختار را بهبود دهید.
در نهایت، کد خوب کدی نیست که بیشترین تعداد کلاس و Interface را داشته باشد؛ کدی است که درک آن آسان باشد و تغییرات مورد نیاز را با کمترین ریسک بپذیرد.
سوالات متداول
اصول SOLID در Laravel چیست؟
SOLID مجموعهای از پنج اصل طراحی شیگراست که به بهبود نگهداری، توسعهپذیری و تستپذیری نرمافزار کمک میکند. این اصول شامل مسئولیت واحد، بازبودن برای توسعه، جایگزینی لیسکوف، تفکیک Interface و وارونگی وابستگی هستند.
آیا رعایت SOLID در تمام پروژههای Laravel ضروری است؟
استفاده از این اصول در همه بخشها به یک اندازه ضروری نیست. در پروژههای کوچک ممکن است ساختار ساده کافی باشد؛ اما با افزایش پیچیدگی، رعایت اصول طراحی میتواند تغییر و نگهداری برنامه را آسانتر کند.
تفاوت SOLID با Design Pattern چیست؟
SOLID مجموعهای از اصول طراحی است که به ارزیابی کیفیت ساختار کد کمک میکند. Design Pattern راهحل شناختهشدهای برای مسئلهای تکرارشونده در طراحی نرمافزار است. این دو میتوانند مکمل یکدیگر باشند.
کدام اصل SOLID با Dependency Injection ارتباط دارد؟
اصل Dependency Inversion Principle یا DIP ارتباط مستقیمی با طراحی وابستگیها دارد. Dependency Injection یکی از روشهای تأمین وابستگیهاست که میتواند در پیادهسازی این اصل به کار رود.
آیا استفاده از Service Layer به معنی رعایت SOLID است؟
خیر. Service Layer میتواند به تفکیک مسئولیتها کمک کند، اما یک Service نیز ممکن است مسئولیتهای بیش از حد داشته باشد یا به پیادهسازیهای مشخص وابسته شود. کیفیت طراحی به نحوه استفاده از این ساختار بستگی دارد.
برای یادگیری SOLID در Laravel از کجا شروع کنیم؟
ابتدا مفاهیم کلاس، Interface و Dependency Injection در PHP و Laravel را یاد بگیرید. سپس هر اصل را با یک مثال واقعی پیادهسازی کنید و در ادامه تستنویسی و بازطراحی کد را تمرین کنید.





