Prinsip Clean Code yang Wajib Diketahui Setiap Developer

Foto oleh Unsplash

Foto oleh Unsplash
Clean code adalah fondasi dari software yang mudah dipelihara. Setiap developer pernah mewarisi codebase yang terasa mustahil untuk dinavigasi — nama variabel yang kriptik, fungsi yang mengerjakan lima belas hal sekaligus, dan nol test. Prinsip clean code memberi Anda kosakata bersama dan teknik konkret untuk menulis software yang akan membuat diri Anda di masa depan (dan tim Anda) berterima kasih. Di postingan ini kita membahas praktik clean code paling berdampak dengan contoh TypeScript nyata yang bisa langsung Anda terapkan.
Nama adalah cara utama kita mengkomunikasikan maksud dalam kode. Variabel, fungsi, atau kelas yang dinamai dengan baik dapat menghilangkan kebutuhan akan komentar sama sekali. Tujuannya adalah menulis kode yang terbaca seperti kalimat yang ditulis dengan baik — siapa pun di tim Anda harus bisa memahami apa yang dilakukan sebuah fungsi hanya dengan membaca nama dan signature-nya.
Hindari variabel satu huruf di luar penghitung loop yang sepele. Hindari singkatan yang menghemat tiga karakter tetapi memakan waktu sepuluh menit kebingungan. Gunakan kata benda untuk variabel dan kelas, serta kata kerja untuk fungsi. Jika Anda membutuhkan komentar untuk menjelaskan apa yang dipegang sebuah variabel, nama variabelnya salah.
Angka hard-coded yang tersebar di seluruh codebase Anda adalah mimpi buruk pemeliharaan. Ganti dengan konstanta bernama yang menjelaskan apa yang diwakili nilai tersebut dan mengapa nilainya ada. Ini membuat perubahan di masa depan menjadi sepele — Anda mengubah konstanta di satu tempat dan setiap penggunaannya diperbarui secara otomatis.
// Bad: cryptic naming
function d(a: number[], x: number): boolean {
for (let i = 0; i < a.length; i++) {
if (a[i] === x) return true;
}
return false;
}
// Good: intention-revealing names
function containsValue(numbers: number[], target: number): boolean {
return numbers.some((n) => n === target);
}
// Bad: magic numbers
if (user.age > 17 && user.memberYears > 2) { /* ... */ }
// Good: named constants
const MINIMUM_ADULT_AGE = 18;
const MINIMUM_MEMBER_YEARS_FOR_DISCOUNT = 2;
if (user.age >= MINIMUM_ADULT_AGE && user.memberYears > MINIMUM_MEMBER_YEARS_FOR_DISCOUNT) {
applyDiscount(user);
}Jalankan ESLint dengan aturan 'no-magic-numbers' dan 'id-length' untuk menangkap nama yang tidak jelas dan magic number secara otomatis selama pipeline CI Anda.
Single Responsibility Principle berlaku untuk fungsi sama seperti untuk kelas. Sebuah fungsi harus mengerjakan satu hal, mengerjakannya dengan baik, dan hanya itu saja. Tanda-tanda fungsi yang mengerjakan terlalu banyak adalah ketika Anda kesulitan memberinya nama tanpa menggunakan kata 'dan'. Fungsi kecil lebih mudah ditest, lebih mudah diberi nama, dan lebih mudah digunakan kembali.
Pecah fungsi besar menjadi fungsi-fungsi lebih kecil yang masing-masing memiliki tujuan tunggal yang jelas. Fungsi pemanggil menjadi orkestrator tingkat tinggi yang terbaca hampir seperti bahasa Inggris. Setiap fungsi pembantu kemudian dapat ditest secara terisolasi, membuat test suite Anda jauh lebih presisi dan sesi debugging Anda jauh lebih singkat.
// Bad: function that does too many things
function processUserOrder(userId: string, items: CartItem[]): void {
const user = db.findUser(userId);
if (!user) throw new Error("User not found");
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const tax = total * 0.1;
const invoice = { user, items, total, tax };
db.saveOrder(invoice);
emailService.sendConfirmation(user.email, invoice);
analyticsService.track("order_placed", { userId, total });
}
// Good: each function has one responsibility
function calculateOrderTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
function buildInvoice(user: User, items: CartItem[]): Invoice {
const total = calculateOrderTotal(items);
return { user, items, total, tax: total * 0.1 };
}
async function placeOrder(userId: string, items: CartItem[]): Promise<void> {
const user = await findUserOrThrow(userId);
const invoice = buildInvoice(user, items);
await db.saveOrder(invoice);
await emailService.sendConfirmation(user.email, invoice);
analyticsService.track("order_placed", { userId, total: invoice.total });
}Fungsi dengan lebih dari tiga argumen adalah sebuah tanda masalah. Ketika Anda menemukan diri Anda meneruskan lima parameter, pertimbangkan apakah beberapa di antaranya termasuk dalam sebuah objek, apakah fungsi tersebut melakukan terlalu banyak, atau apakah beberapa parameter harus diekstrak ke level kelas. Interface TypeScript membuat objek parameter menjadi self-documenting tanpa overhead perlu menebak posisi.
SOLID adalah sekumpulan lima prinsip desain yang membuat software berorientasi objek lebih mudah dipahami, fleksibel, dan dipelihara. Meskipun berasal dari dunia OOP, prinsip-prinsip ini berlaku sama baiknya untuk kode TypeScript fungsional. Memahami SOLID membantu Anda mengenali kapan kode Anda menuju kerapuhan dan memberi Anda alat untuk mengarahkannya kembali.
Open/Closed Principle mengatakan bahwa sebuah modul harus terbuka untuk ekstensi tetapi tertutup untuk modifikasi — Anda harus bisa menambah perilaku baru tanpa mengubah kode yang sudah teruji. Dependency Inversion Principle memberitahu kita bahwa modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada abstraksi. Dalam TypeScript, interface adalah alat utama Anda untuk keduanya.
// SOLID: Open/Closed Principle example
// Bad: must modify existing class to add new discount type
class OrderCalculator {
getDiscount(userType: string): number {
if (userType === "premium") return 0.2;
if (userType === "student") return 0.1;
return 0;
}
}
// Good: open for extension, closed for modification
interface DiscountStrategy {
calculate(orderTotal: number): number;
}
class PremiumDiscount implements DiscountStrategy {
calculate(total: number) { return total * 0.2; }
}
class StudentDiscount implements DiscountStrategy {
calculate(total: number) { return total * 0.1; }
}
class OrderCalculator {
constructor(private discount: DiscountStrategy) {}
getFinalPrice(total: number): number {
return total - this.discount.calculate(total);
}
}Liskov Substitution Principle berarti subkelas harus bisa menggantikan tipe dasarnya tanpa mengubah kebenaran program. Interface Segregation memberitahu kita untuk tidak memaksa klien bergantung pada interface yang tidak mereka gunakan — lebih baik banyak interface kecil yang terfokus daripada satu interface besar yang menangkap semuanya.
Menerapkan SOLID secara berlebihan — membuat interface untuk segalanya, memecah setiap fungsi hingga sangat kecil — menghasilkan bentuk kode yang tidak bisa dipelihara tersendiri: abstraction soup. Terapkan prinsip di mana mereka mengurangi rasa sakit dan meningkatkan kejelasan. Ketika codebase kecil dan tim bergerak cepat, kesederhanaan pragmatis sering mengalahkan kemurnian arsitektur.
Komentar terbaik adalah yang tidak perlu Anda tulis karena kode sudah menjelaskan dirinya sendiri. Komentar yang sekadar mengulang apa yang dilakukan kode menambah kebisingan. Komentar yang menjelaskan mengapa — konteks bisnis, batasan, keputusan performa yang tidak jelas — menambah nilai nyata. Selalu lebih baik self-documenting code daripada komentar penjelasan.
Kandidat yang baik untuk komentar meliputi: penjelasan batasan hukum, tautan ke spesifikasi eksternal atau laporan bug, klarifikasi algoritma kompleks dengan referensi ke paper terkait, dan dokumentasi API publik dengan JSDoc. Jangan pernah commit kode yang dikomentari — jika Anda perlu mengambilnya kembali, itulah kegunaan version control.
Sistem tipe TypeScript adalah alat dokumentasi paling powerful Anda. Signature fungsi dengan tipe parameter bernama dengan baik dan tipe return memberitahu pembaca lebih banyak daripada tiga kalimat prosa. Gabungkan tipe yang presisi dengan anotasi JSDoc @param dan @returns pada API publik dan Anda memiliki dokumentasi yang selalu sinkron dengan kode karena TypeScript menerapkannya.
Gunakan format 'Conventional Comments' dalam code review untuk membedakan nitpick dari blocker, membuat feedback review lebih jelas dan lebih cepat untuk ditindaklanjuti.
Refactoring adalah praktik disiplin dalam merestrukturisasi kode yang ada tanpa mengubah perilaku yang dapat diamati. Ini bukan penulisan ulang besar-besaran — ini adalah serangkaian transformasi kecil yang aman, masing-masing diverifikasi oleh test suite Anda. Wawasan kuncinya adalah: refactoring tidak mungkin tanpa test. Jika Anda tidak memiliki test, pekerjaan pertama Anda adalah menulisnya, lalu refactor.
Refactoring paling berharga adalah: Extract Function (tarik blok kode ke dalam fungsi bernama), Rename Variable (terapkan apa yang Anda pelajari tentang penamaan yang bermakna), Replace Conditional with Polymorphism (gunakan SOLID untuk menghilangkan rantai if/else yang bertingkat), dan Introduce Parameter Object (ganti daftar argumen panjang dengan interface bertipe). Setiap langkah meninggalkan kode lebih baik dari yang Anda temukan.
Konsep-konsep kunci dalam postingan ini meliputi SRP, OCP, DIP, refactoring, DRY, and YAGNI.