Dependency Injection #

Hampir setiap engineer pernah mengalami momen yang sama: diminta memperbaiki sebuah bug di kode yang sudah berumur setahun, dan sebelum bisa menulis satu baris pun harus memahami jaringan dependency yang saling berkaitan seperti benang kusut. ServiceA membuat RepositoryB sendiri di dalam constructor, yang kemudian membuat ClientC secara langsung, yang memerlukan konfigurasi dari os.Getenv yang tersebar di mana-mana. Untuk menulis satu unit test pun harus menyiapkan koneksi database nyata. Dependency Injection adalah pola desain yang langsung menyerang akar masalah ini: objek tidak bertanggung jawab membuat dependency-nya sendiri — dependency itu datang dari luar. Sesederhana itu konsepnya, sebesar itu dampaknya. Artikel ini membahas DI dari masalah konkret yang ia selesaikan, tiga teknik injeksi dengan contoh kode Go, Dart, dan Java, kapan pakai DI manual vs framework, scope lifecycle, hingga anti-pattern yang sering muncul ketika DI diterapkan setengah-setengah.

Apa Itu Dependency Injection? #

Dependency Injection adalah design pattern di mana sebuah objek tidak membuat sendiri dependency yang ia butuhkan, melainkan dependency tersebut diberikan dari luar. Objek hanya mendeklarasikan apa yang ia butuhkan — biasanya melalui interface — dan pihak luar (bisa main(), framework, atau test) yang memutuskan implementasi mana yang diberikan.

// ANTI-PATTERN: UserService membuat dependency sendiri — hidden coupling
type UserService struct{}

func (s *UserService) GetUser(id string) (*User, error) {
    repo := NewMySQLUserRepository() // ← dibuat di dalam — tidak bisa ditest
    return repo.FindByID(id)
}

