Don’t design with interfaces, discover them

Inti Pesan

Jangan memaksakan desain interface di awal pengembangan, tetapi biarkan interface muncul secara alami melalui refactoring setelah Anda memiliki implementasi konkret yang bekerja.

Prinsip Dasar

  1. Start with concrete implementations - Mulai dengan menulis kode konkret terlebih dahulu

  2. Write working code - Fokus pada kode yang berfungsi

  3. Identify patterns - Cari pola dan abstraksi yang muncul secara alami

  4. Extract interfaces - Baru kemudian ekstrak interface dari kode yang sudah ada

Contoh dalam Go

Cara yang Salah (Design dengan Interface di Awal)

// Memaksakan interface di awal tanpa tahu kebutuhan sebenarnya
type Storage interface {
    Save(data []byte) error
    Load(id string) ([]byte, error)
    Delete(id string) error
}

// Harus implementasi semua method bahkan jika tidak perlu
type FileStorage struct{}

func (f *FileStorage) Save(data []byte) error {
    // implementation
}

func (f *FileStorage) Load(id string) ([]byte, error) {
    // implementation
}

func (f *FileStorage) Delete(id string) error {
    // implementation, tapi mungkin tidak pernah digunakan
}

Cara yang Benar (Discover Interfaces)

// Mulai dengan implementasi konkret
type FileStorage struct {
    basePath string
}

// Hanya tulis method yang benar-benar dibutuhkan
func (f *FileStorage) Save(filename string, data []byte) error {
    return os.WriteFile(filepath.Join(f.basePath, filename), data, 0644)
}

func (f *FileStorage) Load(filename string) ([]byte, error) {
    return os.ReadFile(filepath.Join(f.basePath, filename))
}

// Setelah kode digunakan, baru discover interface yang diperlukan
type FileReader interface {
    Load(filename string) ([]byte, error)
}

type FileWriter interface {
    Save(filename string, data []byte) error
}

// Gunakan interface yang lebih spesifik
func processData(reader FileReader, filename string) {
    data, _ := reader.Load(filename)
    // process data
}

Contoh Lain: Database Operations

// Mulai dengan konkret implementation
type MySQLUserRepository struct {
    db *sql.DB
}

func (r *MySQLUserRepository) CreateUser(user User) error {
    _, err := r.db.Exec("INSERT INTO users (...) VALUES (...)")
    return err
}

func (r *MySQLUserRepository) FindUserByID(id int) (*User, error) {
    row := r.db.QueryRow("SELECT * FROM users WHERE id = ?", id)
    // parse row
}

// Setelah beberapa waktu, discover interface yang dibutuhkan
type UserCreator interface {
    CreateUser(user User) error
}

type UserFinder interface {
    FindUserByID(id int) (*User, error)
}

// Test dengan mock yang lebih sederhana
type MockUserFinder struct {
    Users map[int]*User
}

func (m *MockUserFinder) FindUserByID(id int) (*User, error) {
    return m.Users[id], nil
}

Keuntungan Pendekatan Ini

  1. YAGNI (You Ain't Gonna Need It) - Hindari membuat interface untuk fitur yang tidak diperlukan

  2. Interface yang lebih kecil - Interface cenderung lebih fokus dan spesifik

  3. Lebih mudah di-maintain - Tidak terikat dengan desain interface yang mungkin salah

  4. Better abstraction - Abstraksi muncul dari pengalaman nyata bukan asumsi

Kapan Harus Mendesain Interface di Awal?

Hanya ketika:

  • Anda sudah sangat familiar dengan domain

  • Membuat library untuk konsumsi publik

  • Ada requirement yang sangat jelas dan stabil

Quotes ini mengajarkan untuk lebih pragmatis dan membiarkan solusi emerge dari masalah nyata daripada memaksakan abstraksi prematur.

Last updated