Object-Oriented Abuser
Semua jenis Object-Oriented Abusers, penjelasan, contoh kode nyata, dan cara refactoringnya
Code smell ini terjadi karena penerapan prinsip-prinsip OOP yang salah, tidak lengkap, atau dipaksakan.
Sumber: Refactoring Guru
Switch Statements
Kamu punya blok switch yang kompleks atau rangkaian if-else panjang untuk menangani berbagai tipe/varian.
Masalah
Setiap kali ada tipe baru, kamu harus mengedit semua class yang punya if-else tersebut. Ini melanggar Open/Closed Principle — code harusnya terbuka untuk ekstensi, tertutup untuk modifikasi.
Contoh nyata: ShapePrinter dan CharNeededCounter punya if-else identik untuk tipe shape. Tambah shape Circle? Harus edit dua class sekaligus.
package oo_abusers.switch_statements.before;
public class ShapePrinter {
public void print(String shape, int size){
if(shape.equalsIgnoreCase("square")){
for(int i = 0; i < size; i++){
for(int j = 0; j < size; j++){
System.out.print("*");
}
System.out.println("");
}
} else if(shape.equalsIgnoreCase("triangle")){
for(int i = 1; i <= size; i++){
for(int j = 0; j < i; j++){
System.out.print("*");
}
System.out.println("");
}
} else {
System.out.println("invalid shape");
}
}
}
Solusi
Ubah setiap tipe shape menjadi class yang mewarisi abstract class Shape. Logika print() dan charNeeded() dipindahkan ke masing-masing class.
// if-else yang sama ada di ShapePrinter DAN CharNeededCounter!
public class ShapePrinter {
public void print(String shape, int size) {
if (shape.equalsIgnoreCase("square")) {
// logika cetak kotak...
} else if (shape.equalsIgnoreCase("triangle")) {
// logika cetak segitiga...
}
}
}
public class CharNeededCounter {
public int count(String shape, int size) {
if (shape.equalsIgnoreCase("square")) return size * size;
else if (shape.equalsIgnoreCase("triangle")) return ((size+1)*size)/2;
return -1;
}
}
// Tiap shape urus diri sendiri
public abstract class Shape {
protected int size;
public abstract void print();
public abstract int charNeeded();
}
public class Square extends Shape {
@Override public void print() { /* logika kotak */ }
@Override public int charNeeded() { return size * size; }
}
// Tambah Circle? Cukup buat class baru — tidak ada class lain yang diubah!
public class Circle extends Shape {
@Override public void print() { /* logika lingkaran */ }
@Override public int charNeeded() { /* rumus lingkaran */ }
}
Perbandingan
| Fitur | Sebelum | Sesudah |
|---|---|---|
| Keterbacaan | Blok if-else besar menyembunyikan tujuan | Setiap class punya satu tanggung jawab |
| Pengujian | Harus test semua branch dalam satu method | Setiap shape ditest secara mandiri |
| Pemeliharaan | Tambah shape baru = edit banyak file | Buat class baru, tidak ada yang diubah |
| Reusabilitas | Logika terkurung dalam method | Objek shape bisa dioper ke mana saja |
Temporary Field
Field di dalam class hanya diisi nilainya pada kondisi tertentu — "numpang lewat" saja.
Masalah
Field distance, fuelCost, weightFee di CodeSmell_ShippingOrder hanya dipakai saat kalkulasi. Di method lain, field ini tidak dipakai sama sekali — tapi tetap ada dan membingungkan pembaca.
package oo_abusers.temporary_field.before;
public class CodeSmell_ShippingOrder {
private String destination;
private double basePrice;
// --- TEMPORARY FIELDS ---
// Variabel ini cuma dipakai buat hitung-hitungan sementara
private double distance;
private double fuelCost;
private double weightFee;
public CodeSmell_ShippingOrder(String destination, double basePrice) {
this.destination = destination;
this.basePrice = basePrice;
}
public double calculateTotal(double weight, double km) {
// Mengisi temporary fields
distance = km;
fuelCost = distance * 2000;
weightFee = weight * 5000;
return basePrice + fuelCost + weightFee;
}
// Bayangkan ada 10 method lain yang tidak peduli soal fuelCost atau weightFee
public String getReceipt() {
return "Order to: " + destination;
}
}
Solusi
Pindahkan temporary fields ke class baru yang memang bertanggung jawab untuk kalkulasi tersebut.
public class CodeSmell_ShippingOrder {
private String destination;
private double basePrice;
// Field ini hanya hidup selama calculateTotal()!
private double distance; // temporary
private double fuelCost; // temporary
private double weightFee; // temporary
public double calculateTotal(double weight, double km) {
distance = km;
fuelCost = distance * 2000;
weightFee = weight * 5000;
return basePrice + fuelCost + weightFee;
}
}
public class CodeSmell_ShippingOrder {
private String destination;
private double basePrice;
// Tidak ada temporary fields di sini!
public double calculateTotal(double weight, double km) {
CalculateShippingOrder calc = new CalculateShippingOrder(weight, km);
return basePrice + calc.getTotalExtraCost();
}
}
public class CalculateShippingOrder {
public double distance;
public double fuelCost;
public double weightFee;
public CalculateShippingOrder(double weight, double km) {
this.distance = km;
this.fuelCost = distance * 2000;
this.weightFee = weight * 5000;
}
public double getTotalExtraCost() { return fuelCost + weightFee; }
}
Perbandingan
| Fitur | Sebelum | Sesudah |
|---|---|---|
| Keterbacaan | Field membingungkan karena hanya aktif kondisi tertentu | Class hanya berisi data yang relevan permanen |
| Pengujian | Harus track side-effect antar field | Objek kalkulasi bisa ditest terisolasi |
| Pemeliharaan | Tidak jelas kapan field aman untuk dimodifikasi | State sementara hilang setelah digunakan |
| Reusabilitas | Class utama terikat pada logika kalkulasi spesifik | Class kalkulasi bisa dipakai ulang di konteks lain |
Refused Bequest
Subclass hanya memakai sebagian kecil warisan dari parent — sisanya diabaikan atau di-override dengan null.
Masalah
Ini melanggar Liskov Substitution Principle: subclass harus bisa menggantikan parent tanpa merusak program. Stack extends Vector artinya user bisa panggil remove(int), set(), insertElementAt() — operasi yang tidak masuk akal untuk Stack!
package oo_abusers.refused_bequest.before;
import java.util.Vector;
public class Stack<E> extends Vector<E> {
public void push(E data) {
this.add(data);
}
public void pop() {
this.removeElementAt(this.size()-1);
}
public E peek() {
return this.elementAt(this.size()-1);
}
// Problem: tidak boleh remove by index, harus pakai pop()
// Tapi tetap bisa dipanggil dari luar karena extends Vector!
@Override
public synchronized E remove(int index) {
return null; // menolak warisan dengan diam-diam
}
}
Solusi
Ganti inheritance dengan komposisi. Stack punya Vector, bukan adalah Vector.
// Stack ADALAH Vector — user bisa akses semua method Vector
public class Stack<E> extends Vector<E> {
public void push(E data) { this.add(data); }
public void pop() { this.removeElementAt(this.size()-1); }
public E peek() { return this.elementAt(this.size()-1); }
@Override
public synchronized E remove(int index) {
return null; // menolak warisan dengan diam-diam
}
// Tapi stk.set(), stk.insertElementAt(), stk.remove(0) masih bisa dipanggil!
}
// Stack PUNYA Vector — hanya method Stack yang terekspos
public class Stack<E> {
private Vector<E> vector = new Vector<>();
public void push(E data) { vector.add(data); }
public void pop() { vector.removeElementAt(vector.size()-1); }
public E peek() { return vector.elementAt(vector.size()-1); }
// Hanya 3 method ini yang ada. Tidak ada kejutan.
}
Perbandingan
| Fitur | Sebelum | Sesudah |
|---|---|---|
| Keterbacaan | Tidak jelas method mana yang "aman" dipakai | Interface class mencerminkan kemampuan nyata |
| Pengujian | Perilaku tak terduga bisa muncul dari method parent | Setiap method bermakna, tidak ada yang tersembunyi |
| Pemeliharaan | Override yang return null menyembunyikan bug | Tidak ada warisan yang ditolak |
| Reusabilitas | Subclass tidak aman dipakai sebagai parent | Komposisi membuat class lebih fleksibel |
Alternative Classes with Different Interfaces
Dua class melakukan hal yang sama, tapi nama method-nya berbeda-beda.
Masalah
PacMan punya method draw(), sedangkan Ghost punya method paint() — padahal keduanya menggambar sprite ke layar. Code yang menggunakan keduanya terpaksa punya dua cabang logika hanya karena perbedaan nama.
package oo_abusers.alt_classes_with_dif_interfaces.before;
import java.awt.Graphics2D;
public class Ghost {
public void paint(Graphics2D g){ // <- nama berbeda!
//draw Ghost pixel from spritesheet
}
}
Solusi
Buat interface Drawable dengan method draw(). Semua class yang bisa digambar implementasi interface ini.
public class PacMan {
public void draw(Graphics2D g) { /* gambar PacMan */ }
}
public class Ghost {
public void paint(Graphics2D g) { /* gambar Ghost */ } // nama beda!
}
// Code pemanggil terpaksa handle dua cara berbeda:
// if (entity instanceof PacMan) pacman.draw(g)
// if (entity instanceof Ghost) ghost.paint(g)
public interface Drawable {
void draw(Graphics2D g); // kontrak tunggal
}
public class PacMan implements Drawable {
@Override public void draw(Graphics2D g) { /* gambar PacMan */ }
}
public class Ghost implements Drawable {
@Override public void draw(Graphics2D g) { /* gambar Ghost */ }
}
// Code pemanggil cukup:
// for (Drawable entity : entities) entity.draw(g);
Perbandingan
| Fitur | Sebelum | Sesudah |
|---|---|---|
| Keterbacaan | Developer harus hafal nama method tiap class | Satu interface membuat niat langsung jelas |
| Pengujian | Harus tulis test terpisah karena tidak punya tipe sama | Satu test suite untuk semua Drawable |
| Pemeliharaan | Tambah entitas baru = code pemanggil harus tahu nama method-nya | Cukup implementasi interface |
| Reusabilitas | Logika render terikat pada class spesifik | Semua drawable bisa diperlakukan seragam |