حقن التبعية في Go: دليل عملي

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

حقن التبعية في Go: من النظرية إلى الكود الفعلي

\n\n

حقن التبعية (DI) هو أحد أنماط التصميم الرئيسية التي تساعد في إنشاء كود ضعيف الاقتران، قابل للاختبار وسهل الصيانة. في Go، مع فلسفتها في البساطة والوضوح، يمكن تنفيذ حقن التبعيات يدويًا أو باستخدام المكتبات. في هذه المقالة، سنشرح كيفية تطبيق DI بشكل صحيح في Golang عمليًا، وسننظر في النهج اليدوي، بالإضافة إلى المكتبة الشهيرة google/wire.

\n\n

1. لماذا نحتاج إلى حقن التبعية في Go؟

\n\n

تخيل أنك تكتب خدمة HTTP للتعامل مع المستخدمين. بدون DI، قد يبدو كودك كالتالي:

\n\n
package 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
\n\n

DI يحل هذه المشاكل عن طريق تمرير التبعيات من الخارج:

\n\n
type 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\n

2. الحقن اليدوي للتبعيات: البساطة والتحكم

\n\n

Go لا يتطلب أطر عمل لـ 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:

\n\n
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
\n\n

العيوب: مع نمو التطبيق (50+ مكونًا)، يصبح التجميع اليدوي مرهقًا وعرضة للأخطاء.

\n\n

3. الأتمتة باستخدام google/wire

\n\n

مكتبة github.com/google/wire تولد كودًا لتجميع التبعيات. إنها ليست حاوية وقت تشغيل (runtime container) — إنها مولد كود، وهذا يتوافق تمامًا مع فلسفة Go.

\n\n

التثبيت:

\n
go get github.com/google/wir                

المدونات

توصيات الكتب