// BENAR: dependency diterima dari luar via constructor
type UserService struct {
    repo UserRepository // ← interface, bukan implementasi konkret
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUser(id string) (*User, error) {
    return s.repo.FindByID(id) // tidak tahu MySQL, PostgreSQL, atau in-memory
}

Perubahan dari anti-pattern ke yang benar terlihat kecil, tapi dampaknya sangat besar: UserService kini tidak tahu dan tidak peduli apakah UserRepository-nya menyimpan data di MySQL, PostgreSQL, Redis, atau bahkan array in-memory untuk keperluan test. Siapa yang memanggil NewUserService yang menentukan itu.

flowchart LR
    subgraph Before["❌ Sebelum DI — Tight Coupling"]
        US1[UserService] -->|"new MySQLRepo()"| R1[MySQLUserRepository]
        R1 -->|"os.Getenv(DB_URL)"| DB1["(MySQL)"]
    end
    subgraph After["✅ Setelah DI — Loose Coupling"]
        Caller["main / test"] -->|"inject repo"| US2[UserService]
        Caller -->|"buat"| R2["UserRepository\ninterface"]
        R2 -.->|"prod: implements"| R3[MySQLUserRepository]
        R2 -.->|"test: implements"| R4[InMemoryUserRepository]
    end

Hubungan DI dengan IoC dan SOLID #

DI adalah implementasi paling konkret dari Inversion of Control — jika IoC adalah prinsipnya, DI adalah mekanisme teknisnya. DI juga secara langsung mengimplementasikan Dependency Inversion Principle (DIP), huruf terakhir dari SOLID:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

flowchart TD
    subgraph Without["❌ Tanpa DI — High-Level bergantung Low-Level"]
        OS1["OrderService\nhigh-level"] -->|depends on directly| R1["MySQLOrderRepository\nlow-level konkret"]
        Note1["Ganti MySQL → PostgreSQL\n= ubah OrderService"]
    end
    subgraph With["✅ Dengan DI — Keduanya bergantung Abstraksi"]
        OS2["OrderService\nhigh-level"] -->|depends on| IFACE["OrderRepository\ninterface / abstraksi"]
        R2[MySQLOrderRepository] -->|implements| IFACE
        R3[PostgresOrderRepository] -->|implements| IFACE
        R4[InMemoryOrderRepository] -->|implements| IFACE
        Note2["Ganti MySQL → Postgres\n= hanya ubah wiring di main()"]
    end
KonsepHubungan dengan DI
Inversion of ControlDI adalah salah satu cara mengimplementasikan IoC
Dependency Inversion PrincipleDI adalah implementasi teknis dari DIP
Clean ArchitectureDI memungkinkan domain tidak tahu implementasi infrastruktur
TestabilityDI memungkinkan swap implementasi nyata dengan fake/mock

Tiga Teknik Injeksi #

Ada tiga cara untuk “menyuntikkan” dependency ke sebuah objek. Masing-masing punya karakteristik dan use case yang berbeda.

1. Constructor Injection — Pilihan Utama #

Dependency diberikan saat objek dibuat melalui constructor. Ini adalah teknik yang paling direkomendasikan karena membuat semua dependency terlihat jelas, wajib, dan immutable sejak objek pertama kali dibuat.

// Go — constructor injection
type OrderService struct {
    repo    OrderRepository  // interface
    emailer EmailSender      // interface
    logger  Logger           // interface
}

func NewOrderService(
    repo    OrderRepository,
    emailer EmailSender,
    logger  Logger,
) *OrderService {
    // Semua dependency terlihat jelas di sini — tidak ada yang tersembunyi
    return &OrderService{repo: repo, emailer: emailer, logger: logger}
}
// Go — constructor injection (semua dependency wajib)
type AuthBloc struct {
    repository   AuthRepository
    tokenStorage TokenStorage
    analytics    AnalyticsService
}

func NewAuthBloc(repository AuthRepository, tokenStorage TokenStorage, analytics AnalyticsService) *AuthBloc {
    return &AuthBloc{
        repository:   repository,
        tokenStorage: tokenStorage,
        analytics:    analytics,
    }
}

func (b *AuthBloc) Login(email, password string) error {
    token, err := b.repository.Login(email, password)
    if err != nil {
        return err
    }
    if err := b.tokenStorage.Save(token); err != nil {
        return err
    }
    b.analytics.Track("user_logged_in", map[string]string{"email": email})
    return nil
}

// Testing: inject fake implementation
func main() {
    bloc := NewAuthBloc(
        FakeAuthRepository(),     // ← fake, bukan network
        InMemoryTokenStorage(),   // ← fake, bukan Hive/SharedPrefs
        NoOpAnalytics(),          // ← fake, tidak kirim event nyata
    )
    _ = bloc
}
// Go — constructor injection (pola yang direkomendasikan)
type PaymentService struct {
    repo     PaymentRepository
    notifier NotificationService
}

// Dependency wajib lewat constructor — tidak ada framework magic
func NewPaymentService(repo PaymentRepository, notifier NotificationService) *PaymentService {
    return &PaymentService{repo: repo, notifier: notifier}
}

Keunggulan constructor injection:

KeunggulanPenjelasan
Dependency terlihat jelasMelihat constructor cukup untuk tahu semua yang dibutuhkan
Immutable setelah dibuatTidak bisa diganti setelah konstruksi — lebih aman
Compiler enforcementCompiler error jika dependency tidak diberikan
Mudah ditesTest tinggal panggil constructor dengan fake implementation

2. Setter Injection — Untuk Dependency Opsional #

Dependency diberikan setelah objek dibuat melalui setter method. Gunakan ini hanya untuk dependency yang benar-benar opsional — yang punya nilai default yang masuk akal jika tidak diset.

// BENAR: setter injection untuk dependency opsional dengan default
type EmailService struct {
    sender    EmailSender
    logger    Logger
    rateLimit int
}

func NewEmailService(sender EmailSender, logger Logger) *EmailService {
    return &EmailService{
        sender:    sender,
        logger:    logger,
        rateLimit: 100, // ← default yang masuk akal
    }
}

// Setter hanya untuk override default — opsional
func (s *EmailService) SetRateLimit(limit int) {
    s.rateLimit = limit
}

// ANTI-PATTERN: setter injection untuk dependency wajib
type UserService struct {
    repo UserRepository // wajib, tapi tidak di-enforce lewat constructor
}

func NewUserService() *UserService {
    return &UserService{} // repo masih nil — bom waktu!
}

func (s *UserService) SetRepo(repo UserRepository) {
    s.repo = repo
}
// Jika caller lupa memanggil SetRepo → s.repo nil → panic saat dipakai

3. Parameter Injection — Untuk Variasi Per-Call #

Dependency diberikan sebagai parameter fungsi, bukan disimpan di struct. Cocok untuk dependency yang berbeda setiap kali fungsi dipanggil, atau untuk operasi satu kali yang tidak perlu disimpan.

// Parameter injection — context dan transaction diinjeksikan per-call
func ProcessRefund(
    ctx        context.Context, // ← context berbeda setiap request
    tx         *sql.Tx,         // ← transaction berbeda per-operasi
    refundRepo RefundRepository,
    orderRepo  OrderRepository,
    refundID   string,
) error {
    refund, err := refundRepo.FindByID(ctx, refundID)
    if err != nil {
        return err
    }
    order, err := orderRepo.FindByID(ctx, refund.OrderID)
    if err != nil {
        return err
    }
    return refundRepo.MarkCompleted(ctx, tx, refund, order)
}

context.Context di Go adalah contoh klasik parameter injection — ia membawa deadline, cancellation signal, dan trace ID yang berbeda untuk setiap request, sehingga tidak bisa disimpan di struct sebagai field permanen.

Ringkasan perbandingan tiga teknik:

TeknikKapan DigunakanKelebihanKekurangan
ConstructorDependency wajibEksplisit, immutable, compiler-safe
SetterDependency opsional dengan defaultFleksibel untuk konfigurasiBisa digunakan dalam state tidak valid
ParameterDependency per-call (context, tx)Tepat untuk scope terbatasVerbose jika banyak parameter

DI Manual vs DI Framework #

Ada dua pendekatan dalam praktik: merakit dependency secara manual di main(), atau menggunakan framework/container yang melakukan wiring otomatis.

DI Manual — Sederhana dan Transparan #

// main.go — semua wiring eksplisit, tidak ada magic
func main() {
    // Layer infrastruktur
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil { log.Fatal(err) }
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)

    redisClient := redis.NewClient(&redis.Options{
        Addr: os.Getenv("REDIS_URL"),
    })

    // Layer repository
    userRepo  := repository.NewUserRepository(db)
    orderRepo := repository.NewOrderRepository(db)
    cache     := cache.NewRedisCache(redisClient)

    // Layer service
    userService  := service.NewUserService(userRepo, cache)
    orderService := service.NewOrderService(orderRepo, userService)

    // Layer handler
    userHandler  := handler.NewUserHandler(userService)
    orderHandler := handler.NewOrderHandler(orderService)

    // Server
    router := setupRouter(userHandler, orderHandler)
    log.Fatal(http.ListenAndServe(":8080", router))
}

