Warisan dan Komposisi (Hubungan Is-a vs Has-a) di Java

1. Ikhtisar

Pewarisan dan komposisi - bersama dengan abstraksi, enkapsulasi, dan polimorfisme - adalah landasan pemrograman berorientasi objek (OOP).

Dalam tutorial ini, kita akan membahas dasar-dasar pewarisan dan komposisi, dan kita akan sangat fokus untuk menemukan perbedaan antara kedua jenis hubungan tersebut.

2. Dasar-dasar Warisan

Warisan adalah mekanisme yang kuat namun terlalu sering digunakan dan disalahgunakan.

Sederhananya, dengan pewarisan, kelas dasar (alias tipe dasar) mendefinisikan keadaan dan perilaku umum untuk tipe tertentu dan memungkinkan subkelas (alias subtipe) menyediakan versi khusus dari status dan perilaku itu.

Untuk mendapatkan gambaran yang jelas tentang cara bekerja dengan pewarisan, mari buat contoh naif: Kelas dasar Person yang mendefinisikan bidang dan metode umum untuk seseorang, sementara subkelas Pelayan dan Aktris menyediakan implementasi metode tambahan yang terperinci.

Inilah kelas Person :

public class Person { private final String name; // other fields, standard constructors, getters }

Dan ini adalah subkelasnya:

public class Waitress extends Person { public String serveStarter(String starter) { return "Serving a " + starter; } // additional methods/constructors } 
public class Actress extends Person { public String readScript(String movie) { return "Reading the script of " + movie; } // additional methods/constructors }

Selain itu, mari buat pengujian unit untuk memverifikasi bahwa instance dari kelas Pelayan dan Aktris juga merupakan instance dari Orang , sehingga menunjukkan bahwa kondisi "is-a" terpenuhi di tingkat tipe:

@Test public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Waitress("Mary", "[email protected]", 22)) .isInstanceOf(Person.class); } @Test public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Actress("Susan", "[email protected]", 30)) .isInstanceOf(Person.class); }

Penting untuk menekankan di sini segi semantik dari pewarisan . Selain menggunakan kembali implementasi kelas Person , kita telah membuat hubungan "is-a" yang terdefinisi dengan baik antara tipe dasar Person dan subtipe Waitress dan Actress . Pelayan dan aktris, efektif, adalah orang.

Hal ini dapat menyebabkan kita bertanya: dalam kasus penggunaan apa warisan merupakan pendekatan yang tepat?

Jika subtipe memenuhi kondisi "is-a" dan terutama menyediakan fungsionalitas aditif lebih jauh di bawah hierarki kelas, maka pewarisan adalah cara yang harus dilakukan.

Tentu saja, penggantian metode diperbolehkan selama metode yang diganti mempertahankan jenis dasar / subtype substitusi yang dipromosikan oleh Prinsip Substitusi Liskov.

Selain itu, kita harus ingat bahwa subtipe mewarisi API tipe dasar , yang dalam beberapa kasus mungkin berlebihan atau hanya tidak diinginkan.

Jika tidak, kita sebaiknya menggunakan komposisi.

3. Warisan dalam Pola Desain

Meskipun konsensusnya adalah bahwa kita harus lebih menyukai komposisi daripada warisan bila memungkinkan, ada beberapa kasus penggunaan tipikal di mana warisan memiliki tempatnya.

3.1. Pola Layer Supertype

Dalam kasus ini, kami menggunakan pewarisan untuk memindahkan kode umum ke kelas dasar (supertipe), pada basis per lapisan .

Berikut implementasi dasar pola ini di lapisan domain:

