![Pilih Composition atau Inheritance?](https://images.ctfassets.net/7wjlcr9d5ii8/2IdmXbnPO4uHld5qNaCJ9L/cd4dcbc4af0ab6f8aa9f3a48ee4961a9/inheritance-the-myth-of-code-reuse-andrei-raifura-codeway-2015-82-638.jpg?w=638&h=479&fl=progressive&q=100&fm=jpg&fit=thumb&f=faces)
Materi tentang composition atau inheritance biasanya sudah diajarkan sejak kuliah. Tapi seringkali kita hanya sekedar tahu tapi masih bingung tentang best practice-nya gimana. Artikel tentang "dahulukan composition dibandingkan inheritance" sudah sering didiskusikan seperti pada buku-buku tentang design patterns atau tulisan-tulisan dari Martin Fowler, Joshua Bloch atau Robert C Martin. Composition adalah mekanisme dimana sebuah objek menggunakan objek lain dan bergantung dengan objek tersebut (HAS-A relationship). Sedangkan inheritance adalah mekanisme dimana sebuah objek menurunkan behavior-nya ke objek lainnya (IS-A relationship). Salah satu tujuan dari kedua mekanisme adalah untuk mencegah duplikasi code atau spaghetti code agar code yang common bisa digunakan tanpa copy paste secara manual. Masing-masing mekanisme punya kelebihan dan kelemahan masing-masing.
Sekarang misalkan kita punya kasus ketika kita ingin membuat sebuah base dari sebuah objek, dan method dari objek tersebut bisa digunakan di beberapa objek lainnya. Contohnya kita punya dua use case, BookOrderCreatorUseCase dan BookOrderDetailUseCase yang dependent dengan BaseBookOrder.
Class BaseBookOrder
public class BaseBookOrder{
public void validate(){
//code to validate
}
public String getAuthorNameById(int id){
//code to get authors
return "";
}
}
Class BookOrderCreatorUseCase
public class BookOrderCreatorUseCase extends BaseBookOrder{
public void createBookOrder(Book book){
validate();
String authorNameById = getAuthorNameById(book.getAuthorId());
//code to create book order
}
}
Class BookOrderDetailUseCase
public class BookOrderDetailUseCase extends BaseBookOrder{
public void getBookOrderDetail(Book book){
validate();
//code to get book detail
String authorNameById = getAuthorNameById(book.getAuthorId());
}
}
Code-nya sederhana aja ga usah terlalu detail ya, biar ga terlalu panjang tulisannya, fokus ke contoh kasusnya aja😅. Pada kasus di atas, BookOrderCreatorUseCase dan BookOrderDetailUseCase sama-sama inheritance dari BaseBookOrder sehingga method-method yang ada pada BaseBookOrder diturunkan ke masing-masing use case. Tentu ini bisa menjadi solusi dari spaghetti code karena code-nya jadi reusable ke masing-masing use case. Tapi solusi seperti ini punya beberapa kelemahan.
Misalkan suatu saat salah satu use case membutuhkan behavior yang berbeda pada method validate() atau getAuthorNameById(). Biasanya jika ini terjadi maka kita akan bikin method baru dengan nama berbeda. Tentu itu akan menambah kerumitan code pada class BaseBookOrder. Belum lagi jika ternyata turunannya ga hanya BookOrderCreatorUseCase dan BookOrderDetailUseCase, dan pada turunan tertentu ada method lain yang tidak digunakan pada turunan lainnya. Bakal lebih spaghetti lagi code-nya. Selain itu java juga tidak bisa extend lebih dari satu class. Ke-tidak-fleksibel-an ini tentu jadi challenge suatu saat nanti saat terjadi perubahan.
Ketika membuat unit test, method dari BaseBookOrder susah dipalsukan (Mocking/Spy) karena dia merupakan parent dari class tersebut. Setahu gw sih memang ga bisa di-Mock menggunakan framework seperti Mockito atau JUnit. Dari segi teknis ini memang tidak menguntungkan jika ingin membangun kualitas dari sebuah code.
Ini sebenarnya relatif sih, tapi berdasarkan pengalaman gw pribadi selain sulit dimodifikasi, code seperti ini juga sulit di-maintain. Class-nya seringkali menjadi gendut dan bertindak seperti "god object". Bahkan terkadang ga semua behavior dari BaseBookOrder digunakan oleh turunannya, bisa jadi nantinya ada salah satu use case yang tidak membutuhkan behavior getAuthorNameById(). Tapi karena inheritance, semuanya otomatis diwariskan ke turunannya. Melanggar Solid Principle pastinya.
Solusi dari kasus di atas adalah memperlakukan BaseBookOrder sebagai Composition. Plus, BaseBookOrder dijadikan abstract biar gampang dimodifikasi. Jadi setiap perubahan tidak mengganggu kelas lainnya, tinggal bikin subtype dari BaseBookOrder.
Interface BaseBookOrder
public interface BaseBookOrder{
void validate();
String getAuthorNameById(int id);
}
Class BookOrderCreatorUseCase
public class BookOrderCreatorUseCase{
private final BaseBookOrder baseBookOrder;
public BookOrderCreatorUseCase(BaseBookOrder baseBookOrder){
this.baseBookOrder = baseBookOrder;
}
public void createBookOrder(Book book){
baseBookOrder.validate();
String authorNameById = baseBookOrder.getAuthorNameById(book.getAuthorId());
//code to create book order
}
}
Class BookOrderDetailUseCase
public class BookOrderDetailUseCase{
private final BaseBookOrder baseBookOrder;
public BookOrderDetailUseCase(BaseBookOrder baseBookOrder){
this.baseBookOrder = baseBookOrder;
}
public void getBookOrderDetail(Book book){
baseBookOrder.validate();
String authorNameById = baseBookOrder.getAuthorNameById(book.getAuthorId());
//code to get book detail
}
}
Class BaseBookOrderUseCase
public class BaseBookOrderUseCase implements BaseBookOrder{
public void validate(){
//code to validate
}
public String getAuthorNameById(int id){
//code to get authors
return "";
}
}
Dengan struktur seperti di atas, masalah-masalah sebelumnya dapat teratasi. Code jadi lebih fleksibel, tiap ada perubahan behavior pada BaseBookOrder tinggal bikin class baru yang mengimplementasi abstract BaseBookOrder. Perubahan ini bisa dilakukan tanpa mengganggu code lainnya, karena class-class tersebut dependent ke abstract BaseBookOrder, bukan pada concrete object. Selain itu karena dependency-nya di-inject, object-nya bisa di-mock atau di-spy menggunakan Mockito saat melakukan unit test. Masing-masing class dapat menentukan code mana aja yang mau digunakan berdasarakan reference object dari dependency tersebut. Berbeda dengan inheritance yang secara otomatis semua code-nya diturunkan dari super class. Dan tentunya code seperti di atas menghormati Solid Principle.
Gw setuju dengan apa yang para expert katakan, "favor composition over inheritance". Pertanyaan selanjutnya adalah "kapan menggunakan inheritance dan kapan menggunakan composition?". By default dahulukan pendekatan composition. Kecuali ada beberapa alasan khusus. Apakah class tersebut mempunya kriteria yang sama dengan parent-nya. Apakah class tersebut berhak mewarisi semua behavior dari parent-nya karena inheritance berarti semua yang ada di parent diturunkan ke turunannya. Jika ternyata turunan-turunan tersebut hanya membutuhkan sebagian behavior tertentu dari parent-nya, berarti gunakan composition. Pendekatannya kurang lebih mirip dengan Liskov Substitution Principle. Pikir-pikir dulu, jangan sampai pada turunan tertentu ada behavior yang tidak compatible dengan parent class.