Warisan vs properti tambahan dengan nilai nol

12

Untuk kelas dengan bidang opsional, apakah lebih baik menggunakan warisan atau properti nullable? Pertimbangkan contoh ini:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

atau

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

atau

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

Kode yang tergantung pada 3 opsi ini adalah:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

atau

if (book.getColor() != null) { book.getColor(); }

atau

if (book.getColor().isPresent()) { book.getColor(); }

Pendekatan pertama terlihat lebih alami bagi saya, tetapi mungkin itu kurang mudah dibaca karena perlunya melakukan casting. Apakah ada cara lain untuk mencapai perilaku ini?

Bojan VukasovicTest
sumber
1
Saya khawatir solusinya turun ke definisi Anda tentang aturan bisnis. Maksud saya, jika semua buku Anda memiliki warna tetapi warnanya opsional, maka pewarisan berlebihan dan sebenarnya tidak perlu, karena Anda ingin semua buku Anda mengetahui properti warna ada. Di sisi lain, jika Anda dapat memiliki kedua buku yang tidak tahu apa pun tentang warna dan buku-buku yang memiliki warna, membuat kelas khusus tidaklah buruk. Bahkan kemudian, saya mungkin akan memilih komposisi daripada warisan.
Andy
Untuk membuatnya sedikit lebih spesifik - ada 2 jenis buku - satu dengan warna dan satu tanpa. Jadi tidak semua buku memiliki warna.
Bojan VukasovicTest
1
Jika benar-benar ada kasus, di mana buku tidak memiliki warna sama sekali, dalam kasus Anda warisan adalah cara paling sederhana bagaimana memperluas sifat-sifat buku berwarna-warni. Dalam hal ini sepertinya Anda tidak akan melanggar apa pun dengan mewarisi kelas buku dasar dan menambahkan properti warna ke dalamnya. Anda dapat membaca tentang prinsip substitusi liskov untuk mempelajari lebih lanjut tentang kasus ketika memperluas kelas diperbolehkan dan ketika tidak.
Andy
4
Bisakah Anda mengidentifikasi perilaku yang Anda inginkan dari buku-buku dengan pewarnaan? Dapatkah Anda menemukan perilaku umum di antara buku-buku dengan dan tanpa pewarnaan, di mana buku-buku dengan pewarnaan harus berperilaku sedikit berbeda? Jika demikian, ini adalah kasus yang baik untuk OOP dengan berbagai jenis, dan, idenya adalah untuk memindahkan perilaku ke dalam kelas alih-alih menginterogasi keberadaan dan nilai properti secara eksternal.
Erik Eidt
1
Jika warna buku yang mungkin diketahui sebelumnya, bagaimana dengan bidang Enum untuk BookColor dengan opsi untuk BookColor.NoColor?
Graham

Jawaban:

7

Itu tergantung keadaan. Contoh spesifik tidak realistis karena Anda tidak akan memiliki subclass yang dipanggil BookWithColordalam program nyata.

Tetapi secara umum, properti yang hanya masuk akal untuk subclass tertentu seharusnya hanya ada di subclass tersebut.

Sebagai contoh jika Bookmemiliki PhysicalBookdan DigialBooksebagai keturunan, maka PhysicalBookmungkin memiliki weightproperti, dan DigitalBooksebuah sizeInKbproperti. Tetapi DigitalBooktidak akan memiliki weightdan sebaliknya. Booktidak akan memiliki properti, karena sebuah kelas seharusnya hanya memiliki properti yang dibagikan oleh semua keturunan.

Contoh yang lebih baik adalah melihat kelas dari perangkat lunak nyata. The JSliderkomponen memiliki lapangan majorTickSpacing. Karena hanya slider yang memiliki "ticks", bidang ini hanya masuk akal untuk JSliderdan turunannya. Akan sangat membingungkan jika komponen saudara lain seperti JButtonpunya majorTickSpacinglapangan.

