Object Calisthenics adalah sembilan aturan yang diperkenalkan oleh Jeff Bay dalam buku The ThoughtWorks Anthology. Namanya terinspirasi dari kalistenik di dunia olahraga — latihan fisik dengan gerakan-gerakan dasar yang diulang untuk membentuk otot. Bedanya, di sini yang dilatih adalah "otot" desain object-oriented kita.Aturan-aturan ini sengaja dibuat ketat dan agak ekstrem. Tujuannya bukan supaya diikuti 100% di semua kode produksi, tapi sebagai latihan sadar: dengan memaksakan batasan, kita jadi lebih peka terhadap bau kode (code smell) seperti method yang terlalu panjang, class yang mengurus terlalu banyak hal, atau primitive yang dipakai sembarangan tanpa makna domain.Berikut kesembilan aturannya.
1. Satu Level Indentasi per Method
Kalau sebuah method punya banyak level indentasi (nested if, nested for), itu tanda method tersebut melakukan terlalu banyak hal. Solusinya: ekstrak bagian yang bernidasi ke method terpisah.
2. Jangan Gunakan Kata Kunci ELSE
else sering menandakan logic yang bisa disederhanakan dengan early return, polymorphism, atau strategy pattern. Kode tanpa else biasanya lebih mudah dibaca karena alur normal dan alur pengecualian terpisah jelas.
3. Bungkus Semua Primitive dan String
Primitive seperti string atau number tidak membawa makna domain maupun validasi. Membungkusnya dalam class kecil membuat aturan bisnis (misalnya format email, batas umur) terpusat di satu tempat, bukan tercecer di banyak fungsi.
4. First Class Collections
Kalau sebuah class punya properti berupa collection (array/list), bungkus collection itu dalam class tersendiri. Dengan begitu, logic terkait collection (filter, validasi jumlah, dsb.) tidak bocor ke class lain yang memakainya.
5. Satu Titik per Baris (Law of Demeter)
Hindari rantai pemanggilan seperti a.getB().getC().doSomething(). Rantai panjang seperti ini membuat kode kita bergantung pada struktur internal object lain, bukan cuma pada object yang langsung berhubungan.
6. Jangan Disingkat
Nama variabel, method, dan class sebaiknya ditulis lengkap dan jelas. Singkatan seperti calc, mgr, atau tmp menghemat sedikit ketikan tapi membebani setiap orang yang membaca kode setelahnya.
7. Jaga Semua Entitas Tetap Kecil
Class sebaiknya tidak lebih dari sekitar 50 baris, package/folder tidak lebih dari sekitar 10 file. Batasan ini memaksa kita memecah tanggung jawab yang menumpuk di satu tempat — mirip semangat Single Responsibility Principle di prinsip SOLID.
8. Tidak Ada Class dengan Lebih dari Dua Instance Variable
Aturan ini yang paling ekstrem — dan memang sengaja dibuat begitu supaya kita mempertanyakan setiap variabel instance yang ditambahkan. Kalau sebuah class butuh banyak state, biasanya itu tanda class tersebut sebenarnya menyimpan lebih dari satu konsep dan sebaiknya dipecah.
9. Tidak Ada Getter/Setter/Property
Alih-alih meminta data lewat getter lalu memprosesnya di luar (feature envy), pindahkan behavior itu ke dalam class yang memegang data tersebut — konsisten dengan prinsip "Tell, Don't Ask".
Latihan, Bukan Aturan Mati
Sembilan aturan ini bukan checklist yang harus dipatuhi kaku di setiap file produksi — kalau dipaksakan 100%, kadang kita malah menghasilkan abstraksi berlebihan untuk kode yang sebenarnya sederhana. Perlakukan Object Calisthenics seperti namanya: latihan. Coba terapkan sesekali di kode baru atau saat refactoring, rasakan bagaimana batasan-batasan ini mendorong desain yang lebih kecil dan lebih jelas tanggung jawabnya, lalu ambil kebiasaan mana yang paling cocok untuk gaya dan konteks tim kamu.
// Sebelum: dua level indentasi bersarang
function printItems(items: string[]): void {
for (const item of items) {
if (item.length > 0) {
console.log(item);
}
}
}
Typescript
// Sesudah: setiap method hanya satu level indentasi
function printItems(items: string[]): void {
for (const item of items) {
printIfNotEmpty(item);
}
}
function printIfNotEmpty(item: string): void {
if (item.length === 0) return;
console.log(item);
}
Typescript
// Sebelum
function getDiscount(isMember: boolean): number {
if (isMember) {
return 0.1;
} else {
return 0;
}
}
Typescript
// Sesudah: early return, tanpa else
function getDiscount(isMember: boolean): number {
if (isMember) return 0.1;
return 0;
}
Typescript
// Sebelum: email cuma string biasa, validasi bisa tercecer di mana-mana
function registerUser(email: string): void {
if (!email.includes("@")) throw new Error("Email tidak valid");
// ...
}
Typescript
// Sesudah: Email jadi value object dengan aturan validasinya sendiri
class Email {
private readonly value: string;
constructor(value: string) {
if (!value.includes("@")) throw new Error("Email tidak valid");
this.value = value;
}
toString(): string {
return this.value;
}
}
function registerUser(email: Email): void {
// tidak perlu validasi ulang, Email sudah menjamin dirinya valid
}
Typescript
// Sebelum: array of Order dipakai bebas di banyak tempat
class Customer {
orders: Order[] = [];
}
Typescript
// Sesudah: OrderList membungkus logic-nya sendiri
class OrderList {
private items: Order[] = [];
add(order: Order): void {
this.items.push(order);
}
totalValue(): number {
return this.items.reduce((sum, order) => sum + order.total, 0);
}
}
class Customer {
orders = new OrderList();
}
Typescript
// Sebelum: rantai panjang, tahu terlalu dalam soal struktur internal
const city = customer.getAddress().getCity().getName();
Typescript
// Sesudah: delegasikan, customer cukup tahu caranya sendiri
class Customer {
cityName(): string {
return this.address.cityName();
}
}
const city = customer.cityName();
Typescript
// Sebelum
function calcTtlPrc(itms: Item[]): number { ... }
Typescript
// Sesudah
function calculateTotalPrice(items: Item[]): number { ... }
Typescript
// Sebelum: lima instance variable dalam satu class
class Order {
id: string;
customerName: string;
customerEmail: string;
items: Item[];
shippingAddress: string;
}
Typescript
// Sesudah: dipecah menjadi beberapa class kecil yang saling berkolaborasi
class Customer {
constructor(
private name: string,
private email: string
) {}
}
class Order {
constructor(
private customer: Customer,
private items: OrderList
) {}
}
Typescript
// Sebelum: ambil data lewat getter, logic diproses di luar
class Account {
private balance: number;
getBalance(): number {
return this.balance;
}
}
if (account.getBalance() < amount) {
throw new Error("Saldo tidak cukup");
}
account.setBalance(account.getBalance() - amount);
Typescript
// Sesudah: Account yang bertanggung jawab atas aturan saldonya sendiri
class Account {
private balance: number;
withdraw(amount: number): void {
if (this.balance < amount) {
throw new Error("Saldo tidak cukup");
}
this.balance -= amount;
}
}