حقول التزامن في Go: الأنماط والممارسات المثلى
تعلم أنماط DI المميزة لكتابة كود Go قابل للاختبار
حقن التبعيات (DI) هو نمط تصميم أساسي يعزز كتابة كود نظيف وقابل للاختبار والصيانة في تطبيقات GO.
سواء كنت تبني واجهات برمجة تطبيقات REST، أو تُنفِّذ أنماط قواعد بيانات متعددة المستأجرين، أو تعمل مع مكتبات ORM، فإن فهم حقن التبعيات سيحسن جودة كودك بشكل كبير.

ما هو حقن التبعيات؟
حقن التبعيات هو نمط تصميم حيث تتلقى المكونات تبعياتها من مصادر خارجية بدلًا من إنشائها داخليًا. هذا النهج يفصل المكونات، مما يجعل كودك أكثر نمطية وقابلية للاختبار والصيانة.
في GO، يكون حقن التبعيات قويًا بشكل خاص بسبب فلسفته التصميمية القائمة على الواجهات. تحقق GO من الامتثال للواجهات بشكل ضمني، مما يعني أنك يمكن أن تبديل التنفيذات بسهولة دون تعديل الكود الحالي.
لماذا استخدام حقن التبعيات في GO؟
تحسين قابلية الاختبار: من خلال حقن التبعيات، يمكنك استبدال التنفيذات الحقيقية بمحاكيات أو أضعاف الاختبار بسهولة. هذا يسمح لك بكتابة اختبارات وحدة سريعة ومستقلة لا تتطلب خدمات خارجية مثل قواعد البيانات أو الواجهات.
الصيانة الأفضل: تصبح التبعيات واضحة في كودك. عندما تنظر إلى دالة المُنشئ، ترى فورًا ما تتطلبه المكونة. هذا يجعل قاعدة الكود أسهل للفهم والتعديل.
الترابط الضعيف: تعتمد المكونات على التمثيلات (الواجهات) بدلًا من التنفيذات المحددة. هذا يعني أنك يمكن تغيير التنفيذات دون التأثير على الكود المعتمد.
المرونة: يمكنك تكوين تنفيذات مختلفة لبيئات مختلفة (التطوير، الاختبار، الإنتاج) دون تغيير منطق العمل.
حقن التبعيات عبر الدوال المُنشِّئة: الطريقة الخاصة بـ GO
الطريقة الأكثر شيوعًا وطريقة المعايير لتنفيذ حقن التبعيات في GO هي عبر الدوال المُنشِّئة. عادة ما تُسمى هذه الدوال NewXxx وتتلقى التبعيات كمعلمات.
مثال بسيط
إليك مثال بسيط يوضح حقن التبعيات عبر الدوال المُنشِّئة:
// تعريف واجهة للمستودع
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// الخدمة تعتمد على واجهة المستودع
type UserService struct {
repo UserRepository
}
// الدالة المُنشِّئة تحقن التبعية
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// تستخدم الدوال المُشَغَّلة التبعية المحقونة
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
هذا النمط يجعل واضحًا أن UserService يحتاج إلى UserRepository. لا يمكن إنشاء UserService دون تزويده بمستودع، مما يمنع الأخطاء أثناء التشغيل الناتجة عن غياب التبعيات.
عدة تبعيات
عندما يكون لدى مكون عدة تبعيات، أضفها ببساطة كمعلمات في الدالة المُنشِّئة:
type EmailService interface {
Send(to, subject, body string) error
}
type Logger interface {
Info(msg string)
Error(msg string, err error)
}
type OrderService struct {
repo OrderRepository
emailSvc EmailService
logger Logger
paymentSvc PaymentService
}
func NewOrderService(
repo OrderRepository,
emailSvc EmailService,
logger Logger,
paymentSvc PaymentService,
) *OrderService {
return &OrderService{
repo: repo,
emailSvc: emailSvc,
logger: logger,
paymentSvc: paymentSvc,
}
}
تصميم الواجهات لحقن التبعيات
واحد من المبادئ الأساسية عند تنفيذ حقن التبعيات هو مبدأ انقلاب التبعيات (DIP): يجب أن لا تعتمد الوحدات العليا على الوحدات الدنيا؛ بل يجب أن تعتمد كلتا الوحدتين على التمثيلات (الواجهات).
في GO، يعني هذا تعريف واجهات صغيرة ومُركزَة تمثل فقط ما تحتاجه المكونة:
// جيد: واجهة صغيرة ومُركزَة
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// سيء: واجهة كبيرة مع طرق غير ضرورية
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... العديد من الطرق
}
الواجهة الصغيرة تلتزم بمبدأ فصل الواجهات (ISP)—العملاء لا يجب أن يعتمدون على الطرق التي لا يستخدمونها. هذا يجعل كودك أكثر مرونة وأسهل للاختبار.
مثال عملي: تجريد قاعدة البيانات
عند العمل مع قواعد البيانات في تطبيقات GO، ستحتاج غالبًا إلى تجريد عمليات قاعدة البيانات. إليك كيف يساعد حقن التبعيات:
// واجهة قاعدة البيانات - تجريد عالي المستوى
type DB interface {
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
BeginTx(ctx context.Context) (Tx, error)
}
// المستودع يعتمد على التجريد
type UserRepository struct {
db DB
}
func NewUserRepository(db DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
rows, err := r.db.Query(ctx, query, id)
if err != nil {
return nil, err
}
defer rows.Close()
// ... تحليل الصفوف
}
هذا النمط مفيد بشكل خاص عند تنفيذ أنماط قواعد بيانات متعددة المستأجرين، حيث قد تحتاج إلى تبديل بين تنفيذات مختلفة لقواعد البيانات أو استراتيجيات الاتصال.
نمط الجذر المُركَّب
الجذر المُركَّب هو المكان حيث تُجمَع جميع التبعيات عند نقطة دخول التطبيق (عادةً main). هذا يركز تكوين التبعيات ويُظهر الرسم البياني للتبعيات بشكل واضح.
func main() {
// تهيئة التبعيات البنية التحتية
db := initDatabase()
logger := initLogger()
// تهيئة المستودعات
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// تهيئة الخدمات مع التبعيات
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// تهيئة مُعالجي HTTP
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// توصيل الطرق
router := setupRouter(userHandler, orderHandler)
// بدء الخادم
log.Fatal(http.ListenAndServe(":8080", router))
}
هذا النهج يجعل واضحًا كيفية بناء تطبيقك ومكان تأتي منه التبعيات. إنه خاصًا بشكل خاص عند بناء واجهات برمجة تطبيقات REST في GO، حيث تحتاج إلى تنسيق طبقات متعددة من التبعيات.
أطر عمل حقن التبعيات
لتطبيقات أكبر مع رسوم تبعيات معقدة، يمكن أن يصبح إدارة التبعيات يدويًا مرهقًا. يوفر GO عدة أطر عمل لحقن التبعيات يمكن أن تساعد:
Google Wire (حقن التبعيات في وقت التجميع)
Wire هو أداة لحقن التبعيات في وقت التجميع تُنتج الكود. إنها آمنة من حيث النوع ولا تُحدث تكلفة في وقت التشغيل.
التركيب:
go install github.com/google/wire/cmd/wire@latest
مثال:
// wire.go
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
func InitializeApp() (*App, error) {
wire.Build(
NewDB,
NewUserRepository,
NewUserService,
NewUserHandler,
NewApp,
)
return &App{}, nil
}
يُنتج Wire الكود الخاص بحقن التبعيات في وقت التجميع، مما يضمن أمان النوع ويُزيل تكلفة التكرار في وقت التشغيل.
Uber Dig (حقن التبعيات في وقت التشغيل)
Dig هو أطر عمل لحقن التبعيات في وقت التشغيل يُستخدم التكرار. إنه أكثر مرونة لكنه يحتوي على بعض التكلفة في وقت التشغيل.
مثال:
import "go.uber.org/dig"
func main() {
container := dig.New()
// تسجيل مزودي التبعيات
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// استدعاء دالة تحتاج إلى التبعيات
err := container.Invoke(func(handler *UserHandler) {
// استخدام المُعالج
})
if err != nil {
log.Fatal(err)
}
}
متى تستخدم الأطر
استخدم أطر عمل عندما:
- يكون رسم تبعياتك معقدًا مع العديد من المكونات المترابطة
- لديك تنفيذات متعددة لنفس الواجهة تحتاج إلى اختيارها بناءً على التكوين
- ترغب في حل التبعيات تلقائيًا
- تبني تطبيقًا كبيرًا حيث يصبح التوصيل اليدوي خطيرًا
استخدم حقن التبعيات اليدوي عندما:
- تطبيقك صغير أو متوسط الحجم
- رسم تبعياتك بسيط وسهل المتابعة
- ترغب في الحفاظ على التبعيات قليلة ومحددة
- تفضل الكود الصريح على الكود المُنتج
اختبار مع حقن التبعيات
واحد من الفوائد الرئيسية لحقن التبعيات هو تحسين قابلية الاختبار. إليك كيف يجعل حقن التبعيات الاختبار أسهل:
مثال اختبار وحدة
// تنفيذ مُحاكي للاختبار
type mockUserRepository struct {
users map[int]*User
err error
}
func (m *mockUserRepository) FindByID(id int) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *mockUserRepository) Save(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// اختبار باستخدام المُحاكي
func TestUserService_GetUser(t *testing.T) {
mockRepo := &mockUserRepository{
users: map[int]*User{
1: {ID: 1, Name: "John", Email: "john@example.com"},
},
}
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
هذا الاختبار يعمل بسرعة، لا يحتاج إلى قاعدة بيانات، ويختبر منطق العمل الخاص بك بشكل مستقل. عند العمل مع مكتبات ORM في GO، يمكنك حقن مستودعات مُحاكاة لاختبار منطق الخدمة دون إعداد قاعدة بيانات.
الأنماط الشائعة والممارسات الجيدة
1. استخدام فصل الواجهات
احتفظ بالواجهات صغيرة ومُركزَة على ما يحتاجه العميل:
// جيد: العميل يحتاج فقط لقراءة المستخدمين
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// واجهة منفصلة للكتابة
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. إرجاع الأخطاء من الدوال المُنشِّئة
يجب أن تتحقق الدوال المُنشِّئة من التبعيات وتعيد أخطاء إذا فشلت التهيئة:
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("مستودع المستخدم لا يمكن أن يكون فارغًا")
}
return &UserService{repo: repo}, nil
}
3. استخدام السياق للتبعيات المُخصصة للطلب
للتبعيات التي تكون مخصصة للطلب (مثل المعاملات في قاعدة البيانات)، أرسلها عبر السياق:
type ctxKey string
const dbKey ctxKey = "db"
func WithDB(ctx context.Context, db DB) context.Context {
return context.WithValue(ctx, dbKey, db)
}
func DBFromContext(ctx context.Context) (DB, bool) {
db, ok := ctx.Value(dbKey).(DB)
return db, ok
}
4. تجنب حقن التبعيات الزائدة
لا تحقن التبعيات التي هي بالفعل تفاصيل تنفيذ داخليّة. إذا أنشأ المكون مساعدين أو أدواته الخاصة، فهذا مقبول:
// جيد: المساعد الداخلي لا يحتاج حقن
type UserService struct {
repo UserRepository
// التخزين المؤقت الداخلي - لا يحتاج حقن
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. وثيق التبعيات
استخدم التعليقات لتوثيق سبب الحاجة إلى التبعيات والقيود:
// UserService يتعامل مع منطق العمل المرتبط بالمستخدمين.
// يحتاج إلى UserRepository للوصول إلى البيانات وLogger لل
// تتبع الأخطاء. يجب أن يكون المستودع آمنًا في السياقات المتزامنة.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
متى لا تستخدم حقن التبعيات
حقن التبعيات أداة قوية، لكنه ليس دائمًا ضروريًا:
تخطي حقن التبعيات عندما:
- تستخدم كائنات بسيطة أو بنى بيانات
- وظائف مساعدة داخليّة أو أدوات
- نصوص أو أدوات صغيرة
- عندما يكون التصنيع المباشر أوضح وأبسط
مثال على متى لا تستخدم حقن التبعيات:
// كائن بسيط - لا حاجة لحقن التبعيات
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// أداة بسيطة - لا حاجة لحقن التبعيات
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
التكامل مع بيئة GO
يعمل حقن التبعيات بسلاسة مع أنماط GO الأخرى والأدوات. عند بناء تطبيقات تستخدم مكتبات GO القياسيّة لاستخراج البيانات من الويب أو إنشاء تقارير PDF، يمكنك حقن هذه الخدمات في منطق العمل الخاص بك:
type PDFGenerator interface {
GenerateReport(data ReportData) ([]byte, error)
}
type ReportService struct {
pdfGen PDFGenerator
repo ReportRepository
}
func NewReportService(pdfGen PDFGenerator, repo ReportRepository) *ReportService {
return &ReportService{
pdfGen: pdfGen,
repo: repo,
}
}
هذا يسمح لك بتغيير تنفيذات إنشاء PDF أو استخدام محاكيات أثناء الاختبار.
الخاتمة
حقن التبعيات هو حجر أساس لكتابة كود GO قابل للصيانة والاختبار. من خلال اتباع الأنماط المذكورة في هذه المقالة—حقن التبعيات عبر الدوال المُنشِّئة، التصميم القائم على الواجهات، والنمط المركب—you’ll create applications that are easier to understand, test, and modify.
ابدأ بحقن التبعيات يدويًا لتطبيقات صغيرة إلى متوسطة الحجم، واعتبر الأطر مثل Wire أو Dig عندما يكبر رسم تبعياتك. تذكّر أن الهدف هو وضوح وقابلية الاختبار، وليس التعقيد من أجله.
للحصول على موارد إضافية لتطوير GO، تحقق من أسطورة GO.
روابط مفيدة
- أسطورة GO
- بدائل BeautifulSoup في GO
- إنشاء PDF في GO - المكتبات والأمثلة
- أنماط قواعد بيانات متعددة المستأجرين مع أمثلة في GO
- ORM لاستخدامها في GO: GORM، sqlc، Ent أو Bun؟
- دليل شامل لتنفيذ واجهات برمجة تطبيقات REST في GO