JacquesB
sumber
atau Anda bisa mengurangi berat untuk bisa memantulkan keduanya, tapi itu mungkin terlalu banyak.
Walfrat
3
@ Walfrat: Ini akan menjadi ide yang sangat buruk untuk memiliki bidang yang sama mewakili hal-hal yang sangat berbeda di subclass yang berbeda.
JacquesB
Bagaimana jika sebuah buku memiliki edisi fisik dan digital? Anda harus membuat kelas yang berbeda untuk setiap permutasi memiliki atau tidak memiliki setiap bidang opsional. Saya pikir solusi terbaik hanya akan membiarkan beberapa bidang dihilangkan atau tidak tergantung pada buku. Anda mengutip situasi yang sangat berbeda di mana kedua kelas akan berperilaku berbeda secara fungsional. Bukan itu maksud pertanyaan ini. Mereka hanya perlu memodelkan struktur data.
4castle
@ 4castle: Anda perlu kelas terpisah per edisi, karena setiap edisi mungkin memiliki harga yang berbeda. Anda tidak dapat menyelesaikan ini dengan bidang opsional. Tapi kita tidak tahu dari contoh jika op ingin perilaku berbeda untuk buku dengan warna atau "buku tidak berwarna", jadi tidak mungkin untuk mengetahui apa pemodelan yang benar dalam kasus ini.
JacquesB
3
setuju dengan @ JacquesB. Anda tidak dapat memberikan solusi yang benar secara universal untuk "memodelkan buku", karena itu tergantung pada masalah apa yang Anda coba selesaikan dan apa bisnis Anda. Saya pikir itu cukup aman untuk mengatakan bahwa mendefinisikan hierarki kelas di mana Anda melanggar liskov sudah pasti salah (tm) (yeah yeah, kasing Anda mungkin sangat istimewa dan menjaminnya (walaupun saya ragu))
sara
5

Poin penting yang tampaknya tidak disebutkan: Dalam sebagian besar bahasa, instance kelas tidak dapat mengubah kelas mana yang merupakan instance. Jadi jika Anda memiliki buku tanpa warna, dan Anda ingin menambahkan warna, Anda harus membuat objek baru jika Anda menggunakan kelas yang berbeda. Dan kemudian Anda mungkin perlu mengganti semua referensi ke objek lama dengan referensi ke objek baru.

Jika "buku tanpa warna" dan "buku dengan warna" adalah instance dari kelas yang sama, maka menambahkan warna atau menghapus warna akan jauh lebih sedikit dari masalah. (Jika antarmuka pengguna Anda menampilkan daftar "buku dengan warna" dan "buku tanpa warna" maka antarmuka pengguna harus berubah dengan jelas, tapi saya berharap itu adalah sesuatu yang perlu Anda tangani, mirip dengan "daftar merah buku "dan" daftar buku hijau ").

gnasher729
sumber
Ini benar-benar poin penting! Ini mendefinisikan apakah seseorang harus mempertimbangkan koleksi properti opsional, bukan atribut kelas.
chromanoid
1

Pikirkan JavaBean (seperti Anda Book) sebagai catatan dalam database. Kolom opsional adalah nullketika mereka tidak memiliki nilai, tetapi sangat legal. Karena itu, opsi kedua Anda:

class Book {
    private String name;
    private String color; // null when not applicable
}

Apakah yang paling masuk akal. 1

Berhati-hatilah dengan bagaimana Anda menggunakan Optionalkelas. Misalnya, bukan Serializable, yang biasanya merupakan karakteristik JavaBean. Berikut beberapa tips dari Stephen Colebourne :

  1. Jangan mendeklarasikan variabel instan tipe apa pun Optional.
  2. Gunakan nulluntuk menunjukkan data opsional dalam lingkup pribadi kelas.
  3. Gunakan Optionaluntuk getter yang mengakses bidang opsional.
  4. Jangan gunakan Optionalpada setter atau konstruktor.
  5. Gunakan Optionalsebagai tipe pengembalian untuk metode logika bisnis lain yang memiliki hasil opsional.

Oleh karena itu, dalam kelas Anda Anda harus menggunakan nulluntuk mewakili bahwa lapangan tidak ada, tapi ketika color daun yang Book(sebagai jenis kembali) itu harus dibungkus dengan Optional.

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Ini memberikan desain yang jelas dan kode yang lebih mudah dibaca.