DI dengan Framework/Container #

Untuk sistem besar dengan puluhan atau ratusan komponen:

// Go dengan Uber FX — framework DI berbasis functional options
func main() {
    fx.New(
        fx.Provide(
            database.NewPostgresDB,   // provide *sql.DB
            cache.NewRedisClient,     // provide *redis.Client
            repository.NewUserRepo,   // butuh *sql.DB, provide UserRepository
            repository.NewOrderRepo,  // butuh *sql.DB, provide OrderRepository
            cache.NewRedisCache,      // butuh *redis.Client, provide CacheStore
            service.NewUserService,   // butuh UserRepository + CacheStore
            service.NewOrderService,  // butuh OrderRepository + UserService
            handler.NewUserHandler,   // butuh UserService
            handler.NewOrderHandler,  // butuh OrderService
        ),
        fx.Invoke(startHTTPServer),
    ).Run()
    // FX otomatis resolve semua dependency berdasarkan type signature
    // Tidak perlu tulis urutan manual
}
// Go — service locator tidak umum; pola terdekat adalah registry map
// (wiring manual di composition root tetap lebih transparan)
var registry = map[string]any{}

func SetupDependencies() {
    // Infrastruktur
    registry["httpClient"] = NewHTTPClient(Env.APIURL)
    registry["storage"] = NewHiveStorage()

    // Repository
    registry["authRepository"] = NewAuthRepositoryImpl(registry["httpClient"].(*HTTPClient))
    registry["userRepository"] = NewUserRepositoryImpl(
        registry["httpClient"].(*HTTPClient),
        registry["storage"].(*HiveStorage),
    )

    // BLoC / Service — factory: instance baru setiap kali diambil
    registry["authBlocFactory"] = func() *AuthBloc {
        return NewAuthBloc(
            registry["authRepository"].(AuthRepository),
            registry["storage"].(TokenStorage),
        )
    }
}
flowchart TD
    subgraph Manual["DI Manual"]
        M1[main.go] --> M2["Inisialisasi infra\nDB, Redis, HTTP"]
        M2 --> M3["Inisialisasi repo\npakai infra"]
        M3 --> M4["Inisialisasi service\npakai repo"]
        M4 --> M5["Inisialisasi handler\npakai service"]
        M5 --> M6[Start server]
    end
    subgraph Framework["DI Framework / Container"]
        F1["Daftarkan semua\nprovider"] --> F2["Container resolve\ndependency otomatis"]
        F2 --> F3["Lifecycle hooks\nstartup / shutdown"]
        F3 --> F4[Run]
    end
