حكاية هندسية من غزة… انتهت في وادي السيليكون
يا جماعة الخير، خلوني أحكيلكم هالسالفة. قبل كم سنة، كنت في مكتبي بغزة، والكهرباء كالعادة بتقطع وبتيجي. إحنا في غزة، لما الكهربا تقطع، ما بتفكر في “أفضل ممارسة برمجية” أو الـ “Best Practice”… بتفكر في إشي واحد بس: شو اللي راح يشتغل فعلًا ويخلّص الشغل قبل ما البطارية تخلص.
في هذاك اليوم، كنت براجع كود لمشروع Laravel متوسط الحجم. مشروع عادي جدًا: مستخدمين (Users)، طلبات (Orders)، إشعارات (Notifications). ما في أي تعقيدات مثل الـ Microservices أو الـ Distributed Madness اللي بنسمع عنها اليوم.
كان معي مطور شب، موهوب وذكي وسريع البديهة، لكن عنده ولع غريب بالـ Service Container. كان يستخدمه في كل مكان، حرفيًا في كل زاوية من زوايا الكود، حتى في أماكن بسيطة جدًا لا تحتاج لكل هذا التعقيد.
بعد ما شفت الكود، ما عاتبته. سألته سؤال بسيط وهادي:
يا صاحبي، متى برأيك استخدام الـ Service Container بكون “over-engineering”؟ يعني تعقيد زيادة عن اللزوم؟
اليوم، هذا المطور الشاب يعمل في واحدة من أكبر شركات التكنولوجيا في Silicon Valley. مش لأنه كتب كودًا أكثر تعقيدًا، بل لأنه تعلم متى يحذف الكود المعقد ويختار البساطة.
السؤال الحقيقي (وليس سؤال المقابلات)
في مقابلات العمل، يسألونك “ما هو الـ Service Container؟” لكن في الحياة العملية، السؤال الأهم هو:
❓ متى استخدام Service Container يكون over-engineering في Laravel؟
وهنا يكمن سؤال الفخ الذي يقع فيه الكثيرون:
🪤 هل الـ Dependency Injection دائمًا هو الخيار الأفضل؟
الإجابة المتوقعة (التي تسمعها كثيرًا)
أغلب المطورين سيجيبون بسرعة:
“طبعًا! الـ Dependency Injection (DI) يحسن قابلية الكود للاختبار (Testability)، ويقلل الاعتمادية بين المكونات (Coupling)، ويجعل الكود أنظف وأكثر تنظيمًا.”
كل هذا الكلام صحيح 100%… لكنه، يا إخواني، ليس القصة الكاملة.
🧠 الإجابة التي تكشف العقلية الفذّة
المطور الخبير، الذي ذاق مرارة صيانة الكود المعقد، سيجيب بطريقة مختلفة:
الـ Dependency Injection أداة قوية جدًا، لكنها ليست مجانية. لها تكلفتها.
أنا، أبو عمر، أستخدم الـ Service Container عندما يتحقق أحد هذه الشروط:
- يوجد أكثر من implementation حقيقي للواجهة (Interface): يعني عندي أكثر من طريقة لتنفيذ نفس المهمة (مثل بوابات الدفع المختلفة).
- دورة حياة الكائن (lifecycle) معقدة: عندما يحتاج الكائن إلى إعدادات خاصة، أو إدارة حالة (state)، أو تخزين مؤقت (caching)، أو يتعامل مع موارد خارجية (external resources).
- الكلاس جزء من صميم منطق العمل (Domain Core): عندما يكون هذا الكلاس حجر أساس في النظام، وتغييره يؤثر على أجزاء كثيرة.
لكنه يصبح over-engineering عندما:
- الكلاس بسيط ولا يحتفظ بأي حالة (stateless).
- لا يوجد أي سبب منطقي لوجود abstraction أو واجهة (Interface).
- الواجهة (Interface) أُنشئت فقط “لأن مبادئ SOLID تقول ذلك”.
وهنا قال لي ذلك المطور الشاب جملة لم أنسها أبدًا بعد نقاشنا:
“يا عمي أبو عمر، أحيانًا
newأو Facade صادق وصريح أكثر من Interface كاذب ومخادع.”
مثال Laravel واقعي: هنا تُكشف الخبرة
دعونا نرى مثالًا شائعًا جدًا على التعقيد الزائد الذي أراه في كثير من المشاريع.
❌ مثال Over-Engineering شائع جدًا
لنفترض أنك تريد تنسيق تاريخ. المطور “المتحمس” قد يكتب الكود التالي:
// 1. The useless Interface
interface DateFormatterInterface
{
public function format(Carbon $date): string;
}
// 2. The only Implementation
class DateFormatter implements DateFormatterInterface
{
public function format(Carbon $date): string
{
return $date->format('Y-m-d');
}
}
// 3. Binding it in a Service Provider
$this->app->bind(
DateFormatterInterface::class,
DateFormatter::class
);
// 4. Injecting it in a Service
class OrderService
{
public function __construct(
private DateFormatterInterface $formatter
) {}
public function handle(Order $order)
{
// ... some logic
return $this->formatter->format($order->created_at);
}
}
🧨 ما المشكلة هنا؟
هذا الكود يبدو “احترافيًا” للوهلة الأولى، لكنه في الحقيقة تعقيد بلا قيمة:
- لا يوجد سوى implementation واحد: هل تخطط حقًا لإنشاء 10 طرق مختلفة لتنسيق التاريخ بنفس الشكل؟ على الأغلب لا.
- لا يوجد state: الكلاس لا يخزن أي بيانات أو حالة. هو مجرد دالة بسيطة.
- لا يوجد سبب واقعي للاستبدال: لن تحتاج أبدًا لاستبدال هذا الكلاس بآخر في بيئة الاختبار. يمكنك ببساطة اختبار أن التاريخ تم تنسيقه بالشكل الصحيح.
هذا يسمى “Abstraction for the sake of Abstraction” (التجريد من أجل التجريد)، وهو تكلفة بلا مقابل.
✅ الحل الأبسط (والأذكى)
ماذا لو كتبنا الكود هكذا؟
class OrderService
{
public function handle(Order $order)
{
// ... some logic
return $order->created_at->format('Y-m-d');
}
}
بسيط، واضح، ومباشر. لا توجد طبقات إضافية للقراءة والفهم. وإن كنت مصرًا على فصل هذه الوظيفة في كلاس منفصل لتنظيم الكود (وهو أمر جيد)، يمكنك فعل ذلك ببساطة:
class DateFormatter
{
public static function format(Carbon $date): string
{
return $date->format('Y-m-d');
}
}
// And use it like this:
// DateFormatter::format($order->created_at);
لا حاجة لـ Container، ولا Interface، ولا أي وهم هندسي. الكود يخدم الهدف مباشرة.
مثال ثانٍ: متى يكون Service Container قرارًا ذكيًا
الآن، دعونا نرى الحالة التي يكون فيها استخدام الـ Service Container والـ DI عبقريًا.
✅ حالة صحيحة فعلًا
لنفترض أن تطبيقك يدعم أكثر من بوابة دفع إلكتروني:
- Stripe
- PayPal
- Apple Pay
هنا، الـ Abstraction ليس ترفًا، بل هو ضرورة حقيقية.
// 1. A meaningful Interface
interface PaymentGateway
{
public function charge(int $amount): PaymentResult;
}
// 2. Multiple, real Implementations
class StripeGateway implements PaymentGateway
{
public function charge(int $amount): PaymentResult
{
// Logic to charge via Stripe API
}
}
class PaypalGateway implements PaymentGateway
{
public function charge(int $amount): PaymentResult
{
// Logic to charge via PayPal API
}
}
// 3. Smart binding in a Service Provider
$this->app->bind(PaymentGateway::class, function ($app) {
return match (config('payment.provider')) {
'stripe' => new StripeGateway(/* api keys */),
'paypal' => new PaypalGateway(/* credentials */),
default => throw new Exception('Invalid payment provider.'),
};
});
// 4. Clean injection
class CheckoutService
{
public function __construct(
private PaymentGateway $gateway
) {}
public function checkout(int $amount)
{
return $this->gateway->charge($amount);
}
}
🎯 هنا الـ DI عبقري، لماذا؟
- التبديل حقيقي ومطلوب: يمكنك تغيير بوابة الدفع من خلال ملف الإعدادات `config` فقط، دون لمس سطر واحد في `CheckoutService`.
- دورة الحياة معقدة: كل بوابة دفع قد تحتاج مفاتيح API خاصة بها، وإعدادات مختلفة. الـ Service Container هو المكان المثالي لإدارة هذا التعقيد.
- الاختبار ضروري: يمكنك بسهولة حقن بوابة دفع وهمية (Mock) أثناء الاختبار للتأكد من أن منطق الدفع يعمل بشكل صحيح دون إجراء عمليات دفع حقيقية.
هنا، القرار كان معماريًا وهادفًا، وليس مجرد تقليد أعمى للـ Patterns.
لماذا هذه عقلية Senior حقيقية؟
الفرق بين المطور المبتدئ والخبير (Senior) ليس في عدد الـ Patterns التي يعرفها، بل في حكمته في استخدامها.
1. لا يقدّس الأنماط (Patterns)
المطور الخبير لا يطبق الـ DI لأن “الكتاب يقول ذلك” أو لأن “فلان قال ذلك”. هو يطبقه لأنه الحل الأنسب لمشكلة حقيقية أمامه.
2. يفهم تكلفة التجريد (Cost of Abstraction)
يعرف أن كل `Interface` وكل طبقة جديدة تضيف معها تكلفة غير مرئية:
- حمل إدراكي (Cognitive Load): على المطور الجديد أن يفهم الواجهة، ثم يجد الـ implementation، ثم يفهم كيف تم ربطها في الـ Container.
- تكلفة صيانة (Maintenance Cost): المزيد من الملفات والطبقات يعني المزيد من الصيانة.
- ألم في تصحيح الأخطاء (Debugging Pain): تتبع الخطأ عبر عدة طبقات من الـ Abstraction أصعب بكثير من تتبعه في كود مباشر.
3. يفكر بالنظام… لا بالكلاس
يسأل نفسه دائمًا:
هل هذا التعقيد يخدم النظام ككل ويجعله أكثر مرونة وصيانة على المدى الطويل؟ أم أنه يخدم غروري الهندسي لأكتب كودًا “ذكيًا”؟
الحكمة التي خرجت من غزة
في بيئة محدودة الموارد مثل غزة، حيث كل دقيقة كهرباء تساوي ذهبًا، تتعلم قاعدة ذهبية:
لا تبنِ تعقيدًا لا تحتاجه اليوم.
أفضل مطور عرفته هناك لم يكن أكثرهم استخدامًا للـ Service Container، بل كان أكثرهم شجاعة في أن يقول لزملائه ولي:
“يا جماعة، هذا الكلاس بسيط… دعوه بسيطًا.”
واليوم، وهو يبني أنظمة تخدم ملايين المستخدمين في وادي السيليكون، ما زال يطبق نفس المبدأ البسيط الذي تعلمه تحت ضغط الظروف.
خلاصة القول: نصيحة أبو عمر 💡
يا صديقي المبرمج، الـ Service Container والـ Dependency Injection أدوات رائعة في صندوق أدواتك، لكنها مثل المطرقة؛ يمكنك استخدامها لبناء منزل جميل، أو لتحطيم أصابعك إذا استخدمتها في كل مكان.
قبل أن تنشئ `Interface` جديدًا أو تربط شيئًا في الـ Container، اسأل نفسك: “هل أحل مشكلة حقيقية، أم أصنع مشكلة مستقبلية؟”
تذكر دائمًا الحكمة التي تعلمتها من ذلك الشاب الموهوب:
الكود النظيف… هو الذي يعرف متى يتوقف عن أن يكون ذكيًا.