حقن التبعية في Go: من النظرية إلى الكود الفعلي
\n\nحقن التبعية (DI) هو أحد أنماط التصميم الرئيسية التي تساعد في إنشاء كود ضعيف الاقتران، قابل للاختبار وسهل الصيانة. في Go، مع فلسفتها في البساطة والوضوح، يمكن تنفيذ حقن التبعيات يدويًا أو باستخدام المكتبات. في هذه المقالة، سنشرح كيفية تطبيق DI بشكل صحيح في Golang عمليًا، وسننظر في النهج اليدوي، بالإضافة إلى المكتبة الشهيرة google/wire.
1. لماذا نحتاج إلى حقن التبعية في Go؟
\n\nتخيل أنك تكتب خدمة HTTP للتعامل مع المستخدمين. بدون DI، قد يبدو كودك كالتالي:
\n\npackage main\n\nimport "database/sql"\n\ntype UserHandler struct {\n db *sql.DB\n}\n\nfunc NewUserHandler() *UserHandler {\n db, _ := sql.Open("postgres", "conn_string")\n return &UserHandler{db: db}\n}\n\nfunc (h *UserHandler) GetUser(id int) (*User, error) {\n // العمل مع h.db\n}\n\n\nالمشاكل واضحة:
\n- \n
- اقتران قوي — المعالج نفسه يقوم بإنشاء قاعدة البيانات. \n
- صعوبة الاختبار — لا يمكن استبدال
dbبنموذج وهمي (mock). \n - انتهاك مبدأ المسؤولية الواحدة — المعالج مسؤول عن إنشاء التبعيات وعن منطق الأعمال. \n
DI يحل هذه المشاكل عن طريق تمرير التبعيات من الخارج:
\n\ntype UserHandler struct {\n db Database\n}\n\nfunc NewUserHandler(db Database) *UserHandler {\n return &UserHandler{db: db}\n}\n\n// الآن يمكن استبداله بسهولة بنموذج وهمي\ntype Database interface {\n GetUser(id int) (*User, error)\n}\n\n\n2. الحقن اليدوي للتبعيات: البساطة والتحكم
\n\nGo لا يتطلب أطر عمل لـ DI. الطريقة الأبسط والأكثر شفافية هي تمرير التبعيات عبر المنشئات (constructors). لننظر إلى مثال بثلاث طبقات من التطبيق: handler → service → repository.
\n\n// repository/user.go\ntype UserRepository interface {\n FindByID(id int) (*User, error)\n}\n\ntype postgresUserRepo struct {\n db *sql.DB\n}\n\nfunc NewPostgresUserRepo(db *sql.DB) *postgresUserRepo {\n return &postgresUserRepo{db: db}\n}\n\nfunc (r *postgresUserRepo) FindByID(id int) (*User, error) {\n // التنفيذ\n}\n\n\n// service/user.go\ntype UserService struct {\n repo UserRepository\n}\n\nfunc NewUserService(repo UserRepository) *UserService {\n return &UserService{repo: repo}\n}\n\nfunc (s *UserService) GetUser(id int) (*User, error) {\n return s.repo.FindByID(id)\n}\n\n\n// handler/user.go\ntype UserHandler struct {\n svc *UserService\n}\n\nfunc NewUserHandler(svc *UserService) *UserHandler {\n return &UserHandler{svc: svc}\n}\n\nfunc (h *UserHandler) HandleGetUser(w http.ResponseWriter, r *http.Request) {\n // العمل مع h.svc\n}\n\n\nالآن يتم تجميع التطبيق في main.go:
func main() {\n db, _ := sql.Open("postgres", os.Getenv("DB_URL"))\n \n repo := NewPostgresUserRepo(db)\n svc := NewUserService(repo)\n handler := NewUserHandler(svc)\n \n http.HandleFunc("/user", handler.HandleGetUser)\n log.Fatal(http.ListenAndServe(":8080", nil))\n}\n\n\nمزايا DI اليدوي:
\n- \n
- شفافية كاملة — ترى جميع التبعيات بوضوح. \n
- لا يوجد سحر أو تبعيات إضافية في المشروع. \n
- مثالي للمشاريع الصغيرة والمتوسطة. \n
العيوب: مع نمو التطبيق (50+ مكونًا)، يصبح التجميع اليدوي مرهقًا وعرضة للأخطاء.
\n\n3. الأتمتة باستخدام google/wire
\n\nمكتبة github.com/google/wire تولد كودًا لتجميع التبعيات. إنها ليست حاوية وقت تشغيل (runtime container) — إنها مولد كود، وهذا يتوافق تمامًا مع فلسفة Go.
التثبيت:
\ngo get github.com/google/wir