مقدمة: لماذا Dependency Injection مهم في Go؟
Go (Golang) هي لغة تقدر البساطة والوضوح. ومع ذلك، مع نمو المشروع، تظهر الحاجة إلى إدارة التبعيات بين المكونات. Dependency Injection (DI) أو حقن التبعيات هو نمط تصميم يساعد في إنشاء كود ضعيف الاقتران، قابل للاختبار، وسهل الصيانة. على عكس الأطر التي تستخدم سحر الانعكاس (مثل Spring في Java)، تقدم Go نهجًا أكثر عملية: تمرير التبعيات عبر الواجهات والمنشئات.
في هذه المقالة، سنشرح كيفية تنفيذ DI في Go دون استخدام مكتبات خارجية، باستخدام واجهات نظيفة، بالإضافة إلى استعراض حاويات DI الشائعة. ستتعلم كيف يساعد حقن التبعيات في كتابة اختبارات الوحدة واتباع مبادئ SOLID.
الأساسيات: الواجهات والحقن عبر المنشئ
في Go لا توجد كلاسات، ولكن هناك واجهات — وهي عقود سلوكية. وهي تشكل الأساس لـ DI. بدلاً من إنشاء تطبيق محدد داخل الهيكل، نقوم بتمريره من الخارج.
مثال بدون DI (اقتران قوي)
package main
import "fmt"
// Logger — تطبيق محددtype Logger struct{}
func (l *Logger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — يعتمد بشكل صارم على Loggertype Service struct { logger *Logger}
func NewService() *Service { return &Service{logger: &Logger{}}}
func (s *Service) DoWork() { s.logger.Log("تم تنفيذ العمل")}
func main() { svc := NewService() svc.DoWork()}المشكلة: لا يمكن اختبار Service دون إنشاء Logger حقيقي. لا يمكن استبداله بـ mock أو أي مسجل آخر.
مثال مع DI عبر الواجهة
package main
import "fmt"
// LogInterface — تجريد للتسجيلtype LogInterface interface { Log(msg string)}
// ConsoleLogger — تطبيقtype ConsoleLogger struct{}
func (l *ConsoleLogger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — الآن يعتمد على الواجهةtype Service struct { logger LogInterface}
// NewService — منشئ مع حقن التبعيةfunc NewService(logger LogInterface) *Service { return &Service{logger: logger}}
func (s *Service) DoWork() { s.logger.Log("تم تنفيذ العمل")}
func main() { logger := &ConsoleLogger{} svc := NewService(logger) svc.DoWork()}الآن يعمل Service مع أي كائن يطبق LogInterface. هذا يبسط الاختبار: يمكننا تمرير كائن وهمي (mock).
تقنية متقدمة: حاويات DI في Go
عندما يكبر المشروع، يصبح تمرير التبعيات يدويًا أمرًا مرهقًا. هنا تأتي حاويات DI للمساعدة — وهي مكتبات تقوم تلقائيًا بحل شبكة التبعيات. في Go، تشتهر google/wire (من Google) و uber-go/dig (من Uber).
مثال مع google/wire
يستخدم Wire توليد الكود، مما يلغي الحاجة إلى الانعكاس في وقت التشغيل — وهذا يتوافق مع روح Go.
// wire.go//go:build wireinject// +build wireinject
package main
import "github.com/google/wire"
func InitializeService() *Service { wire.Build(NewService, NewConsoleLogger) return nil}بعد تشغيل wire، سيتم إنشاء كود يقوم بإنشاء ConsoleLogger وتمريره إلى NewService.
مثال مع uber-go/dig
يعمل Dig من خلال الانعكاس، لكنه مريح جدًا للبناء الديناميكي.
package main
import ( "go.uber.org/dig")
func main() { container := dig.New()
// تسجيل المنشئات container.Provide(NewConsoleLogger) container.Provide(NewService)
// استدعاء دالة تستخدم التبعية container.Invoke(func(s *Service) { s.DoWork() })}الخلاصة: متى تستخدم DI في Go؟
حقن التبعية في Go ليس حلاً شاملاً. استخدم DI عندما:
- تريد اختبار المكونات بشكل منفصل (اختبار الوحدة).
- يحتاج المشروع إلى استبدال التطبيقات (على سبيل المثال، قواعد بيانات مختلفة).
- تتبع مبدأ انعكاس التبعية (Dependency Inversion Principle) من SOLID.
ومع ذلك، لا تفرط في استخدام DI في المشاريع الصغيرة — فالبساطة هي القوة الرئيسية لـ Go. ابدأ بالحقن اليدوي عبر المنشئات، ثم انتقل إلى الحاويات (مثل Wire) عندما يصبح المشروع معقدًا.
اكتب كودًا نظيفًا، واختبر بثقة!