Pertimbangkan desain berikut
public class Person
{
public virtual string Name { get; }
public Person (string name)
{
this.Name = name;
}
}
public class Karl : Person
{
public override string Name
{
get
{
return "Karl";
}
}
}
public class John : Person
{
public override string Name
{
get
{
return "John";
}
}
}
Apakah Anda pikir ada sesuatu yang salah di sini? Bagi saya kelas Karl dan John seharusnya hanya instance daripada kelas karena mereka persis sama dengan:
Person karl = new Person("Karl");
Person john = new Person("John");
Mengapa saya membuat kelas baru ketika instance sudah cukup? Kelas-kelas tidak menambahkan apa pun ke instance.
programming-practices
inheritance
class-design
anti-patterns
Ignacio Soler Garcia
sumber
sumber
Jawaban:
Tidak perlu memiliki subclass khusus untuk setiap orang.
Anda benar, itu harus menjadi contoh.
Sasaran subclass: untuk memperluas kelas induk
Subkelas digunakan untuk memperluas fungsionalitas yang disediakan oleh kelas induk. Misalnya, Anda mungkin memiliki:
Kelas induk
Battery
yang dapatPower()
sesuatu dan dapat memilikiVoltage
properti,Dan subclass
RechargeableBattery
, yang bisa mewarisiPower()
danVoltage
, tetapi juga bisaRecharge()
d.Perhatikan bahwa Anda bisa melewatkan instance
RechargeableBattery
kelas sebagai parameter ke metode apa pun yang menerimaBattery
argumen. Ini disebut prinsip substitusi Liskov , salah satu dari lima prinsip SOLID. Demikian pula, dalam kehidupan nyata, jika pemutar MP3 saya menerima dua baterai AA, saya dapat menggantinya dengan dua baterai AA yang dapat diisi ulang.Perhatikan bahwa kadang-kadang sulit untuk menentukan apakah Anda perlu menggunakan bidang atau subkelas untuk mewakili perbedaan antara sesuatu. Misalnya, jika Anda harus menangani baterai AA, AAA dan 9-Volt, apakah Anda akan membuat tiga subclass atau menggunakan enum? “Ganti Subclass dengan Fields” di Refactoring oleh Martin Fowler, halaman 232, dapat memberi Anda beberapa ide dan cara berpindah dari satu ke yang lain.
Dalam contoh Anda,
Karl
danJohn
jangan memperluas apa pun, mereka juga tidak memberikan nilai tambahan: Anda dapat memiliki fungsionalitas yang persis sama menggunakanPerson
kelas secara langsung. Memiliki lebih banyak baris kode tanpa nilai tambahan tidak pernah baik.Contoh kasus bisnis
Apa yang mungkin menjadi kasus bisnis di mana sebenarnya masuk akal untuk membuat subkelas untuk orang tertentu?
Katakanlah kita membangun aplikasi yang mengelola orang yang bekerja di perusahaan. Aplikasi ini mengelola izin juga, sehingga Helen, akuntan, tidak dapat mengakses repositori SVN, tetapi Thomas dan Mary, dua programmer, tidak dapat mengakses dokumen terkait akuntansi.
Jimmy, bos besar (pendiri dan CEO perusahaan) memiliki hak istimewa yang sangat spesifik yang tidak dimiliki siapa pun. Dia dapat, misalnya, mematikan seluruh sistem, atau memecat seseorang. Anda mendapatkan idenya.
Model termiskin untuk aplikasi tersebut adalah memiliki kelas seperti:
karena duplikasi kode akan muncul dengan sangat cepat. Bahkan dalam contoh dasar empat karyawan, Anda akan menduplikasi kode antara kelas Thomas dan Mary. Ini akan mendorong Anda untuk membuat kelas induk biasa
Programmer
. Karena Anda mungkin memiliki beberapa akuntan juga, Anda mungkin akan membuatAccountant
kelas juga.Sekarang, Anda perhatikan bahwa memiliki kelas
Helen
tidak terlalu berguna, juga menjagaThomas
danMary
: sebagian besar kode Anda bekerja di tingkat atas — di tingkat akuntan, programmer, dan Jimmy. Server SVN tidak peduli apakah Thomas atau Mary yang perlu mengakses log — ia hanya perlu tahu apakah itu programmer atau akuntan.Jadi Anda akhirnya menghapus kelas yang tidak Anda gunakan:
"Tapi aku bisa menjaga Jimmy apa adanya, karena selalu ada hanya satu CEO, satu bos besar — Jimmy", begitu. Selain itu, Jimmy banyak digunakan dalam kode Anda, yang sebenarnya lebih mirip ini, dan tidak seperti pada contoh sebelumnya:
Masalah dengan pendekatan itu adalah bahwa Jimmy masih bisa ditabrak bus, dan akan ada CEO baru. Atau dewan direksi dapat memutuskan bahwa Mary sangat hebat sehingga ia harus menjadi CEO baru, dan Jimmy akan diturunkan jabatannya menjadi tenaga penjual, jadi sekarang, Anda perlu memeriksa semua kode Anda dan mengubah segalanya.
sumber
Adalah konyol menggunakan struktur kelas semacam ini hanya untuk memvariasikan detail yang jelas-jelas merupakan bidang yang dapat diselesaikan pada instance. Tapi itu khusus untuk contoh Anda.
Membuat berbagai
Person
kelas yang berbeda hampir pasti merupakan ide yang buruk - Anda harus mengubah program Anda dan mengkompilasi ulang setiap kali Orang baru memasuki domain Anda. Namun, itu tidak berarti mewarisi dari kelas yang sudah ada sehingga string yang berbeda dikembalikan ke suatu tempat terkadang tidak berguna.Perbedaannya adalah bagaimana Anda mengharapkan entitas yang diwakili oleh kelas itu bervariasi. Orang-orang hampir selalu dianggap datang dan pergi, dan hampir setiap aplikasi serius yang berurusan dengan pengguna diharapkan dapat menambah dan menghapus pengguna pada saat run-time. Tetapi jika Anda memodelkan hal-hal seperti algoritma enkripsi yang berbeda, mendukung yang baru mungkin merupakan perubahan besar, dan masuk akal untuk menciptakan kelas baru yang
myName()
metodenya mengembalikan string yang berbeda (dan yangperform()
metodenya mungkin melakukan sesuatu yang berbeda).sumber
Dalam kebanyakan kasus, Anda tidak akan melakukannya. Contoh Anda benar-benar kasus bagus di mana perilaku ini tidak menambah nilai nyata.
Ini juga melanggar prinsip Terbuka Tertutup , karena subclass pada dasarnya bukan ekstensi tetapi memodifikasi cara kerja orang tua. Selain itu, konstruktor publik dari induk sekarang menjengkelkan dalam subkelas dan dengan demikian API menjadi kurang dipahami.
Namun, kadang-kadang, jika Anda hanya akan memiliki satu atau dua konfigurasi khusus yang sering digunakan di seluruh kode, kadang-kadang lebih nyaman dan memakan waktu lebih sedikit untuk hanya subkelas induk yang memiliki konstruktor kompleks. Dalam kasus khusus seperti itu, saya tidak dapat melihat ada yang salah dengan pendekatan seperti itu. Sebut saja konstruktor currying j / k
sumber
Jika ini sejauh praktik, maka saya setuju ini praktik buruk.
Jika John dan Karl memiliki perilaku yang berbeda, maka segalanya berubah sedikit. Bisa jadi itu
person
memiliki metode untuk dicleanRoom(Room room)
mana ternyata John adalah teman sekamar yang hebat dan membersihkan sangat efektif, tetapi Karl tidak dan tidak terlalu banyak membersihkan kamar.Dalam hal ini, masuk akal untuk menjadikan mereka subkelas mereka sendiri dengan perilaku yang ditentukan. Ada cara yang lebih baik untuk mencapai hal yang sama (misalnya,
CleaningBehavior
kelas), tetapi setidaknya cara ini bukanlah pelanggaran prinsip OO yang mengerikan.sumber
Dalam beberapa kasus Anda tahu hanya akan ada sejumlah contoh dan kemudian akan baik-baik saja (meskipun menurut saya jelek) untuk menggunakan pendekatan ini. Lebih baik menggunakan enum jika bahasa memungkinkan.
Contoh jawa:
sumber
Banyak orang sudah menjawab. Kupikir aku akan memberikan perspektif pribadiku sendiri.
Sekali waktu saya mengerjakan sebuah aplikasi (dan masih melakukannya) yang menciptakan musik.
Aplikasi ini memiliki abstrak
Scale
kelas dengan beberapa subclass:CMajor
,DMinor
, dllScale
tampak sesuatu seperti:Generator musik bekerja dengan
Scale
instance spesifik untuk menghasilkan musik. Pengguna akan memilih skala dari daftar, untuk menghasilkan musik.Suatu hari, sebuah ide keren muncul di benak saya: mengapa tidak mengizinkan pengguna untuk membuat skala sendiri? Pengguna akan memilih catatan dari daftar, tekan tombol, dan skala baru akan ditambahkan ke daftar skala yang tersedia.
Tetapi saya tidak dapat melakukan ini. Itu karena semua skala sudah ditetapkan pada waktu kompilasi - karena mereka dinyatakan sebagai kelas. Lalu aku tersadar:
Seringkali intuitif untuk berpikir dalam istilah 'superclasses and subclasses'. Hampir semuanya dapat diekspresikan melalui sistem ini: superclass
Person
dan subclassJohn
danMary
; superclassCar
dan subclassVolvo
danMazda
; superclassMissile
dan subclassSpeedRocked
,LandMine
danTrippleExplodingThingy
.Sangat wajar untuk berpikir seperti ini, terutama bagi orang yang relatif baru mengenal OO.
Tetapi kita harus selalu ingat bahwa kelas adalah template , dan objek adalah konten yang dituangkan ke dalam template ini . Anda dapat menuangkan konten apa pun yang Anda inginkan ke dalam templat, menciptakan kemungkinan yang tak terhitung jumlahnya.
Bukan tugas subclass untuk mengisi templat. Ini pekerjaan dari objek. Tugas subclass adalah menambahkan fungsionalitas aktual , atau memperluas templat .
Dan itu sebabnya saya seharusnya membuat
Scale
kelas yang konkret , denganNote[]
bidang, dan membiarkan objek mengisi template ini ; mungkin melalui konstruktor atau sesuatu. Dan akhirnya, jadi saya melakukannya.Setiap kali Anda mendesain templat di kelas (misalnya, anggota kosong
Note[]
yang perlu diisi, atauString name
bidang yang perlu diberi nilai), ingat bahwa tugas dari objek kelas ini untuk mengisi templat ( tugas atau mungkin mereka yang membuat objek ini). Subkelas dimaksudkan untuk menambah fungsionalitas, bukan untuk mengisi templat.Anda mungkin tergoda untuk membuat semacam "superclass
Person
, subclass,John
danMary
" sistem, seperti yang Anda lakukan, karena Anda menyukai formalitas yang Anda dapatkan.Dengan cara ini, Anda bisa mengatakan
Person p = new Mary()
, alih-alihPerson p = new Person("Mary", 57, Sex.FEMALE)
. Itu membuat segalanya lebih teratur, dan lebih terstruktur. Tapi seperti yang kami katakan, membuat kelas baru untuk setiap kombinasi data bukanlah pendekatan yang baik, karena membengkokkan kode untuk apa-apa dan membatasi Anda dalam hal kemampuan runtime.Jadi inilah solusinya: gunakan pabrik dasar, bahkan mungkin yang statis. Seperti itu:
Dengan cara ini, Anda dapat dengan mudah menggunakan 'preset' yang 'datang dengan program', seperti:,
Person mary = PersonFactory.createMary()
tetapi Anda juga berhak untuk merancang orang baru secara dinamis, misalnya dalam hal Anda ingin mengizinkan pengguna untuk melakukannya . Misalnya:Atau bahkan lebih baik: lakukan sesuatu seperti itu:
Saya terbawa suasana. Saya pikir Anda mendapatkan idenya.
Subclass tidak dimaksudkan untuk mengisi template yang ditetapkan oleh superclasses mereka. Subkelas dimaksudkan untuk menambah fungsionalitas . Objek dimaksudkan untuk mengisi templat, itulah gunanya.
Anda seharusnya tidak membuat kelas baru untuk setiap kemungkinan kombinasi data. (Sama seperti saya seharusnya tidak membuat
Scale
subclass baru untuk setiap kombinasiNote
s yang mungkin).Ini adalah pedoman: setiap kali Anda membuat subkelas baru, pertimbangkan apakah itu menambahkan fungsionalitas baru yang tidak ada di superclass. Jika jawaban untuk pertanyaan itu adalah "tidak", daripada Anda mungkin mencoba untuk 'mengisi template' dari superclass, dalam hal ini buat saja sebuah objek. (Dan mungkin Pabrik dengan 'preset', untuk membuat hidup lebih mudah).
Semoga itu bisa membantu.
sumber
Ini adalah pengecualian untuk 'harus menjadi contoh' di sini - meskipun sedikit ekstrim.
Jika Anda ingin kompiler untuk menegakkan izin untuk mengakses metode berdasarkan apakah itu Karl atau John (dalam contoh ini) daripada meminta implementasi metode untuk memeriksa instance mana yang telah berlalu maka Anda mungkin menggunakan kelas yang berbeda. Dengan menerapkan kelas yang berbeda daripada instantiasi maka Anda membuat pemeriksaan diferensiasi antara 'Karl' dan 'John' dimungkinkan pada waktu kompilasi daripada waktu berjalan dan ini mungkin berguna - khususnya dalam konteks keamanan di mana kompiler mungkin lebih dipercaya daripada waktu berjalan. pemeriksaan kode perpustakaan (misalnya) eksternal.
sumber
Ini dianggap praktik buruk untuk memiliki kode duplikat .
Ini setidaknya sebagian karena garis kode dan karena itu ukuran basis kode berkorelasi dengan biaya pemeliharaan dan cacat .
Inilah logika akal sehat yang relevan bagi saya tentang topik khusus ini:
1) Jika saya perlu mengubah ini, berapa banyak tempat yang perlu saya kunjungi? Kurang lebih baik di sini.
2) Apakah ada alasan mengapa dilakukan seperti ini? Dalam contoh Anda - tidak mungkin ada - tetapi dalam beberapa contoh mungkin ada semacam alasan untuk melakukan ini. Satu yang bisa saya pikirkan dari atas kepala saya adalah dalam beberapa kerangka kerja seperti awal Grails di mana warisan tidak selalu bermain dengan baik dengan beberapa plugin untuk GORM. Jarang sekali.
3) Apakah lebih bersih dan mudah dipahami dengan cara ini? Sekali lagi, ini tidak ada dalam contoh Anda - tetapi ada beberapa pola pewarisan yang mungkin lebih merupakan peregangan di mana kelas yang terpisah sebenarnya lebih mudah untuk dipertahankan (penggunaan pola perintah atau strategi strategi tertentu muncul dalam pikiran).
sumber
Ada use case: membentuk referensi ke fungsi atau metode sebagai objek biasa.
Anda dapat melihat ini dalam kode Java GUI:
Kompiler membantu Anda di sini dengan membuat subkelas anonim baru.
sumber