Interface on the Producer Side

1. Definisi Producer vs Consumer Side

  • Producer side: Interface didefinisikan di package yang sama dengan implementasi konkret

  • Consumer side: Interface didefinisikan di package eksternal dimana interface tersebut digunakan

2. Kesalahan Umum

  • Developer sering membuat interface di producer side (kebiasaan dari Java/C#)

  • Ini memaksakan abstraksi yang mungkin tidak diperlukan oleh client

3. Prinsip yang Benar

  • "Abstractions should be discovered, not created"

  • Biarkan client yang menentukan kebutuhan abstraksinya

  • Producer hanya menyediakan implementasi konkret

4. Keuntungan Interface di Consumer Side

  • Client bisa membuat interface yang spesifik sesuai kebutuhannya

  • Mengikuti Interface Segregation Principle (SOLID)

  • Interface bisa dibuat unexported jika hanya digunakan internal

  • Tidak ada circular dependency

5. Pengecualian

  • Standard library seperti encoding package memang mendefinisikan interface di producer side

  • Hanya lakukan ini jika benar-benar yakin abstraksi akan berguna untuk banyak client

6. Returning Interfaces

  • Hindari mengembalikan interface dari fungsi

  • Kembalikan struct konkret, terima interface sebagai parameter

  • Pengecualian: error interface dan beberapa case di standard library

Contoh Kode

Salah - Interface di Producer Side

// package store (producer)
package store

type Customer struct {
    ID   string
    Name string
}

// Interface dipaksakan di producer side
type CustomerStorage interface {
    StoreCustomer(customer Customer) error
    GetCustomer(id string) (Customer, error)
    UpdateCustomer(customer Customer) error
    GetAllCustomers() ([]Customer, error)
    GetCustomersWithoutContract() ([]Customer, error)
    GetCustomersWithNegativeBalance() ([]Customer, error)
}

// Implementasi konkret
type InMemoryStore struct {
    customers map[string]Customer
}

func (s *InMemoryStore) StoreCustomer(customer Customer) error {
    s.customers[customer.ID] = customer
    return nil
}

func (s *InMemoryStore) GetCustomer(id string) (Customer, error) {
    return s.customers[id], nil
}

// ... method lainnya

// Fungsi factory yang buruk - mengembalikan interface
func NewInMemoryStore() CustomerStorage {
    return &InMemoryStore{
        customers: make(map[string]Customer),
    }
}

Benar - Interface di Consumer Side

// package store (producer) - Hanya提供 implementasi konkret
package store

type Customer struct {
    ID   string
    Name string
}

// Hanya提供 struct konkret tanpa interface
type InMemoryStore struct {
    customers map[string]Customer
}

func (s *InMemoryStore) StoreCustomer(customer Customer) error {
    s.customers[customer.ID] = customer
    return nil
}

func (s *InMemoryStore) GetCustomer(id string) (Customer, error) {
    return s.customers[id], nil
}

func (s *InMemoryStore) GetAllCustomers() ([]Customer, error) {
    // implementasi
}

// Fungsi factory yang baik - mengembalikan struct konkret
func NewInMemoryStore() *InMemoryStore {
    return &InMemoryStore{
        customers: make(map[string]Customer),
    }
}

Consumer Mendefinisikan Interface Sesuai Kebutuhan

// package client (consumer)
package client

import "project/store"

// Client hanya butuh method GetAllCustomers
type customersGetter interface {
    GetAllCustomers() ([]store.Customer, error)
}

// Client lain butuh method yang berbeda
type customerWriter interface {
    StoreCustomer(customer store.Customer) error
    UpdateCustomer(customer store.Customer) error
}

func ProcessCustomers(getter customersGetter) {
    customers, _ := getter.GetAllCustomers()
    // process customers
}

func main() {
    // Gunakan concrete implementation
    store := store.NewInMemoryStore()
    
    // Client bisa menggunakan concrete implementation langsung
    // atau membuat abstraction sesuai kebutuhan
    ProcessCustomers(store) // works because InMemoryStore implements the interface implicitly
}

Penerapan Postel's Law dalam Go

// Conservative in what you do: return structs
func NewDatabaseStore() *DatabaseStore { // return concrete struct
    return &DatabaseStore{}
}

// Liberal in what you accept: accept interfaces
func ProcessData(reader io.Reader) error { // accept interface
    data, _ := io.ReadAll(reader)
    // process data
    return nil
}

Best Practices

  1. Start with concrete implementations - Jangan mulai dengan interface

  2. Let clients discover interfaces - Biarkan client yang menentukan abstraksi

  3. Keep interfaces small - Interface seharusnya spesifik dan minimalis

  4. Return structs, accept interfaces - Pola umum yang baik

  5. Use unexported interfaces - Untuk kebutuhan internal client

Pengecualian yang Valid

// Standard library case where interface on producer side makes sense
package encoding

type BinaryMarshaler interface {
    MarshalBinary() (data []byte, err error)
}

type BinaryUnmarshaler interface {
    UnmarshalBinary(data []byte) error
}

Kesimpulan: Jangan paksakan interface di producer side kecuali Anda benar-benar yakin abstraksi tersebut akan berguna untuk banyak client dan sudah terbukti kebutuhan nyatanya.

Last updated