Setiap kali saya melihat metode di mana perilaku mengaktifkan jenis parameternya, saya segera mempertimbangkan terlebih dahulu apakah metode itu benar-benar milik pada parameter metode. Misalnya, alih-alih memiliki metode seperti:
public void sort(List values) {
if (values instanceof LinkedList) {
// do efficient linked list sort
} else { // ArrayList
// do efficient array list sort
}
}
Saya akan melakukan ini:
values.sort();
// ...
class ArrayList {
public void sort() {
// do efficient array list sort
}
}
class LinkedList {
public void sort() {
// do efficient linked list sort
}
}
Kami memindahkan perilaku ke tempat yang tahu kapan harus menggunakannya. Kami membuat abstraksi nyata di mana Anda tidak perlu tahu jenis atau detail implementasi. Untuk situasi Anda, mungkin lebih masuk akal untuk memindahkan metode ini dari kelas asli (yang akan saya panggil O
) untuk mengetik A
dan menimpanya dalam jenis B
. Jika metode ini dipanggil doIt
pada beberapa objek, pindah doIt
ke A
dan timpa dengan perilaku yang berbeda di B
. Jika ada bit data dari tempat doIt
awalnya disebut, atau jika metode ini digunakan di tempat yang cukup, Anda dapat meninggalkan metode asli dan mendelegasikan:
class O {
int x;
int y;
public void doIt(A a) {
a.doIt(this.x, this.y);
}
}
Kita bisa menyelam lebih dalam lagi. Mari kita lihat saran untuk menggunakan parameter boolean dan melihat apa yang bisa kita pelajari tentang cara rekan kerja Anda berpikir. Usulannya adalah melakukan:
public void doIt(A a, boolean isTypeB) {
if (isTypeB) {
// do B stuff
} else {
// do A stuff
}
}
Ini terlihat sangat mengerikan seperti yang instanceof
saya gunakan pada contoh pertama saya, kecuali bahwa kita mengeksternalkan cek itu. Ini berarti bahwa kita harus menyebutnya dengan salah satu dari dua cara:
o.doIt(a, a instanceof B);
atau:
o.doIt(a, true); //or false
Pertama-tama, titik panggilan tidak tahu jenisnya A
. Karena itu, haruskah kita melewati boolean sepanjang jalan? Apakah itu benar-benar pola yang kita inginkan di seluruh basis kode? Apa yang terjadi jika ada tipe ketiga yang perlu kita pertanggungjawabkan? Jika ini adalah bagaimana metode ini dipanggil, kita harus memindahkannya ke tipe dan membiarkan sistem memilih implementasi untuk kita secara polimorfis.
Dengan cara kedua, kita harus sudah tahu jenis a
di titik panggilan. Biasanya itu berarti kita membuat instance di sana, atau mengambil instance dari tipe itu sebagai parameter. Membuat metode O
yang dibutuhkan di B
sini akan berhasil. Kompiler akan tahu metode mana yang harus dipilih. Ketika kita mengemudi melalui perubahan seperti ini, duplikasi lebih baik daripada menciptakan abstraksi yang salah , setidaknya sampai kita mengetahui ke mana kita benar-benar pergi. Tentu saja, saya menyarankan agar kita tidak benar-benar melakukan apa pun yang telah kita ubah ke titik ini.
Kita perlu melihat lebih dekat hubungan antara A
dan B
. Secara umum, kita diberitahu bahwa kita harus memilih komposisi daripada warisan . Ini tidak benar dalam setiap kasus, tetapi itu benar dalam jumlah kasus yang mengejutkan begitu kita menggali. B
Warisan dari A
, artinya kita percaya B
adalah A
. B
harus digunakan seperti A
, kecuali bahwa itu bekerja sedikit berbeda. Tetapi apa perbedaan itu? Bisakah kita memberi perbedaan nama yang lebih konkret? Bukankah B
ini sebuah A
, tetapi benar-benar A
memiliki X
yang bisa A'
atau B'
? Akan seperti apa kode kita jika kita melakukan itu?
Jika kami memindahkan metode ke A
seperti yang disarankan sebelumnya, kami dapat menyuntikkan instance X
ke A
, dan mendelegasikan metode itu ke X
:
class A {
X x;
A(X x) {
this.x = x;
}
public void doIt(int x, int y) {
x.doIt(x, y);
}
}
Kita dapat menerapkan A'
dan B'
, serta menyingkirkan B
. Kami telah memperbaiki kode dengan memberikan nama pada konsep yang mungkin lebih implisit, dan memungkinkan kami untuk mengatur perilaku itu saat runtime alih-alih waktu kompilasi. A
sebenarnya menjadi kurang abstrak juga. Alih-alih hubungan warisan yang diperpanjang, itu memanggil metode pada objek yang didelegasikan. Objek itu abstrak, tetapi lebih fokus hanya pada perbedaan implementasi.
Ada satu hal terakhir yang harus dilihat. Mari kembali ke proposal rekan kerja Anda. Jika pada semua situs panggilan kita secara eksplisit mengetahui jenis yang A
kita miliki, maka kita harus membuat panggilan seperti:
B b = new B();
o.doIt(b, true);
Kami berasumsi sebelumnya ketika menulis yang A
memiliki salah X
satu A'
atau B'
. Tetapi mungkin bahkan anggapan ini tidak benar. Apakah ini satu-satunya tempat di mana perbedaan antara A
dan B
hal-hal? Jika ya, maka mungkin kita bisa mengambil pendekatan yang sedikit berbeda. Kami masih memiliki X
yang salah A'
atau B'
, tetapi bukan milik A
. Hanya O.doIt
peduli tentang itu, jadi mari kita serahkan saja ke O.doIt
:
class O {
int x;
int y;
public void doIt(A a, X x) {
x.doIt(a, x, y);
}
}
Sekarang situs panggilan kami terlihat seperti:
A a = new A();
o.doIt(a, new B'());
Sekali lagi, B
menghilang, dan abstraksi bergerak ke yang lebih fokus X
. Namun, kali A
ini bahkan lebih sederhana dengan mengetahui lebih sedikit. Itu bahkan kurang abstrak.
Penting untuk mengurangi duplikasi dalam basis kode, tetapi kita harus mempertimbangkan mengapa duplikasi terjadi di tempat pertama. Duplikasi bisa menjadi tanda abstraksi yang lebih dalam yang berusaha keluar.