1 Jika Anda bermaksud untuk BookWithColormerangkum seluruh "kelas" buku yang memiliki kemampuan khusus di atas buku-buku lain, maka masuk akal untuk menggunakan warisan.

4castle
sumber
1
Siapa yang mengatakan sesuatu tentang database? Jika semua pola OO harus sesuai dengan paradigma basis data relasional, kita harus mengubah banyak pola OO.
alexanderbird
Saya berpendapat menambahkan menambahkan properti yang tidak digunakan "berjaga-jaga" adalah pilihan yang paling jelas. Seperti orang lain katakan, itu tergantung pada use case tetapi situasi yang paling diinginkan adalah tidak membawa properti di mana aplikasi eksternal perlu tahu apakah properti yang ada benar-benar sedang digunakan.
Volker Kueffel
@Volker Mari kita setuju untuk tidak setuju, karena saya dapat mengutip beberapa orang yang berperan dalam menambahkan Optionalkelas ke Jawa untuk tujuan yang tepat ini. Mungkin bukan OO, tapi itu tentu pendapat yang valid.
4castle
@Alexanderbird Saya secara khusus menangani JavaBean, yang harus serializeable. Jika saya keluar topik dengan membawa JavaBeans, maka itu adalah kesalahan saya.
4castle
@Alexanderbird Saya tidak berharap semua pola cocok dengan database relasional, tapi saya berharap Java Bean untuk
4castle
0

Anda harus menggunakan antarmuka (bukan warisan) atau semacam mekanik properti (bahkan mungkin sesuatu yang berfungsi seperti kerangka deskripsi sumber daya).

Begitu juga

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

atau

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Klarifikasi (edit):

Cukup menambahkan bidang opsional di kelas entitas

Terutama dengan Opsional-wrapper (edit: lihat jawaban 4castle ) Saya pikir ini (menambahkan bidang dalam entitas asli) adalah cara yang layak untuk menambahkan properti baru dalam skala kecil. Masalah terbesar dengan pendekatan ini adalah bahwa itu mungkin bekerja melawan kopling rendah.

Bayangkan kelas buku Anda didefinisikan dalam proyek khusus untuk model domain Anda. Sekarang Anda menambahkan proyek lain yang menggunakan model domain untuk melakukan tugas khusus. Tugas ini membutuhkan properti tambahan di kelas buku. Entah Anda berakhir dengan warisan (lihat di bawah) atau Anda harus mengubah model domain umum untuk memungkinkan tugas baru. Dalam kasus terakhir Anda mungkin berakhir dengan banyak proyek yang semuanya tergantung pada sifat mereka sendiri ditambahkan ke kelas buku sedangkan kelas buku itu sendiri dengan cara tergantung pada proyek-proyek ini, karena Anda tidak dapat memahami kelas buku tanpa proyek tambahan.

Mengapa warisan bermasalah ketika datang ke cara untuk memberikan properti tambahan?

Ketika saya melihat contoh kelas Anda "Buku", saya berpikir tentang objek domain yang seringkali memiliki banyak bidang dan subtipe opsional. Bayangkan saja Anda ingin menambahkan properti untuk buku yang menyertakan CD. Sekarang ada empat jenis buku: buku, buku dengan warna, buku dengan CD, buku dengan warna dan CD. Anda tidak dapat menggambarkan situasi ini dengan warisan di Jawa.

Dengan antarmuka, Anda menghindari masalah ini. Anda dapat dengan mudah menyusun properti dari kelas buku tertentu dengan antarmuka. Delegasi dan komposisi akan membuatnya mudah untuk mendapatkan kelas yang Anda inginkan. Dengan warisan Anda biasanya berakhir dengan beberapa properti opsional di kelas yang hanya ada karena kelas saudara membutuhkannya.

Bacaan lebih lanjut mengapa pewarisan seringkali merupakan ide bermasalah:

Mengapa warisan umumnya dipandang sebagai hal yang buruk oleh para pendukung OOP

JavaWorld: Mengapa meluas itu jahat

Masalah dengan mengimplementasikan seperangkat antarmuka untuk menyusun seperangkat properti