AspekDI ManualDI Framework
TransparansiSangat eksplisit, mudah traceAda “magic”, perlu belajar framework
BoilerplateBertambah seiring sistem besarMinimal, auto-resolve
LifecycleManual (defer close)Built-in startup/shutdown hooks
Cocok untukSistem kecil-menengahSistem besar (50+ komponen)
Error detectionCompile timeRuntime (beberapa framework)
DI framework yang menggunakan reflection (seperti beberapa framework Java/PHP) mendeteksi kesalahan wiring saat runtime, bukan compile time. Ini berarti konfigurasi yang salah baru ketahuan saat aplikasi dijalankan. Untuk Go, lebih baik menggunakan code generation (Wire) atau functional type matching (Uber FX) yang error-nya terdeteksi lebih awal.

DI dan Testability — Dampak Paling Nyata #

Manfaat DI yang paling langsung dirasakan dalam kehidupan sehari-hari adalah kemampuan menulis unit test yang cepat, terisolasi, dan deterministik — tanpa memerlukan database nyata, SMTP server, atau third-party API.

// Interface yang sama diimplementasikan oleh produksi dan fake
type UserRepository interface {
    Save(ctx context.Context, user *User) error
    FindByEmail(ctx context.Context, email string) (*User, error)
}

// Produksi: pakai PostgreSQL
type PostgresUserRepository struct{ db *sql.DB }

func (r *PostgresUserRepository) Save(ctx context.Context, user *User) error {
    _, err := r.db.ExecContext(ctx,
        "INSERT INTO users (id, email, name) VALUES ($1, $2, $3)",
        user.ID, user.Email, user.Name)
    return err
}

// Test: in-memory, tidak butuh infrastruktur apapun
type InMemoryUserRepository struct {
    users map[string]*User
    mu    sync.RWMutex
}

func NewInMemoryUserRepository() *InMemoryUserRepository {
    return &InMemoryUserRepository{users: make(map[string]*User)}
}

func (r *InMemoryUserRepository) Save(_ context.Context, user *User) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.users[user.Email] = user
    return nil
}

func (r *InMemoryUserRepository) FindByEmail(_ context.Context, email string) (*User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    if user, ok := r.users[email]; ok {
        return user, nil
    }
    return nil, ErrUserNotFound
}
// Test berjalan dalam milidetik, tidak butuh Docker/database
func TestRegisterUser_EmailAlreadyExists(t *testing.T) {
    repo    := NewInMemoryUserRepository()
    emailer := &FakeEmailSender{}
    svc     := NewUserService(repo, emailer)

    // Setup: user sudah ada
    repo.Save(context.Background(), &User{
        ID: "existing-1", Email: "[email protected]",
    })

    // Test: registrasi dengan email yang sama harus gagal
    _, err := svc.Register(context.Background(), RegisterRequest{
        Email: "[email protected]",
        Name:  "Someone Else",
    })

    assert.ErrorIs(t, err, ErrEmailAlreadyExists)
    assert.Empty(t, emailer.SentEmails) // tidak ada email yang dikirim
}

