Clean Code dengan Go — Part 8: Refactoring — Perbaiki Tanpa Merusak
Refactoring tanpa test adalah judi. Pelajari teknik refactoring yang aman dan terapkan Boy Scout Rule di setiap commit Anda.
Jumat sore, jam 4 lebih. Deadline sprint sudah lewat dua hari yang lalu. Arif, seorang backend engineer senior di sebuah startup fintech, membuka file order.go yang sudah bertahun-tahun tidak disentuh. Fungsinya? Satu fungsi, 300 baris, nama variabel x, tmp, d2. Dia menggeleng — “ini harus dirapikan.” Dan karena percaya diri, dia langsung refactor tanpa membuat satu pun unit test.
Sabtu pagi, alarm notifikasi berbunyi. Production down. Order tidak bisa diproses. Saldo pelanggan terpotong tapi barang tidak dikirim. Arif menghabiskan seluruh akhir pekan me-rollback perubahan, debug, dan menulis postmortem yang memalukan. Senin paginya, dia menulis satu kalimat di sticky note yang ditempel di monitornya: “Refactoring tanpa safety net adalah judi.”
Itulah pelajaran yang kita bahas hari ini. Refactoring bukan soal mengubah kode karena terlihat jelek — melainkan tentang memperbaiki struktur tanpa mengubah perilaku, dengan jaring pengaman yang kokoh.
Siklus Refactoring yang Aman
Sebelum menyentuh satu baris pun kode lama, pastikan Anda mengikuti siklus ini:
flowchart LR
A[Existing Code] --> B{Tests Exist?}
B -- No --> C[Write Tests First]
C --> D[Refactor]
B -- Yes --> D
D --> E[Run Tests]
E -- Fail --> F[Fix Refactor]
F --> D
E -- Pass --> G[Commit]
G --> H[Code Review]
H --> I[Done]
Aturannya sederhana: jangan refactor jika belum ada test. Test adalah jaring pengaman Anda. Jika test sudah ada dan tetap hijau setelah refactoring, Anda bisa yakin perilaku kode tidak berubah.
Konsep Inti
Boy Scout Rule
“Leave the campsite cleaner than you found it.” — Robert C. Martin
Dalam konteks kode: setiap kali Anda membuka sebuah file, tinggalkan dalam kondisi sedikit lebih baik dari sebelumnya. Bukan berarti harus refactor total — cukup rename satu variabel yang membingungkan, pecah satu fungsi yang terlalu panjang, atau hapus komentar yang sudah basi.
Teknik Refactoring Umum
| Teknik | Penjelasan |
|---|---|
| Extract Method | Pisahkan blok logika ke fungsi tersendiri |
| Rename | Beri nama yang mencerminkan maksud sebenarnya |
| Extract Interface | Abstraksi dependensi agar mudah diganti/ditest |
| Move Function | Pindahkan fungsi ke package yang lebih sesuai |
Code Smells yang Perlu Diwaspadai
- Long Method — fungsi lebih dari ~20 baris, tanda tanggung jawab terlalu banyak
- Feature Envy — fungsi lebih banyak mengakses data struct lain daripada miliknya sendiri
- Primitive Obsession — mengoper 6 parameter
stringterpisah padahal harusnya satu struct - Data Clumps — kelompok data yang selalu muncul bersama tapi belum dijadikan struct
❌ Implementasi yang Salah
Berikut adalah fungsi pemrosesan order yang khas dari codebase yang belum pernah disentuh:
// ❌ BAD: Satu fungsi dengan terlalu banyak tanggung jawab,
// primitive obsession, dan magic numbers
func processOrder(custName string, custEmail string, custPhone string,
itemName string, itemQty int, itemPrice float64) (string, error) {
// validasi manual yang tersebar
if custName == "" || custEmail == "" || custPhone == "" {
return "", fmt.Errorf("customer data incomplete")
}
if itemQty <= 0 || itemPrice <= 0 {
return "", fmt.Errorf("invalid item")
}
// magic number: apa itu 1.11? Pajak? Diskon?
total := float64(itemQty) * itemPrice * 1.11
// logika diskon tertanam di sini
if total > 500000 {
total = total * 0.95
}
// logging, DB insert, email — semua di sini
log.Printf("Processing order for %s (%s)", custName, custEmail)
orderID := fmt.Sprintf("ORD-%d", time.Now().Unix())
// db.Insert(...) <- bayangkan ini ada
// email.Send(...) <- dan ini juga
log.Printf("Order %s created, total: %.2f", orderID, total)
return orderID, nil
}
Mengapa salah?
- 6 parameter primitif yang bisa salah urutan kapan saja
- Magic number
1.11dan0.95tanpa penjelasan - Validasi, kalkulasi, logging, dan persistensi bercampur dalam satu fungsi
- Tidak bisa ditest secara terisolasi
✅ Implementasi yang Benar
Setelah refactoring bertahap dengan test di setiap langkah:
// ✅ GOOD: Setelah refactoring — struct yang tepat, konstanta bernama,
// dan fungsi dengan tanggung jawab tunggal
// Extract Method + Primitive Obsession fix
type Customer struct {
Name string
Email string
Phone string
}
type OrderItem struct {
Name string
Quantity int
Price float64
}
// Named constants menggantikan magic numbers
const (
taxRate = 0.11
bulkDiscountRate = 0.05
bulkDiscountMin = 500_000.0
)
func (c Customer) validate() error {
if c.Name == "" || c.Email == "" || c.Phone == "" {
return fmt.Errorf("customer data incomplete")
}
return nil
}
func (item OrderItem) validate() error {
if item.Quantity <= 0 || item.Price <= 0 {
return fmt.Errorf("invalid order item: %s", item.Name)
}
return nil
}
// Extract Method: kalkulasi total berdiri sendiri dan bisa ditest
func calculateTotal(item OrderItem) float64 {
total := float64(item.Quantity) * item.Price * (1 + taxRate)
if total > bulkDiscountMin {
total *= (1 - bulkDiscountRate)
}
return total
}
// Fungsi utama kini bersih dan mudah dibaca
func processOrder(customer Customer, item OrderItem) (string, error) {
if err := customer.validate(); err != nil {
return "", err
}
if err := item.validate(); err != nil {
return "", err
}
total := calculateTotal(item)
orderID := generateOrderID()
log.Printf("Order %s created for %s, total: %.2f", orderID, customer.Name, total)
return orderID, nil
}
func generateOrderID() string {
return fmt.Sprintf("ORD-%d", time.Now().Unix())
}
Mengapa benar?
- Struct
CustomerdanOrderItemmenggantikan 6 parameter primitif - Konstanta bernama menggantikan magic numbers
- Setiap fungsi punya satu tanggung jawab yang jelas
- Setiap bagian bisa ditest secara independen
Use Case Nyata: Refactoring Bertahap
Bayangkan Anda punya fungsi processOrder versi buruk di atas. Berikut urutan refactoring yang aman:
Langkah 1 — Tulis test dulu (sebelum ubah apapun):
func TestCalculateTotal_WithBulkDiscount(t *testing.T) {
item := OrderItem{Name: "Laptop", Quantity: 10, Price: 60_000}
got := calculateTotal(item)
want := 10 * 60_000.0 * 1.11 * 0.95
if math.Abs(got-want) > 0.01 {
t.Errorf("got %.2f, want %.2f", got, want)
}
}
Langkah 2 — Extract Method: Pisahkan calculateTotal ke fungsi tersendiri. Jalankan test → harus tetap hijau.
Langkah 3 — Fix Primitive Obsession: Buat struct Customer dan OrderItem. Update signature fungsi. Jalankan test → harus tetap hijau.
Langkah 4 — Named Constants: Ganti 1.11 dengan taxRate, 0.95 dengan 1 - bulkDiscountRate. Jalankan test → harus tetap hijau.
Langkah 5 — Commit dan code review.
Setiap langkah kecil, setiap langkah aman.
Ringkasan
Kunci Refactoring yang Aman:
- 🧪 Test dulu, refactor kemudian — jangan pernah sebaliknya
- 🏕️ Boy Scout Rule — tinggalkan kode lebih bersih dari sebelumnya
- 🔬 Langkah kecil — satu teknik, satu commit, satu PR
- 🏷️ Nama yang jelas — konstanta, fungsi, dan struct yang mencerminkan maksud
- 👃 Kenali code smells — Long Method, Primitive Obsession, Feature Envy, Data Clumps
🎯 Challenge
Terapkan Boy Scout Rule hari ini:
Buka file Go mana saja yang akan Anda sentuh minggu ini. Sebelum menambahkan fitur baru, lakukan satu hal kecil:
- Rename variabel
dmenjadidiscount - Pisahkan satu blok logika menjadi fungsi tersendiri
- Ganti satu magic number dengan konstanta bernama
Satu perubahan kecil. Satu commit terpisah. Tulis di commit message: refactor: apply boy scout rule.
🎉 Selamat! Kamu Telah Menyelesaikan Seri Clean Code dengan Go!
Ini adalah bagian terakhir dari seri Clean Code dengan Go. Kamu telah menempuh perjalanan dari nama variabel yang bermakna hingga refactoring yang aman. Berikut adalah indeks lengkap semua part:
| Part | Topik | Link |
|---|---|---|
| 1 | Meaningful Names — Nama yang Bermakna | Baca |
| 2 | Functions — Fungsi yang Bersih | Baca |
| 3 | Comments — Komentar yang Tepat | Baca |
| 4 | Error Handling — Tangani Error dengan Elegan | Baca |
| 5 | Formatting & Structure — Kode yang Rapi | Baca |
| 6 | SOLID Principles — Desain yang Kokoh | Baca |
| 7 | Testing — Test yang Bersih | Baca |
| 8 | Refactoring — Perbaiki Tanpa Merusak | Kamu di sini |
Terima kasih sudah mengikuti seri ini sampai akhir. Sekarang giliran kamu untuk menerapkannya — satu fungsi, satu commit, satu hari pada satu waktu. 🚀
| 🇮🇩 Versi Indonesia | 🇬🇧 English version |
| ← Part 7: Testing — Test yang Bersih | Ini adalah bagian terakhir dari seri ini 🏁 |