गो में डिपेंडेंसी इंजेक्शन: पैटर्न और सर्वोत्तम प्रथाएँ

टेस्टेबल गो कोड के लिए DI पैटर्नों का अध्ययन करें

Page content

Dependency injection (DI) एक मूलभूत डिज़ाइन पैटर्न है जो Go एप्लिकेशन्स में साफ़, टेस्ट योग्य, और बनाए रखने योग्य कोड को प्रोत्साहित करता है।

चाहे आप REST APIs बनाएँ, मल्टी-टेनेंट डेटाबेस पैटर्न लागू करें, या ORM लाइब्रेरी के साथ काम करें, डिपेंडेंसी इंजेक्शन को समझना आपकी कोड गुणवत्ता को महत्वपूर्ण रूप से सुधार देगा।

go-dependency-injection

डिपेंडेंसी इंजेक्शन क्या है?

डिपेंडेंसी इंजेक्शन एक डिज़ाइन पैटर्न है जहां घटक अपने डिपेंडेंसी को बाहरी स्रोतों से प्राप्त करते हैं बजाय उन्हें आंतरिक रूप से बनाने के। यह दृष्टिकोण घटकों को डिकपल करता है, जिससे आपका कोड अधिक मॉड्यूलर, टेस्ट योग्य, और बनाए रखने योग्य होता है।

Go में, डिपेंडेंसी इंजेक्शन विशेष रूप से शक्तिशाली है क्योंकि भाषा के इंटरफेस-आधारित डिज़ाइन दर्शन के कारण। Go के इम्प्लिसिट इंटरफेस सैटिस्फैक्शन का मतलब है कि आप आसानी से इम्प्लिमेंटेशन बदल सकते हैं बिना मौजूदा कोड को संशोधित किए।

Go में डिपेंडेंसी इंजेक्शन का उपयोग क्यों करें?

सुधारित टेस्ट योग्यता: डिपेंडेंसी इंजेक्ट करके, आप आसानी से वास्तविक इम्प्लिमेंटेशन को मॉक या टेस्ट डबल्स के साथ बदल सकते हैं। यह आपको उन यूनिट टेस्ट लिखने की अनुमति देता है जो तेज़, अलग, और बाहरी सेवाओं जैसे डेटाबेस या APIs की आवश्यकता नहीं होती।

बेहतर बनाए रखने योग्यता: डिपेंडेंसी कोड में स्पष्ट हो जाते हैं। जब आप एक कंस्ट्रक्टर फंक्शन देखते हैं, तो आपको तुरंत पता चलता है कि एक घटक किसकी आवश्यकता है। यह कोडबेस को समझने और संशोधित करने में आसान बनाता है।

लूज़ कपलिंग: घटक कंक्रीट इम्प्लिमेंटेशन के बजाय अभिसरण (इंटरफेस) पर निर्भर करते हैं। इसका मतलब है कि आप इम्प्लिमेंटेशन बदल सकते हैं बिना निर्भर कोड को प्रभावित किए।

लचीलापन: आप विभिन्न इम्प्लिमेंटेशन को विभिन्न वातावरणों (विकास, टेस्टिंग, उत्पादन) के लिए कॉन्फ़िगर कर सकते हैं बिना अपने बिजनेस लॉजिक को बदले।

कंस्ट्रक्टर इंजेक्शन: 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
    // ... और कई और विधियाँ
}

छोटा इंटरफेस इंटरफेस सेग्रीगेशन प्रिंसिपल का पालन करता है—क्लाइंट्स को उन विधियों पर निर्भर नहीं होना चाहिए जिन्हें वे उपयोग नहीं करते। यह आपके कोड को अधिक लचीला और आसान टेस्ट योग्य बनाता है।

रियल-वर्ल्ड उदाहरण: डेटाबेस अभिसरण

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 APIs in Go बनाते हैं, जहां आपको कई स्तरों के डिपेंडेंसी को समन्वित करना पड़ता है।

डिपेंडेंसी इंजेक्शन फ्रेमवर्क

बड़े एप्लिकेशन्स के लिए जटिल डिपेंडेंसी ग्राफ़ के साथ, डिपेंडेंसी को मैन्युअल रूप से प्रबंधित करना बोझिल हो सकता है। Go के पास कई DI फ्रेमवर्क हैं जो मदद कर सकते हैं:

Google Wire (कॉम्पाइल-टाइम DI)

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 (रनटाइम DI)

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)
    }
}