func TestRegisterUser_Success(t *testing.T) {
    repo    := NewInMemoryUserRepository()
    emailer := &FakeEmailSender{}
    svc     := NewUserService(repo, emailer)

    user, err := svc.Register(context.Background(), RegisterRequest{
        Email: "[email protected]",
        Name:  "Unis Badri",
    })

    assert.NoError(t, err)
    assert.NotEmpty(t, user.ID)
    assert.Len(t, emailer.SentEmails, 1) // welcome email terkirim
}
flowchart LR
    subgraph WithDI["Dengan DI — Unit Test Murni"]
        T1[Test] -->|inject| FAKE["InMemoryRepo\nFakeEmailer"]
        T1 -->|test| SVC[UserService]
        SVC --> FAKE
        T1 --> RESULT["✓ Cepat < 1ms\n✓ Deterministic\n✓ No infra"]
    end
    subgraph WithoutDI["Tanpa DI — Integration Test Terpaksa"]
        T2[Test] --> SVC2[UserService]
        SVC2 -->|butuh| DB[("(MySQL\nrunning)")]
        SVC2 -->|butuh| SMTP["SMTP Server\nrunning"]
        T2 --> RESULT2["✗ Lambat\n✗ Flaky\n✗ Butuh infra"]
    end

Scope Lifecycle dalam DI #

Satu hal yang sering dilupakan saat merancang DI adalah scope — berapa lama sebuah dependency hidup dan kapan instance baru dibuat.

ScopeDeskripsiContohDibuat
SingletonSatu instance seumur hidup appDB pool, logger, configSaat startup
TransientInstance baru setiap kali dibutuhkanCommand handler, DTO builderSetiap inject
ScopedSatu instance per unit kerjaHTTP request context, DB transactionPer request/job
// ANTI-PATTERN: buat koneksi DB setiap request — sangat mahal dan lambat
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // ← boros!
    defer db.Close()
    repo := repository.NewUserRepository(db)
    // ...
}

// BENAR: singleton — connection pool dibuat sekali, dibagikan ke semua handler
func main() {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    db.SetMaxOpenConns(25)   // max 25 koneksi concurrent
    db.SetMaxIdleConns(10)   // min 10 koneksi siap pakai

    // Singleton: dibuat sekali, digunakan sepanjang lifetime aplikasi
    repo    := repository.NewUserRepository(db)   // singleton
    service := service.NewUserService(repo)        // singleton
    handler := handler.NewUserHandler(service)     // singleton

    http.HandleFunc("/users", handler.GetUser)
    http.ListenAndServe(":8080", nil)
}
// Go — scope dengan composition root manual
type Container struct {
    apiClient *ApiClient
    authRepo  AuthRepository
}

func NewContainer() *Container {
    // Singleton: satu instance seumur lifetime app
    c := &Container{apiClient: NewApiClient(Env.APIURL)}
    // LazySingleton: di Go cukup dibuat sekali di sini
    c.authRepo = NewAuthRepositoryImpl(c.apiClient)
    return c
}

// Factory: instance baru setiap kali dipanggil
// Cocok untuk BLoC yang di-dispose dan di-recreate per screen
func (c *Container) NewAuthBloc() *AuthBloc {
    return NewAuthBloc(c.authRepo)
}

Anti-Pattern yang Harus Dihindari #

// ✗ Membuat dependency di dalam method — IoC dilanggar kembali
func (s *OrderService) CreateOrder(req Request) (*Order, error) {
    repo := &MySQLOrderRepository{db: globalDB} // ← tight coupling kembali!
    return repo.Save(buildOrder(req))
}
// ✓ Gunakan s.repo yang sudah diinjeksikan via constructor

// ✗ Global variable sebagai dependency — tidak bisa di-mock, race condition
var globalDB *sql.DB