Ketika Anda menggunakan antarmuka untuk ekstensi semuanya baik-baik saja selama Anda hanya memiliki satu set kecil dari mereka. Terutama ketika model objek Anda digunakan dan diperluas oleh pengembang lain, misalnya di perusahaan Anda, jumlah antarmuka akan bertambah. Dan akhirnya Anda akhirnya membuat "antarmuka properti" resmi baru yang menambahkan metode yang sudah digunakan rekan kerja Anda dalam proyek mereka untuk pelanggan X untuk kasus penggunaan yang sama sekali tidak terkait - Ugh.

sunting: Aspek penting lainnya disebutkan oleh gnasher729 . Anda sering ingin secara dinamis menambahkan properti opsional ke objek yang ada. Dengan pewarisan atau antarmuka Anda harus membuat ulang seluruh objek dengan kelas lain yang jauh dari opsional.

Ketika Anda mengharapkan ekstensi ke model objek Anda sedemikian rupa, Anda akan lebih baik dengan secara eksplisit memodelkan kemungkinan ekstensi dinamis . Saya mengusulkan sesuatu seperti di atas di mana setiap "ekstensi" (dalam hal ini properti) memiliki kelasnya sendiri. Kelas berfungsi sebagai namespace dan pengidentifikasi untuk ekstensi. Ketika Anda mengatur konvensi penamaan paket sesuai dengan cara ini memungkinkan jumlah tak terbatas ekstensi tanpa mencemari namespace untuk metode di kelas entitas asli.

Dalam pengembangan game, Anda sering menghadapi situasi di mana Anda ingin membuat perilaku dan data dalam banyak variasi. Inilah sebabnya mengapa arsitektur pola entitas-komponen-sistem menjadi cukup populer di kalangan pengembangan game. Ini juga merupakan pendekatan menarik yang mungkin ingin Anda lihat, ketika Anda mengharapkan banyak ekstensi untuk model objek Anda.

kromanoid
sumber
Dan itu belum menjawab pertanyaan "bagaimana memutuskan antara opsi-opsi ini", itu hanyalah pilihan lain. Mengapa ini selalu lebih baik daripada semua opsi yang disajikan OP?
alexanderbird
Anda benar, saya akan memperbaiki jawabannya.
chromanoid
@ 4castle Di antarmuka Java bukan warisan! Menerapkan antarmuka adalah cara untuk mematuhi kontrak sementara mewarisi kelas pada dasarnya memperluas perilakunya. Anda dapat mengimplementasikan beberapa antarmuka sementara Anda tidak dapat memperluas beberapa kelas dalam satu kelas. Dalam contoh saya, Anda memperluas dengan antarmuka saat Anda menggunakan warisan untuk mewarisi perilaku entitas asli.
chromanoid
Masuk akal. Dalam contoh Anda, PropertiesHoldermungkin kelas abstrak. Tapi apa yang Anda sarankan untuk implementasi getPropertyatau getPropertyValue? Data masih harus disimpan dalam semacam bidang contoh di suatu tempat. Atau apakah Anda menggunakan a Collection<Property>untuk menyimpan properti alih-alih menggunakan bidang?
4castle
1
Saya membuat Bookperluasan PropertiesHolderuntuk alasan kejelasan. The PropertiesHolderbiasanya harus menjadi anggota di semua kelas yang membutuhkan fungsi ini. Implementasinya akan mengadakan Map<Class<? extends Property<?>>,Property<?>>atau sesuatu seperti ini. Ini adalah bagian buruk dari implementasi tetapi memberikan kerangka kerja yang sangat bagus yang bahkan bekerja dengan JAXB dan JPA.
chromanoid
-1

Jika Bookkelas dapat diturunkan tanpa properti warna seperti di bawah ini

class E_Book extends Book{
   private String type;
}

maka pewarisan itu masuk akal.

adem caglin
sumber
Saya tidak yakin saya mengerti teladan Anda? Jika colorbersifat pribadi, maka setiap kelas turunan seharusnya tidak perlu khawatir color. Cukup tambahkan beberapa konstruktor ke Bookdiabaikan itu colorsama sekali dan itu akan default untuk null.
4castle