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
Start with concrete implementations - Mulai dengan menulis kode konkret terlebih dahulu
Write working code - Fokus pada kode yang berfungsi
Identify patterns - Cari pola dan abstraksi yang muncul secara alami
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
YAGNI (You Ain't Gonna Need It) - Hindari membuat interface untuk fitur yang tidak diperlukan
Interface yang lebih kecil - Interface cenderung lebih fokus dan spesifik
Lebih mudah di-maintain - Tidak terikat dengan desain interface yang mungkin salah
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