Object Orientation Abusers
Smell → Martin Fowler Code Smells → OO-Abusers
Semua smell di dalam grup ini berkaitan dengan penerapan prinsip OOP yang kurang tepat, baik karena kurangnya pengetahuan mengenai OOP maupun pemikiran programmer yang terlalu procedural pada OOP.
Switch Statements
sourcemaking | refactoring.guru | before | after
Penjelasan Smell
Apa gunanya mati-matian ngoding switch-case kalo ujung-ujungnya dianggap jorok? Kan aku kan udah menciptakan inovasi istimewa biar programmer tinggal oper-operan ke method lain lalu kenapa enggan dengan diri anda?
Dalam kasus smell tersebut terdapat pemakaian switch atau if-else untuk menentukan alur operasi.
Apalagi hal ini memang dianggap pro-kontra terutama jika anda ingin melakukan tindakan antar object ataupun sekedar oper-operan method. Namun biasanya penggunaan switch-case ini diharamkan jika anda melakukan hal ini untuk deklarasi atau kasus yang melibatkan inheritance.
Tidak semua switch atau if-else itu berbahaya. Perlu dipertimbangkan apakah akan terjadi violasi terhadap OCP (Open Closed Principle).
Lihat class ShapePrinter.java dan CharNeededCounter.java.
if(shape.equalsIgnoreCase("square")){
...
} else if(shape.equalsIgnoreCase("triangle")){
...
} else {
...
}
Kedua class tersebut akan melanggar OCP. Bayangkan bila ada tipe Shape
baru yang perlu dibuat, tentu saja akan bertambah lagi if
di masing-masing ShapePrinter dan CharNeededCounter.
Misal bertambah logic shape Circle
. Violasi OCP terjadi di 2 class tersebut.
if(shape.equalsIgnoreCase("square")){
...
} else if(shape.equalsIgnoreCase("triangle")){
...
} else if(shape.equalsIgnoreCase("circle")){
...
} else {
...
}
Di dalam contoh ini, if-else square dan triangle ada di 2 class. Pada kondisi nyata bila hal ini dibiarkan terjadi, if-else square dan triangle akan terus beranak-pinak bila ada kebutuhan logic lain.
Semakin sedikit kita menggunakan if-else di dalam code, maka akan semakin baik. Bahkan ada orang yang membuat campaign untuk ini: Anti-IF Campaign.
Penyelesaian
Untuk contoh kasus ini, kita melakukan tahapan berikut:
Kita memiliki dua type code. square
dan triangle
. Oleh karena itu, kita buat class Shape.java sebagai abstract class yang memiliki fungsi charNeeded
dan print
, lalu Triangle.java dan Square.java meng-extends class Shape
.
Setelah class Square
dan Triangle
sudah terbentuk. Logic print dari ShapePrinter
dan logic menghitung karakter dari CharNeededCounter
dipindahkan ke masing-masing class.
Dengan begini, bila ada jenis baru, misalkan Circle
, kita tinggal extends dari class Shape
.
Tambahan
Revisi Martin Fowler
Switch Statements adalah code smell yang dibuat Fowler di buku edisi pertamanya. Di buku edisi kedua, beliau meniadakan code smell ini. Beliau membuat smell baru bernama Repeated Switches. Beliau ingin lebih menekankan if-else yang perlu diberantas adalah if-else yang berstruktur sama/mirip dan sering muncul di beberapa tempat. Contohnya di contoh kasus ini if-else square dan triangle muncul dua kali.
ShapeFactory
User tetap akan meng-input string melalui console. Oleh karena itu, kita perlu menyiapkan sebuah class Factory untuk membuat class Shape dari string yang diinput.
Harusnya Anda menyadari bahwa terjadi violasi OCP disini. Bila class Circle
dibuat, maka if di Factory bertambah. Hal ini dimaklumi karena OCP hanya dilanggar satu kali saja di dalam Factory (tidak akan dilanggar lagi di tempat lain) dan memang terpaksa dilakukan karena input dari user adalah string. Ibaratkan Factory disini berperan sebagai anti-corruption layer.
Di beberapa bahasa pemrograman, ada cara spesifik untuk mengakali menghilangkan if-else pada class Factory, contohnya untuk Java: https://www.javacodegeeks.com/2014/10/factory-without-if-else.html.
Temporary Field
sourcemaking | refactoring.guru | before | after
Penjelasan Smell
Kalo ngoding di C (atau bahasa Procedural lainnya) kan kadang perlu buat variabel global scope, lalu kalo misalnya aku coba terapin ginian di OOP kenapa dibilang jorok? Ya kan ngoding di OOP dan Procedural beda mas! OOP ngoding per-satuan class dan object, kalo procedural ngoding per kit/procedure atau hal-hal yang diperlukan/diproses disitu.
Perlu diingatkan ngoding di Object-oriented Programming tidaklah sama dengan Procedural Programming! Dalam kasus ini terdapat field yang bukan bagian dari data class yang bersangkutan. Field ini hanya dipakai sementara oleh beberapa fungsi sehingga ketika fungsi tersebut selesai dijalankan, field ini tidak pernah lagi digunakan.
SOLID Principle yang perlu dicek untuk smell ini adalah SRP (Single-responsibility principle). Kita perlu mempertimbangkan apakah field yang bersangkutan adalah field yang cocok menempati class tersebut.
Lihat class BojekDriver.java
private int f;
private int g;
private int h;
Vector<Location> shortestPath(){
Vector<Location> paths = new Vector<>();
//...
//complex A* algorithm code. using f, g, h variable
//...
return paths;
}
Disini dimisalkan ada driver yang perlu melakukan pencarian rute terdekat. Salah satu algoritma yang dapat digunakan untuk mencari rute adalah [A](https://en.wikipedia.org/wiki/A_search_algorithm). Seperti yang sudah Anda pelajari di matakuliah AI semester lalu, A* membutuhkan beberapa hal untuk beroperasi, misalnya adalah variabel f, g, h.
Di dalam class ini, bayangkan ketiga variabel itu akan digunakan di fungsi shortestPath
dan juga di fungsi-fungsi private lainnya yang adalah hasil extract fungsi shortestPath
(diekstrak agar tidak menimbulkan code smell Long Method).
BojekDriver seharusnya hanya memperdulikan data miliknya. Variabel f, g, dan h hanyalah temporary field untuk keperluan algoritma A*.
Penyelesaian
Untuk contoh kasus ini, kita melakukan Extract Class
Semua fungsi berkaitan dengan A* dan semua variabelnya kita usir ke class baru AStar.java.
Di class BojekDriver, fungsi shortestPath
tetap ada, namun didelegasi ke class AStar.
Vector<Location> shortestPath(){
return new AStar().shortestPath(current, destination);
}
Refused Bequest
sourcemaking | refactoring.guru | contoh 1 (ISP) | contoh 2 (LSP)
Penjelasan Smell
Aku pernah diminta untuk implement suatu abstract class namun saya rada-rada benci sama fitur yang enggan class ini pakai makanya saya
return null
aja atau lempar exception berupaUnsupportedOperationException
karena dianggap useless bagi class itu. Lah kalo misalnya code saya dianggap jorok, lalu diapakan itu class yang saya implement dari abstract class?
Refused bequest arti harafiahnya adalah “menolak warisan”. Dalam smell ini, sebuah class turunan tidak memakai seluruh method hasil extendsnya. Hal ini mengarah ke violasi LSP dan/atau ISP.
Refused Bequest (Interface Segregation Violation)
Dalam contoh kasus pertama, terdapat class Stack.java yang melakukan extends terhadap java.util.Vector
.
Di dalam class Stack, terdapat fungsi standar sebuah stack LIFO yaitu: pop, push, dan peek.
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);
}
Namun, terdapat satu masalah. java.util.Vector
memiliki banyak fungsi yang memungkinkan class melakukan manipulasi data di dalam array (misalnya bisa hapus data menggunakan indeks). Tentu saja ini melanggar prinsip LIFO (Last-In First-Out).
Oleh karena itu, di class Stack, diakali dengan cara melakukan override pada masing-masing fungsi java.util.Vector
yang tidak ingin digunakan, dan kita menghilangkan kinerjanya dengan cara menghapus pemanggilan super
.
Sebelumnya seperti ini:
@Override
public synchronized E remove(int index) {
return super.remove(index);
}
Return super kita ubah menjadi return null. Sehingga remove by index tidak terjadi.
/*
* you cannot remove by index, use pop instead
*/
@Override
public synchronized E remove(int index) {
return null;
}
Hal ini menjadi code smell Refused Bequest, karena class Stack menolak warisan dari class Vector. Hal ini tentunya melanggar prinsip Interface Segregation Principle yang mengharuskan pembagian method dalam abstrak terbagi rata sesuai kebutuhan subclassnya dan tidak boleh ditolak oleh subclass tersebut.
Penyelesaian
Untuk contoh kasus ini, kita melakukan Replace Inheritance with Delegation.
Hubungan is-a
tidak cocok untuk Stack dan Vector. Kita ubah hubungannya menjadi hubungan has-a
.
class Stack menyimpan java.util.Vector
sebagai field-nya. Pop, push, dan peek dilakukan dengan Vector ini.
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);
}
}
Refused Bequest (Liskov Substitution Violation)
Pada kasus pelanggaran LSP, disebutkan bahwa terdapat method turunan yang mendelegasikan ke method turunan lainnya.
Perhatikan contoh Square.java di package before
.
@Override
public void setWidth(float width) {
this.setHeight(width);
}
@Override
public void setHeight(float height) {
this.height = this.width = height;
}
Karena class Square memiliki invariant width dan height harus sama, maka class Square pun melakukan overriding seperti diatas agar widt hdan height selalu disamakan ketika di-set.
Dalam kasus ini, penolakan warisan terjadi saat Square melakukan override pada fungsi setter parent-nya.
Masalah terjadi ketika code ini dijalankan:
@Test
void test() {
foo(new Rectangle());
foo(new Square());
}
void foo(Rectangle r) {
r.setHeight(10);
r.setWidth(20);
assertEquals(200, r.area());
}
Fungsi foo memiliki parameter Rectangle. Tentunya, fungsi ini berekspektasi bahwa parameter Rectangle berperilaku sebagaimananya persegi panjang. Bila height = 10, width = 20, maka area 200.
Sayangnya, unit test tersebut akan failed. Karena ketika foo(new Square());
, terjadi:
expected: <200.00> but was: <400.0>
Fungsi foo mendapatkan area Square adalah 400 karena ketika width di-set 20, height pun turut diubah menjadi 20.
Penyelesaian
Rectangle dan Square (dan Triangle) memiliki struktur yang serupa, dilakukan extract class, menjadi abstract class Shape2D. Kemudian semua class lain menjadi subclass dari abstract class ini. Silakan cek code di package after
.
Tambahan
A. java.util.Stack
Java sudah memiliki class Stack-nya sendiri di package java.util
. Class Stack disini dibuat sendiri hanya untuk keperluan studi kasus. FYI, java.util.Stack
adalah hasil inherit dari java.util.Vector
. Anda dapat menghapus data di tengah-tengah Stack layaknya menggunakan Vector.
B. Square-Rectangle Problem
Kasus ini merupakan contoh umum untuk menjelaskan violasi Liskov Substitution Principle (LSP). Kasus ini dikenal dengan square-rectangle problem.
Wajar bila Anda pernah melakukan violasi LSP ketika baru belajar konsep OOP di semester lalu. Square dan Rectangle terkesan memiliki hubungan is-a, namun ternyata tidak boleh karena dalam kasus ini, Square hanya meminjam sebagian behavior dari Rectangle (fungsi area). Sedangkan behavior yang lain, malah ditimpa.
Alternative Classes with Different Interfaces
sourcemaking | refactoring.guru | before | after
Penjelasan Smell
Menebak smell dengan class yang sama namun beda interface atau abstract bisa jadi menandakan bahwa class tersebut diciptakan dengan kemiripan namun seharusnya bisa diciptakan inheritance. Entah developernya lupa kalo bisa dibuatin abstract/interface atau dia jangan-jangan copas lalu ganti nama methodnya?
Ada beberapa class yang memiliki fungsi yang sama, namun tidak datang dari interface atau abstract class.
Kesamaan fungsi yang dimaksud adalah fungsi memiliki tujuan yang sama. Namun bisa saja memiliki nama yang berbeda, atau bisa juga memiliki parameter yang serupa namun beda urutan, atau ada parameter yang satuan, ada yang berupa objek (Preserve whole object).
Hal ini bisa terjadi bisa karena class-class tersebut dikerjakan oleh programmer yang berbeda. Sehingga antar programmer tidak sadar ada yang bisa dibuatkan interface diantara code mereka berdua.
Atau bisa karena tidak mengikuti prinsip DIP (Dependency Inversion Principle).
Contohnya, class Ghost.java memiliki fungsi paint
yang bertujuan menggambar Ghost dari spritesheet yang tersedia.
public class Ghost {
public void paint(Graphics2D g){
//draw Ghost pixel from spritesheet
}
}
Di sisi lain, class PacMan.java memiliki fungsi draw
yang bertujuan sama. Menggambar PacMan dari spritesheet yang tersedia.
public class PacMan {
public void draw(Graphics2D g){
//draw PacMan pixel from spritesheet
}
}
Penyelesaian
Untuk contoh kasus ini, kita melakukan Extract Superclass. Kita membuat interface Drawable.java yang akan di-implement oleh kedua class.
public interface Drawable {
public void draw(Graphics2D g);
}
Pada class PacMan sebelumnya, nama fungsi adalah draw
, sedangkan pada Ghost nama fungsi adalah paint
. Dipilih salah satu dari kedua sinonim tersebut yang akan dipakai. Misal diputuskan draw
yang dipakai sebagai nama method di interface Drawable
, maka dilakukan Rename Method pada fungsi paint
di Ghost.
Referensi Gambar
Semua gambar referensi mengikuti pictorial gambar pada Refactoring.guru dengan tetap mengutamakan link credit pada sourcemaking.com maupun refactoring.guru