फ्रेमवर्क का उपयोग कब करें

फ्रेमवर्क का उपयोग करें जब:

  • आपका डिपेंडेंसी ग्राफ़ जटिल है जिसमें कई परस्पर निर्भर घटक हैं
  • आपके पास एक ही इंटरफेस के कई इम्प्लिमेंटेशन हैं जिन्हें कॉन्फ़िगरेशन के आधार पर चुना जाना चाहिए
  • आप ऑटोमैटिक डिपेंडेंसी रिज़ॉल्यूशन चाहते हैं
  • आप एक बड़े एप्लिकेशन बनाते हैं जहां मैन्युअल वायरिंग गलतियों का कारण बन सकता है

मैन्युअल DI पर रहें जब:

  • आपका एप्लिकेशन छोटा से मध्यम आकार का है
  • डिपेंडेंसी ग्राफ़ सरल और आसान है
  • आप डिपेंडेंसी को न्यूनतम और स्पष्ट रखना चाहते हैं
  • आप स्पष्ट कोड को जनरेटेड कोड के ऊपर पसंद करते हैं

डिपेंडेंसी इंजेक्शन के साथ टेस्टिंग

डिपेंडेंसी इंजेक्शन का एक प्राथमिक लाभ सुधारित टेस्ट योग्यता है। यहाँ यह दिखाता है कि DI टेस्टिंग को कैसे आसान बनाता है:

यूनिट टेस्टिंग उदाहरण

// टेस्टिंग के लिए मॉक इम्प्लिमेंटेशन
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 लाइब्रेरी के साथ काम करते हैं, तो आप मॉक रिपॉजिटरी इंजेक्ट कर सकते हैं ताकि सर्विस लॉजिक को टेस्ट किया जा सके बिना डेटाबेस सेटअप की आवश्यकता हो।

सामान्य पैटर्न और सर्वोत्तम प्रथाएँ

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("user repository cannot be nil")
    }
    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 {
    // ...
}

जब निर्भरता इंजेक्शन का उपयोग न करें

निर्भरता इंजेक्शन एक शक्तिशाली उपकरण है, लेकिन यह हमेशा आवश्यक नहीं होता:

DI छोड़ें:

  • सरल वैल्यू ऑब्जेक्ट या डेटा संरचनाओं के लिए
  • आंतरिक सहायक फंक्शंस या यूटिलिटीज के लिए
  • वन-ऑफ स्क्रिप्ट्स या छोटे यूटिलिटीज के लिए
  • जब सीधा संस्थापन स्पष्ट और सरल हो

DI का उपयोग न करने का उदाहरण:

// सरल संरचना - DI की आवश्यकता नहीं है
type Point struct {
    X, Y float64
}

func NewPoint(x, y float64) Point {
    return Point{X: x, Y: y}
}

// सरल यूटिलिटी - DI की आवश्यकता नहीं है
func FormatCurrency(amount float64) string {
    return fmt.Sprintf("$%.2f", amount)
}

Go इकोसिस्टम के साथ एकीकरण

निर्भरता इंजेक्शन Go के मानक लाइब्रेरी के लिए वेब स्क्रैपिंग या PDF रिपोर्ट्स उत्पन्न करने का उपयोग करने वाले अनुप्रयोगों के निर्माण के साथ अन्य Go पैटर्न और उपकरणों के साथ बिना किसी समस्या के काम करता है:

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 कोड। इस लेख में उल्लिखित पैटर्नों का पालन करके—कंस्ट्रक्टर इंजेक्शन, इंटरफेस-आधारित डिजाइन, और कम्पोजिशन रूट पैटर्न—आप उन अनुप्रयोगों को बनाएंगे जो आसानी से समझे, परीक्षण किए और संशोधित किए जा सकते हैं।

छोटे से मध्यम अनुप्रयोगों के लिए मैनुअल कंस्ट्रक्टर इंजेक्शन से शुरू करें, और जैसे-जैसे आपका निर्भरता ग्राफ बढ़ता है, Wire या Dig जैसे फ्रेमवर्क का विचार करें। याद रखें कि लक्ष्य स्पष्टता और परीक्षण योग्यता है, नहीं तो अपनी ही तरह की जटिलता।

अधिक Go विकास संसाधनों के लिए, हमारी Go चिट्ठा पर Go सिंटैक्स और सामान्य पैटर्न पर त्वरित संदर्भ के लिए देखें।

उपयोगी लिंक

बाहरी संसाधन