مبادئ SOLID في Laravel
فهم مبادئ SOLID
SOLID هو اختصار لخمسة مبادئ تصميم تجعل تصميمات البرمجيات أكثر قابلية للفهم والمرونة والصيانة. تم تقديم هذه المبادئ بواسطة Robert C. Martin وهي أساسية للتصميم الموجه للكائنات.
- S - مبدأ المسؤولية الواحدة (Single Responsibility Principle)
- O - مبدأ المفتوح/المغلق (Open/Closed Principle)
- L - مبدأ استبدال ليسكوف (Liskov Substitution Principle)
- I - مبدأ فصل الواجهات (Interface Segregation Principle)
- D - مبدأ عكس التبعية (Dependency Inversion Principle)
1. مبدأ المسؤولية الواحدة (SRP)
يجب أن يكون للصف سبب واحد فقط للتغيير. بمعنى آخر، يجب أن يكون للصف وظيفة أو مسؤولية واحدة فقط.
// ❌ سيء: ينتهك SRP - مسؤوليات متعددة
class UserManager
{
public function register(array $data)
{
// التحقق من البيانات
$validator = Validator::make($data, [
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
if ($validator->fails()) {
throw new ValidationException($validator);
}
// إنشاء مستخدم
$user = User::create([
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
// إرسال بريد إلكتروني
Mail::to($user->email)->send(new WelcomeEmail($user));
// تسجيل النشاط
Log::info('تم تسجيل المستخدم: ' . $user->email);
// تحديث الإحصائيات
Cache::increment('total_users');
return $user;
}
}
// ✅ جيد: يتبع SRP - مسؤولية واحدة لكل صف
class UserRepository
{
public function create(array $data): User
{
return User::create([
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}
class UserNotificationService
{
public function sendWelcomeEmail(User $user): void
{
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}
class UserActivityLogger
{
public function logRegistration(User $user): void
{
Log::info('تم تسجيل المستخدم: ' . $user->email);
}
}
class UserStatisticsService
{
public function incrementUserCount(): void
{
Cache::increment('total_users');
}
}
// المتحكم ينسق هذه الخدمات
class RegisterController extends Controller
{
public function __construct(
private UserRepository $userRepository,
private UserNotificationService $notificationService,
private UserActivityLogger $activityLogger,
private UserStatisticsService $statisticsService
) {}
public function register(RegisterRequest $request)
{
$user = $this->userRepository->create($request->validated());
$this->notificationService->sendWelcomeEmail($user);
$this->activityLogger->logRegistration($user);
$this->statisticsService->incrementUserCount();
return response()->json(['user' => $user]);
}
}
2. مبدأ المفتوح/المغلق (OCP)
يجب أن تكون الكيانات البرمجية مفتوحة للتوسيع ولكن مغلقة للتعديل. يجب أن تكون قادرًا على إضافة وظائف جديدة دون تغيير الكود الموجود.
// ❌ سيء: ينتهك OCP - يجب تعديل الصف لإضافة طرق دفع
class PaymentProcessor
{
public function process(string $type, float $amount)
{
if ($type === 'credit_card') {
// معالجة بطاقة الائتمان
return $this->processCreditCard($amount);
} elseif ($type === 'paypal') {
// معالجة PayPal
return $this->processPayPal($amount);
} elseif ($type === 'stripe') {
// معالجة Stripe
return $this->processStripe($amount);
}
// إضافة طريقة دفع جديدة تتطلب تعديل هذا الصف!
}
}
// ✅ جيد: يتبع OCP - استخدام الواجهات وتعدد الأشكال
interface PaymentGateway
{
public function process(float $amount): PaymentResult;
public function getName(): string;
}
class CreditCardGateway implements PaymentGateway
{
public function process(float $amount): PaymentResult
{
// منطق معالجة بطاقة الائتمان
return new PaymentResult(true, 'CC-' . uniqid());
}
public function getName(): string
{
return 'credit_card';
}
}
class PayPalGateway implements PaymentGateway
{
public function process(float $amount): PaymentResult
{
// منطق معالجة PayPal
return new PaymentResult(true, 'PP-' . uniqid());
}
public function getName(): string
{
return 'paypal';
}
}
class StripeGateway implements PaymentGateway
{
public function process(float $amount): PaymentResult
{
// منطق معالجة Stripe
return new PaymentResult(true, 'ST-' . uniqid());
}
public function getName(): string
{
return 'stripe';
}
}
// معالج الدفع لا يحتاج تعديل لبوابات جديدة
class PaymentProcessor
{
private array $gateways = [];
public function registerGateway(PaymentGateway $gateway): void
{
$this->gateways[$gateway->getName()] = $gateway;
}
public function process(string $gatewayName, float $amount): PaymentResult
{
if (!isset($this->gateways[$gatewayName])) {
throw new \InvalidArgumentException("البوابة غير موجودة: {$gatewayName}");
}
return $this->gateways[$gatewayName]->process($amount);
}
}
// تسجيل مزود الخدمة
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PaymentProcessor::class, function ($app) {
$processor = new PaymentProcessor();
$processor->registerGateway(new CreditCardGateway());
$processor->registerGateway(new PayPalGateway());
$processor->registerGateway(new StripeGateway());
// أضف بوابة جديدة هنا دون تعديل PaymentProcessor
return $processor;
});
}
}
3. مبدأ استبدال ليسكوف (LSP)
يجب أن تكون كائنات الصف الأساسي قابلة للاستبدال بكائنات صفوفه الفرعية دون كسر التطبيق. يجب أن تكون الصفوف المشتقة قابلة للاستبدال بصفوفها الأساسية.
// ❌ سيء: ينتهك LSP - Square يغير سلوك Rectangle
class Rectangle
{
protected float $width;
protected float $height;
public function setWidth(float $width): void
{
$this->width = $width;
}
public function setHeight(float $height): void
{
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
}
class Square extends Rectangle
{
public function setWidth(float $width): void
{
$this->width = $width;
$this->height = $width; // يكسر LSP!
}
public function setHeight(float $height): void
{
$this->width = $height;
$this->height = $height; // يكسر LSP!
}
}
// هذا ينكسر عند استخدام Square
function testRectangle(Rectangle $rectangle)
{
$rectangle->setWidth(5);
$rectangle->setHeight(10);
// المتوقع: 50، لكن Square يعطي: 100
return $rectangle->getArea();
}
// ✅ جيد: يتبع LSP - استخدام التكوين بدلاً من الوراثة
interface Shape
{
public function getArea(): float;
}
class Rectangle implements Shape
{
public function __construct(
private float $width,
private float $height
) {}
public function getArea(): float
{
return $this->width * $this->height;
}
}
class Square implements Shape
{
public function __construct(private float $side) {}
public function getArea(): float
{
return $this->side * $this->side;
}
}
// كلاهما يمكن استخدامه بشكل متبادل
function calculateArea(Shape $shape): float
{
return $shape->getArea();
}
4. مبدأ فصل الواجهات (ISP)
لا يجب إجبار العملاء على الاعتماد على واجهات لا يستخدمونها. قسم الواجهات الكبيرة إلى واجهات أصغر وأكثر تحديدًا.
// ❌ سيء: ينتهك ISP - واجهة سمينة
interface Worker
{
public function work(): void;
public function eat(): void;
public function sleep(): void;
public function getPaid(): void;
}
class HumanWorker implements Worker
{
public function work(): void { /* يعمل */ }
public function eat(): void { /* يأكل */ }
public function sleep(): void { /* ينام */ }
public function getPaid(): void { /* يحصل على راتب */ }
}
class RobotWorker implements Worker
{
public function work(): void { /* يعمل */ }
public function eat(): void { /* الروبوتات لا تأكل! */ }
public function sleep(): void { /* الروبوتات لا تنام! */ }
public function getPaid(): void { /* الروبوتات لا تحصل على راتب! */ }
}
// ✅ جيد: يتبع ISP - واجهات منفصلة
interface Workable
{
public function work(): void;
}
interface Eatable
{
public function eat(): void;
}
interface Sleepable
{
public function sleep(): void;
}
interface Payable
{
public function getPaid(): void;
}
class HumanWorker implements Workable, Eatable, Sleepable, Payable
{
public function work(): void { /* يعمل */ }
public function eat(): void { /* يأكل */ }
public function sleep(): void { /* ينام */ }
public function getPaid(): void { /* يحصل على راتب */ }
}
class RobotWorker implements Workable
{
public function work(): void { /* يعمل */ }
// ينفذ فقط ما يحتاجه!
}
// مثال Laravel
interface Authenticatable
{
public function getAuthIdentifier();
public function getAuthPassword();
}
interface CanResetPassword
{
public function getEmailForPasswordReset();
public function sendPasswordResetNotification($token);
}
interface MustVerifyEmail
{
public function hasVerifiedEmail();
public function markEmailAsVerified();
public function sendEmailVerificationNotification();
}
// User ينفذ فقط الواجهات المطلوبة
class User extends Model implements Authenticatable, CanResetPassword
{
// ينفذ فقط المصادقة وإعادة تعيين كلمة المرور
}
5. مبدأ عكس التبعية (DIP)
لا يجب أن تعتمد الوحدات عالية المستوى على الوحدات منخفضة المستوى. كلاهما يجب أن يعتمد على التجريدات. حاوية خدمة Laravel تجعل هذا سهلاً.
- اعتمد على التجريدات (الواجهات)، وليس التنفيذات الملموسة
- استخدم حقن التبعية لتوفير التنفيذات
- يجعل الكود أكثر قابلية للاختبار والمرونة
// ❌ سيء: ينتهك DIP - يعتمد على صف ملموس
class OrderProcessor
{
private MySqlOrderRepository $repository;
private SmtpEmailService $emailService;
public function __construct()
{
// اقتران محكم مع التنفيذات الملموسة
$this->repository = new MySqlOrderRepository();
$this->emailService = new SmtpEmailService();
}
public function process(array $orderData)
{
$order = $this->repository->create($orderData);
$this->emailService->send($order->customer_email, 'تم إنشاء الطلب');
return $order;
}
}
// ✅ جيد: يتبع DIP - يعتمد على التجريدات
interface OrderRepositoryInterface
{
public function create(array $data): Order;
public function find(int $id): ?Order;
}
interface EmailServiceInterface
{
public function send(string $to, string $subject, array $data = []): void;
}
class MySqlOrderRepository implements OrderRepositoryInterface
{
public function create(array $data): Order
{
return Order::create($data);
}
public function find(int $id): ?Order
{
return Order::find($id);
}
}
class MongoOrderRepository implements OrderRepositoryInterface
{
public function create(array $data): Order
{
// تنفيذ MongoDB
}
public function find(int $id): ?Order
{
// تنفيذ MongoDB
}
}
class SmtpEmailService implements EmailServiceInterface
{
public function send(string $to, string $subject, array $data = []): void
{
Mail::to($to)->send(new OrderPlacedMail($data));
}
}
class SendGridEmailService implements EmailServiceInterface
{
public function send(string $to, string $subject, array $data = []): void
{
// تنفيذ SendGrid
}
}
// OrderProcessor يعتمد على التجريدات
class OrderProcessor
{
public function __construct(
private OrderRepositoryInterface $repository,
private EmailServiceInterface $emailService
) {}
public function process(array $orderData)
{
$order = $this->repository->create($orderData);
$this->emailService->send(
$order->customer_email,
'تم إنشاء الطلب',
['order' => $order]
);
return $order;
}
}
// ربط مزود الخدمة
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
OrderRepositoryInterface::class,
MySqlOrderRepository::class
);
$this->app->bind(
EmailServiceInterface::class,
SmtpEmailService::class
);
}
}
// الاختبار الآن سهل - حقن mocks
class OrderProcessorTest extends TestCase
{
public function test_processes_order()
{
$mockRepo = Mockery::mock(OrderRepositoryInterface::class);
$mockEmail = Mockery::mock(EmailServiceInterface::class);
$mockRepo->shouldReceive('create')->once()->andReturn(new Order());
$mockEmail->shouldReceive('send')->once();
$processor = new OrderProcessor($mockRepo, $mockEmail);
$processor->process(['customer_id' => 1]);
}
}
class BlogPostManager {
public function publish($data) {
// يتحقق من البيانات
// ينشئ المنشور
// يرسل إشعارات
// يحدث خريطة الموقع SEO
// يمسح الذاكرة المؤقتة
}
}
قسم إلى: BlogPostRepository، NotificationService، SeoService، CacheService
- إشعارات البريد الإلكتروني
- إشعارات SMS
- إشعارات Push
- يجب أن يكون من السهل إضافة Slack/Discord دون تعديل الكود الموجود
- أنشئ FileStorageInterface مع طرق upload/download/delete
- نفذ LocalFileStorage و S3FileStorage
- أنشئ FileManager يعتمد على الواجهة
- سجل الروابط في مزود الخدمة