public class Entity { protected long id; // setters } 
public class User extends Entity { // additional fields and methods } 

Kita dapat menerapkan pendekatan yang sama ke lapisan lain dalam sistem, seperti lapisan layanan dan persistensi.

3.2. Pola Metode Template

Dalam pola metode template, kita dapat menggunakan kelas dasar untuk menentukan bagian invarian dari suatu algoritme, dan kemudian mengimplementasikan bagian varian dalam subkelas :

public abstract class ComputerBuilder { public final Computer buildComputer() { addProcessor(); addMemory(); } public abstract void addProcessor(); public abstract void addMemory(); } 
public class StandardComputerBuilder extends ComputerBuilder { @Override public void addProcessor() { // method implementation } @Override public void addMemory() { // method implementation } }

4. Dasar-dasar Komposisi

Komposisi adalah mekanisme lain yang disediakan oleh OOP untuk menggunakan kembali implementasi.

Singkatnya, komposisi memungkinkan kita untuk memodelkan objek yang terdiri dari objek lain , sehingga mendefinisikan hubungan "has-a" di antara objek tersebut.

Lebih lanjut, komposisi adalah bentuk asosiasi terkuat , artinya objek yang menyusun atau terkandung dalam satu objek akan ikut hancur juga saat objek tersebut dimusnahkan .

Untuk lebih memahami cara kerja komposisi, anggaplah kita perlu bekerja dengan objek yang mewakili komputer .

Komputer terdiri dari bagian-bagian yang berbeda, termasuk mikroprosesor, memori, kartu suara, dan sebagainya, sehingga kita dapat memodelkan komputer dan setiap bagiannya sebagai kelas individual.

Berikut tampilan implementasi sederhana dari kelas Komputer :

public class Computer { private Processor processor; private Memory memory; private SoundCard soundCard; // standard getters/setters/constructors public Optional getSoundCard() { return Optional.ofNullable(soundCard); } }

Kelas-kelas berikut memodelkan mikroprosesor, memori, dan kartu suara (antarmuka dihilangkan demi singkatnya):

public class StandardProcessor implements Processor { private String model; // standard getters/setters }
public class StandardMemory implements Memory { private String brand; private String size; // standard constructors, getters, toString } 
public class StandardSoundCard implements SoundCard { private String brand; // standard constructors, getters, toString } 

Sangat mudah untuk memahami motivasi di balik mendorong komposisi daripada warisan. Dalam setiap skenario di mana dimungkinkan untuk membangun hubungan "has-a" yang benar secara semantik antara kelas tertentu dan lainnya, komposisi adalah pilihan yang tepat untuk dibuat.

Dalam contoh di atas, Komputer memenuhi kondisi "has-a" dengan kelas yang memodelkan bagian-bagiannya.

Perlu juga dicatat bahwa dalam kasus ini, objek Komputer yang memuat memiliki kepemilikan objek yang terkandung jika dan hanya jika objek tersebut tidak dapat digunakan kembali dalam objek Komputer lain . Jika mereka bisa, kami akan menggunakan agregasi, bukan komposisi, di mana kepemilikan tidak tersirat.

5. Komposisi Tanpa Abstraksi

Alternatifnya, kita bisa mendefinisikan hubungan komposisi dengan melakukan hard-coding dependensi kelas Komputer , daripada mendeklarasikannya di konstruktor:

public class Computer { private StandardProcessor processor = new StandardProcessor("Intel I3"); private StandardMemory memory = new StandardMemory("Kingston", "1TB"); // additional fields / methods }

Tentu saja, ini akan menjadi desain yang kaku dan erat, karena kami akan membuat Komputer sangat bergantung pada implementasi spesifik dari Prosesor dan Memori .

Kami tidak akan memanfaatkan tingkat abstraksi yang disediakan oleh antarmuka dan injeksi ketergantungan.

Dengan desain awal berdasarkan antarmuka, kami mendapatkan desain yang digabungkan secara longgar, yang juga lebih mudah untuk diuji.

6. Kesimpulan

Dalam artikel ini, kami mempelajari dasar-dasar pewarisan dan komposisi di Java, dan kami menjelajahi secara mendalam perbedaan antara dua jenis hubungan ("is-a" vs. "has-a").

Seperti biasa, semua contoh kode yang ditampilkan dalam tutorial ini tersedia di GitHub.