لماذا تهم مبادئ SOLID (عند تطبيقها بشكل عملي)
مبادئ SOLID ليست قواعد منقوشة على حجر. إنها إرشادات، عند تطبيقها بعناية، تجعل كودك أسهل في الفهم والاختبار والتعديل. الكلمة المفتاحية هي بعناية — الالتزام الأعمى بمبادئ SOLID قد ينتج كوداً أكثر تعقيداً من المشكلة التي يحلها.
دعنا نرى كيف يبدو كل مبدأ في تطبيق Laravel حقيقي.
S — مبدأ المسؤولية الواحدة
"يجب أن يكون للفئة سبب واحد فقط للتغيير."
هذا لا يعني "يجب أن تفعل الفئة شيئاً واحداً فقط." بل يعني أن الفئة يجب أن تكون مسؤولة أمام جهة فاعلة أو صاحب مصلحة واحد.
قبل: متحكم يفعل الكثير
class OrderController extends Controller
{
public function store(Request $request)
{
// Validation
$validated = $request->validate([
'items' => 'required|array',
'payment_method' => 'required|string',
]);
// Business logic: calculate totals
$subtotal = 0;
foreach ($validated['items'] as $item) {
$product = Product::findOrFail($item['id']);
$subtotal += $product->price * $item['quantity'];
}
$tax = $subtotal * 0.1;
$total = $subtotal + $tax;
// Persistence
$order = Order::create([
'user_id' => auth()->id(),
'subtotal' => $subtotal,
'tax' => $tax,
'total' => $total,
]);
// Payment processing
Stripe::charges()->create([
'amount' => $total * 100,
'currency' => 'usd',
'source' => $validated['payment_method'],
]);
// Notification
Mail::to(auth()->user())->send(new OrderConfirmation($order));
return redirect()->route('orders.show', $order);
}
}
بعد: كل مسؤولية في مكانها
class OrderController extends Controller
{
public function store(StoreOrderRequest $request, PlaceOrder $placeOrder)
{
$order = $placeOrder->execute(
PlaceOrderData::fromRequest($request)
);
return redirect()->route('orders.show', $order);
}
}
class PlaceOrder
{
public function __construct(
private readonly PaymentGateway $payments,
) {}
public function execute(PlaceOrderData $data): Order
{
$order = Order::create($data->toArray());
$this->payments->charge($order->total, $data->paymentMethod);
event(new OrderPlaced($order));
return $order;
}
}
// Side effects handled by event listeners
// OrderPlaced → SendConfirmationEmail
// OrderPlaced → UpdateInventory
// OrderPlaced → NotifySalesTeam
O — مبدأ الفتح/الإغلاق
"مفتوح للتوسعة، مغلق للتعديل." عملياً، هذا يعني أنه يمكنك إضافة سلوك جديد دون تغيير الكود الحالي.
// Instead of a growing switch statement...
class NotificationSender
{
public function send(User $user, string $message, string $channel)
{
match($channel) {
'email' => $this->sendEmail($user, $message),
'sms' => $this->sendSms($user, $message),
'slack' => $this->sendSlack($user, $message),
// Adding a new channel = modifying this class
};
}
}
// ...use an interface and let Laravel's container resolve it
interface NotificationChannel
{
public function send(User $user, string $message): void;
}
class EmailChannel implements NotificationChannel { /* ... */ }
class SmsChannel implements NotificationChannel { /* ... */ }
class SlackChannel implements NotificationChannel { /* ... */ }
// Register in a service provider
$this->app->tag([EmailChannel::class, SmsChannel::class, SlackChannel::class], 'notification.channels');
// Adding Telegram? Just create a new class and tag it. No existing code changes.
class TelegramChannel implements NotificationChannel { /* ... */ }
L — مبدأ استبدال ليسكوف
"يجب أن تكون الأنواع الفرعية قابلة للاستبدال بأنواعها الأساسية." إذا كان كودك يتوقع Logger، فأي تنفيذ لـ Logger يجب أن يعمل دون أن يكسر شيئاً.
// Violation: CachedRepository changes behavior in unexpected ways
class CachedProductRepository extends ProductRepository
{
public function find(int $id): ?Product
{
return Cache::remember("product.{$id}", 3600, function () use ($id) {
return parent::find($id);
});
}
public function save(Product $product): void
{
parent::save($product);
// But what if callers expect find() to return the latest data
// immediately after save()? The cache might serve stale data.
// This violates LSP.
}
}
// Better: make caching explicit and invalidate properly
class CachedProductRepository extends ProductRepository
{
public function save(Product $product): void
{
parent::save($product);
Cache::forget("product.{$product->id}");
}
}
I — مبدأ فصل الواجهات
"لا ينبغي إجبار أي عميل على الاعتماد على دوال لا يستخدمها."
// Too broad: not every export needs all these methods
interface Exportable
{
public function toCsv(): string;
public function toPdf(): string;
public function toExcel(): string;
public function toJson(): string;
}
// Better: small, focused interfaces
interface CsvExportable
{
public function toCsv(): string;
}
interface PdfExportable
{
public function toPdf(): string;
}
// Classes implement only what they need
class InvoiceReport implements CsvExportable, PdfExportable
{
public function toCsv(): string { /* ... */ }
public function toPdf(): string { /* ... */ }
}
class AuditLog implements CsvExportable
{
public function toCsv(): string { /* ... */ }
// No need to implement PDF — audit logs are CSV-only
}
D — مبدأ عكس التبعية
"اعتمد على التجريدات، لا على التطبيقات الملموسة." هنا يتألق حاوي الخدمات في Laravel.
// Tightly coupled to a specific implementation
class OrderService
{
public function sendConfirmation(Order $order): void
{
$mailer = new SendGridMailer(); // Hard dependency
$mailer->send($order->user->email, 'Your order is confirmed');
}
}
// Inverted: depend on the interface
class OrderService
{
public function __construct(
private readonly Mailer $mailer // Interface, not concrete class
) {}
public function sendConfirmation(Order $order): void
{
$this->mailer->send($order->user->email, 'Your order is confirmed');
}
}
// Bind in AppServiceProvider
$this->app->bind(Mailer::class, SendGridMailer::class);
// In tests, swap for a fake
$this->app->bind(Mailer::class, FakeMailer::class);
المنهج العملي
لا تطبّق مبادئ SOLID بشكل استباقي. طبّقها عندما تشعر بألم عدم وجودها — عندما يصبح اختبار فئة صعباً، أو عندما تتطلب إضافة ميزة تعديل خمسة ملفات، أو عندما يؤدي إصلاح خطأ في مكان إلى كسر شيء آخر.
- SRP: أعد الهيكلة عندما يصبح من الصعب تسمية فئة أو اختبارها
- OCP: أدخل الواجهات عندما يكون لديك 3 تنفيذات أو أكثر لنفس المفهوم
- LSP: انتبه للفئات الفرعية التي تطرح استثناءات غير متوقعة أو تغيّر أنواع القيم المُرجعة
- ISP: قسّم الواجهات عندما تبدأ التنفيذات بإرجاع
nullأو طرح "غير مدعوم" - DIP: احقن التبعيات عندما تحتاج إلى تبديل التنفيذات (الاختبار، تعدد المزودين، إلخ.)