func init() {
    globalDB, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// ✓ Inject *sql.DB via constructor, bukan global

// ✗ God constructor — tanda service melakukan terlalu banyak hal
func NewOrderService(
    repo OrderRepository,
    userRepo UserRepository,
    productRepo ProductRepository,
    inventoryRepo InventoryRepository,
    paymentGW PaymentGateway,
    emailer EmailSender,
    sms SMSSender,
    push PushNotifier,
    logger Logger,
    cache CacheStore,
    config *Config,
    bus EventBus,
) *OrderService { ... }
// 12 dependency = OrderService melakukan terlalu banyak → pecah menjadi service lebih kecil
// ✓ Jika > 4-5 dependency, evaluasi apakah SRP sudah terpenuhi

// ✗ Inject concrete type bukan interface
func NewOrderService(repo *MySQLOrderRepository) *OrderService { ... }
// ✓ Inject interface
func NewOrderService(repo OrderRepository) *OrderService { ... }

// ✗ Setter injection untuk dependency wajib — bisa digunakan dalam state invalid
type UserService struct{ repo UserRepository }
func NewUserService() *UserService { return &UserService{} } // repo = nil!
func (s *UserService) SetRepo(r UserRepository) { s.repo = r }
// ✓ Constructor injection untuk dependency wajib
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo} // compiler error jika tidak ada repo
}

// ✗ Inject IoC container ke dalam service — service tahu tentang container
type OrderService struct { container *Container }
func (s *OrderService) CreateOrder(req Request) error {
    repo := s.container.Get("orderRepo").(OrderRepository) // hidden dependency!
    ...
}
// ✓ Inject dependency langsung, bukan container

Checklist Implementasi DI #

DESAIN INTERFACE:
  □ Setiap dependency dideklarasikan sebagai interface di sisi consumer
  □ Interface minimal — hanya method yang benar-benar digunakan consumer
  □ Interface tidak bocorkan detail implementasi (tidak ada *sql.DB di interface)

CONSTRUCTOR INJECTION:
  □ Semua dependency wajib di-inject via constructor
  □ Constructor tidak membuat dependency sendiri (tidak ada `new`, `Open`, dll)
  □ Dependency yang opsional punya default yang masuk akal

WIRING:
  □ Semua wiring terpusat di satu tempat (main.go, wire.go, module.go)
  □ Tidak ada global variable yang digunakan sebagai dependency
  □ Scope lifecycle setiap dependency sudah dipertimbangkan

TESTING:
  □ Setiap interface punya fake/mock implementation untuk testing
  □ Unit test tidak butuh koneksi DB, SMTP, atau network
  □ Test bisa dijalankan secara paralel tanpa race condition

SCOPE:
  □ Database connection pool sebagai singleton
  □ BLoC/Command handler sebagai factory (instance baru per penggunaan)
  □ Request-scoped data dilewatkan via context, bukan disimpan di struct

Ringkasan #

  • Dependency Injection adalah pattern di mana objek menerima dependency dari luar — memindahkan tanggung jawab konstruksi ke caller, bukan ke objek itu sendiri.
  • Tiga teknik injeksi: constructor injection (dependency wajib, eksplisit, immutable — pilihan utama), setter injection (dependency opsional dengan default yang masuk akal), parameter injection (dependency per-call seperti context.Context dan *sql.Tx).
  • Constructor injection paling direkomendasikan — dependency terlihat jelas dari signature, bersifat immutable, dan compiler langsung error jika ada yang tidak diberikan.
  • DI adalah implementasi DIP dari SOLID — high-level module bergantung pada abstraksi (interface), bukan implementasi konkret; ganti implementasi cukup di wiring, bukan di konsumen.
  • Dampak paling nyata adalah testability — dengan DI, setiap dependency bisa diganti fake/in-memory yang berjalan dalam milidetik tanpa database atau network.
  • DI manual (wiring di main()) cukup dan lebih transparan untuk sistem kecil-menengah; DI framework (Uber FX, Wire, Spring, get_it) lebih cocok untuk sistem besar dengan banyak komponen.
  • Perhatikan scope lifecycle: database connection pool wajib singleton, BLoC per-screen sebaiknya factory, request-scoped object lewat context.
  • God constructor (8+ dependency) adalah code smell kuat — tanda komponen melakukan terlalu banyak hal dan perlu dipecah.
  • Inject interface, bukan concrete type — satu-satunya cara memungkinkan swap implementasi tanpa mengubah semua konsumennya.

← Sebelumnya: Inversion of Control   Berikutnya: Aspect Oriented Programming →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact