Saya sedang berdiskusi dengan rekan kerja, dan kami akhirnya memiliki intuisi yang bertentangan tentang tujuan subklasifikasi. Intuisi saya adalah bahwa jika fungsi utama dari sebuah subclass adalah untuk mengekspresikan kisaran terbatas dari nilai yang mungkin dari induknya, maka itu mungkin seharusnya tidak menjadi sebuah subclass. Dia berpendapat untuk intuisi yang berlawanan: bahwa subclass mewakili objek yang lebih "spesifik", dan karena itu hubungan subclass lebih tepat.
Untuk menempatkan intuisi saya lebih konkret, saya berpikir bahwa jika saya memiliki subclass yang memperpanjang kelas induk tetapi satu-satunya kode yang menimpa subclass adalah konstruktor (ya, saya tahu konstruktor umumnya tidak "menimpa", bersabarlah), lalu yang benar-benar dibutuhkan adalah metode pembantu.
Sebagai contoh, pertimbangkan kelas kehidupan nyata ini:
public class DataHelperBuilder
{
public string DatabaseEngine { get; set; }
public string ConnectionString { get; set; }
public DataHelperBuilder(string databaseEngine, string connectionString)
{
DatabaseEngine = databaseEngine;
ConnectionString = connectionString;
}
// Other optional "DataHelper" configuration settings omitted
public DataHelper CreateDataHelper()
{
Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
dh.SetConnectionString(ConnectionString);
// Omitted some code that applies decorators to the returned object
// based on omitted configuration settings
return dh;
}
}
Klaimnya adalah bahwa sepenuhnya pantas untuk memiliki subkelas seperti ini:
public class SystemDataHelperBuilder
{
public SystemDataHelperBuilder()
: base(Configuration.GetSystemDatabaseEngine(),
Configuration.GetSystemConnectionString())
{
}
}
Jadi, pertanyaannya:
- Di antara orang yang berbicara tentang pola desain, intuisi mana yang benar? Apakah subkelas seperti yang dijelaskan di atas merupakan anti-pola?
- Jika itu anti-pola, apa namanya?
Saya minta maaf jika ini ternyata merupakan jawaban yang mudah di-googleable; pencarian saya di google sebagian besar mengembalikan informasi tentang anti-pola konstruktor telescoping dan tidak benar-benar apa yang saya cari.
sumber
Jawaban:
Jika semua yang ingin Anda lakukan adalah membuat kelas X dengan argumen tertentu, subklasifikasi adalah cara yang aneh untuk mengekspresikan maksud itu, karena Anda tidak menggunakan fitur apa pun yang diberikan oleh kelas dan pewarisan. Ini bukan benar-benar anti-pola, itu hanya aneh dan sedikit sia-sia (kecuali Anda punya alasan lain untuk itu). Cara yang lebih alami untuk mengungkapkan niat ini adalah Metode Pabrik , yang dalam hal ini adalah nama yang bagus untuk "metode penolong" Anda.
Mengenai intuisi umum, baik "lebih spesifik" dan "rentang terbatas" adalah cara berpikir yang berpotensi berbahaya tentang subkelas, karena keduanya menyiratkan bahwa menjadikan Square sebagai subkelas Persegi Panjang adalah ide yang bagus. Tanpa mengandalkan sesuatu yang formal seperti LSP, saya akan mengatakan intuisi yang lebih baik adalah bahwa subkelas menyediakan implementasi antarmuka dasar, atau memperluas antarmuka untuk menambahkan beberapa fungsi baru.
sumber
SystemDataHelperBuilder
khusus.Rekan kerja Anda benar (dengan asumsi sistem tipe standar).
Pikirkan tentang itu, kelas mewakili nilai-nilai hukum yang memungkinkan. Jika kelas
A
memiliki bidang satu byteF
, Anda mungkin cenderung berpikir yangA
memiliki 256 nilai hukum, tetapi intuisi itu salah.A
membatasi "setiap permutasi nilai yang pernah" ke "harus memiliki bidangF
0-255".Jika Anda memperpanjang
A
denganB
yang memiliki bidang byte lainFF
, itu menambahkan batasan . Alih-alih "nilai di manaF
byte", Anda memiliki "nilai di manaF
byte &&FF
juga byte". Karena Anda mematuhi aturan lama, semua yang bekerja untuk tipe dasar masih berfungsi untuk subtipe Anda. Tetapi karena Anda menambahkan aturan, Anda lebih jauh mengkhususkan diri dari "bisa apa saja".Atau memikirkan cara lain, menganggap bahwa
A
memiliki banyak subtipe:B
,C
, danD
. Variabel tipeA
dapat berupa subtipe ini, tetapi variabel tipeB
lebih spesifik. Ini (dalam kebanyakan sistem tipe) tidak boleh aC
atau aD
.Tingkatkan? Memiliki objek khusus berguna dan tidak jelas merusak kode. Mencapai hasil itu dengan menggunakan subtyping mungkin sedikit dipertanyakan, tetapi tergantung pada alat apa yang Anda miliki. Bahkan dengan alat yang lebih baik, implementasi ini sederhana, mudah dan kuat.
Saya tidak akan menganggap ini sebagai anti-pola karena tidak jelas salah dan buruk dalam situasi apa pun.
sumber
byte and byte
pasti memiliki nilai lebih dari sekadar tipebyte
; 256 kali lebih banyak. Selain itu, saya tidak berpikir Anda bisa mengacaukan aturan dengan sejumlah elemen (atau kardinalitas jika Anda ingin lebih tepatnya). Itu rusak ketika Anda mempertimbangkan set tak terbatas. Ada bilangan genap sama banyaknya dengan bilangan bulat, tetapi mengetahui bahwa suatu bilangan bahkan masih merupakan batasan / memberi saya lebih banyak informasi daripada hanya mengetahui bilangan bulat! Hal yang sama bisa dikatakan untuk bilangan asli.n -> 2n
). Ini tidak selalu mungkin; Anda tidak dapat memetakan bilangan bulat ke real, jadi set real "lebih besar" daripada bilangan bulat. Tetapi Anda dapat memetakan interval (nyata) [0, 1] ke himpunan semua real, sehingga menjadikannya "ukuran" yang sama. Himpunan bagian didefinisikan sebagai "setiap elemenA
juga merupakan elemenB
" justru karena begitu set Anda menjadi tak terbatas, dapat menjadi kasus bahwa mereka kardinalitas yang sama tetapi memiliki elemen yang berbeda.Tidak, ini bukan anti-pola. Saya dapat memikirkan sejumlah kasus penggunaan praktis untuk ini:
MySQLDao
danSqliteDao
di sistem Anda, tetapi karena alasan tertentu Anda ingin memastikan kumpulan hanya berisi data dari satu sumber, jika Anda menggunakan subkelas seperti yang Anda jelaskan, Anda dapat meminta kompilator memverifikasi kebenaran khusus ini. Di sisi lain, jika Anda menyimpan sumber data sebagai bidang, ini akan menjadi pemeriksaan run-time.AlgorithmConfiguration
+ProductClass
danAlgorithmInstance
. Dengan kata lain, jika Anda mengonfigurasiFooAlgorithm
untukProductClass
di file properti, Anda hanya bisa mendapatkan satuFooAlgorithm
untuk kelas produk itu. Satu-satunya cara untuk mendapatkan duaFooAlgorithm
s untuk diberikanProductClass
adalah dengan membuat subclassFooBarAlgorithm
. Ini tidak apa-apa, karena itu membuat file properti mudah digunakan (tidak perlu sintaks untuk banyak konfigurasi berbeda dalam satu bagian dalam file properti), dan sangat jarang akan ada lebih dari satu contoh untuk kelas produk yang diberikan.Intinya: Tidak ada salahnya melakukan hal ini. Sebuah Anti-pola didefinisikan sebagai:
Tidak ada risiko menjadi sangat kontraproduktif di sini, jadi ini bukan anti-pola.
sumber
Jika sesuatu adalah pola atau antipattern sangat tergantung pada bahasa dan lingkungan tempat Anda menulis. Misalnya,
for
loop adalah pola dalam assembly, C, dan bahasa yang serupa dan anti-pola dalam lisp.Izinkan saya menceritakan kisah beberapa kode yang saya tulis dulu ...
Itu dalam bahasa yang disebut LPC dan menerapkan kerangka kerja untuk casting mantra dalam game. Anda memiliki superclass mantra, yang memiliki subkelas mantra tempur yang menangani beberapa pemeriksaan, dan kemudian subkelas untuk mantra kerusakan target langsung tunggal, yang kemudian disubklasifikasikan ke mantra individu - misil sihir, baut petir dan sejenisnya.
Sifat bagaimana LPC bekerja adalah bahwa Anda memiliki objek master yang merupakan Singleton. sebelum Anda pergi 'eww Singleton' - dulu dan itu bukan "eww". Seseorang tidak membuat salinan mantra, tetapi lebih memilih metode statis (efektif) dalam mantra. Kode untuk rudal ajaib tampak seperti:
Dan kamu lihat itu? Kelasnya hanya konstruktor. Ini menetapkan beberapa nilai yang ada di kelas abstrak induknya (tidak, itu tidak boleh menggunakan setter - itu tidak berubah) dan hanya itu. Itu bekerja dengan baik . Itu mudah dikodekan, mudah diperluas dan mudah dimengerti.
Pola semacam ini diterjemahkan ke situasi lain di mana Anda memiliki subkelas yang memperluas kelas abstrak dan menetapkan beberapa nilai sendiri, tetapi semua fungsionalitas berada dalam kelas abstrak yang diwarisi. Melirik StringBuilder di Jawa untuk contoh ini - tidak cukup hanya konstruktor, tapi saya ditekan untuk menemukan banyak logika dalam metode itu sendiri (semuanya ada di AbstractStringBuilder).
Ini bukan hal yang buruk, dan tentu saja bukan anti-pola (dan dalam beberapa bahasa itu mungkin pola itu sendiri).
sumber
Penggunaan subclass yang paling umum yang dapat saya pikirkan adalah hierarki pengecualian, meskipun itu adalah kasus yang merosot di mana kita biasanya tidak mendefinisikan apa pun di dalam kelas, selama bahasanya memungkinkan kita mewarisi konstruktor. Kami menggunakan warisan untuk menyatakan bahwa
ReadError
ini adalah kasus khususIOError
, dan seterusnya, tetapiReadError
tidak perlu mengganti metode apa punIOError
.Kami melakukan ini karena pengecualian ditangkap dengan memeriksa tipenya. Jadi, kita perlu menyandikan spesialisasi dalam jenis sehingga seseorang dapat menangkap hanya
ReadError
tanpa menangkap semuaIOError
, jika mereka mau.Biasanya, memeriksa jenis barang adalah bentuk yang sangat buruk dan kami berusaha menghindarinya. Jika kita berhasil menghindarinya, maka tidak ada kebutuhan penting untuk mengekspresikan spesialisasi dalam sistem tipe.
Ini mungkin masih berguna, misalnya jika nama tipe muncul dalam bentuk string dari objek: ini adalah bahasa dan mungkin kerangka spesifik. Pada prinsipnya Anda dapat mengganti nama tipe dalam sistem seperti itu dengan panggilan ke metode kelas, dalam hal ini kami akan menimpa lebih dari sekadar konstruktor
SystemDataHelperBuilder
, kami juga akan menimpanyaloggingname()
. Jadi, jika ada pencatatan seperti itu di sistem Anda, Anda dapat membayangkan bahwa meskipun kelas Anda secara harfiah hanya mengabaikan konstruktor, "secara moral" juga menimpa nama kelas, dan untuk tujuan praktis, bukan subkelas konstruktor saja. Ia memiliki perilaku berbeda yang diinginkan, ituJadi, saya akan mengatakan bahwa kodenya dapat menjadi ide yang bagus jika ada beberapa kode di tempat lain yang entah bagaimana memeriksa contoh
SystemDataHelperBuilder
, baik secara eksplisit dengan cabang (seperti pengecualian menangkap cabang berdasarkan jenis) atau mungkin karena ada beberapa kode generik yang akan berakhir menggunakan namaSystemDataHelperBuilder
dengan cara yang bermanfaat (bahkan jika itu hanya logging).Namun, menggunakan jenis untuk kasus khusus memang menimbulkan kebingungan, karena jika
ImmutableRectangle(1,1)
danImmutableSquare(1)
berperilaku berbeda dengan cara apa pun maka pada akhirnya seseorang akan bertanya-tanya mengapa dan mungkin berharap tidak. Tidak seperti hierarki pengecualian, Anda memeriksa apakah persegi panjang adalah persegi dengan memeriksa apakahheight == width
, tidak denganinstanceof
. Dalam contoh Anda, ada perbedaan antara aSystemDataHelperBuilder
dan a yangDataHelperBuilder
dibuat dengan argumen yang sama persis, dan yang berpotensi menyebabkan masalah. Oleh karena itu, fungsi untuk mengembalikan objek yang dibuat dengan argumen "benar" menurut saya biasanya merupakan hal yang benar untuk dilakukan: subkelas menghasilkan dua representasi berbeda dari hal "yang sama", dan itu hanya membantu jika Anda melakukan sesuatu yang Anda biasanya akan berusaha untuk tidak melakukannya.Perhatikan bahwa saya tidak berbicara dalam hal pola desain dan anti-pola, karena saya tidak percaya bahwa itu mungkin untuk memberikan taksonomi semua ide baik dan buruk yang pernah dipikirkan oleh seorang programmer. Jadi, tidak setiap ide adalah pola atau anti-pola, hanya ketika seseorang mengidentifikasi bahwa itu terjadi berulang kali dalam konteks yang berbeda dan menamainya ;-) Saya tidak mengetahui adanya nama khusus untuk jenis subklasifikasi ini.
sumber
ImmutableSquareMatrix:ImmutableMatrix
, asalkan ada cara yang efisien untuk meminta suatuImmutableMatrix
untuk membuat isinyaImmutableSquareMatrix
jika mungkin. SekalipunImmutableSquareMatrix
pada dasarnya seorangImmutableMatrix
yang konstruktornya mensyaratkan tinggi dan lebar yang cocok, dan yangAsSquareMatrix
metodenya hanya akan mengembalikan dirinya sendiri (daripada misalnya membangunImmutableSquareMatrix
yang membungkus susunan backing yang sama), desain semacam itu akan memungkinkan validasi parameter kompilasi-waktu untuk metode yang memerlukan kuadrat matriksSystemDataHelperBuilder
, dan bukan sembarang yangDataHelperBuilder
akan dilakukan, maka masuk akal untuk menentukan tipe untuk itu (dan antarmuka, dengan asumsi Anda menangani DI dengan cara itu) . Dalam contoh Anda, ada beberapa operasi yang bisa dimiliki oleh matriks kuadrat yang bukan kuadrat, seperti determinan, tetapi poin Anda bagus bahwa meskipun sistem tidak membutuhkan yang Anda masih ingin memeriksa berdasarkan jenis .height == width
, tidak denganinstanceof
". Anda mungkin lebih suka sesuatu yang bisa Anda nyatakan secara statis.foo=new ImmutableMatrix(someReadableMatrix)
lebih jelas daripadasomeReadableMatrix.AsImmutable()
atau bahkanImmutableMatrix.Create(someReadableMatrix)
, tetapi bentuk-bentuk terakhir menawarkan kemungkinan semantik yang bermanfaat yang tidak dimiliki oleh yang pertama; jika konstruktor dapat mengatakan bahwa matriks adalah kuadrat, dapat memiliki jenisnya mencerminkan yang bisa lebih bersih daripada membutuhkan kode hilir yang membutuhkan matriks kuadrat untuk membuat instance objek baru.new
operator. Kelas hanyalah callable, yang ketika dipanggil akan mengembalikan (secara default) instance dari dirinya sendiri. Jadi, jika Anda ingin mempertahankan konsistensi antarmuka sangat mungkin untuk menulis fungsi pabrik yang namanya dimulai dengan huruf kapital, karena itu terlihat seperti panggilan konstruktor. Bukannya saya melakukan ini secara rutin dengan fungsi pabrik, tetapi ada di sana saat Anda membutuhkannya.Definisi Object Oriented yang paling ketat dari hubungan subclass dikenal sebagai «is-a». Menggunakan contoh tradisional persegi dan persegi panjang, persegi «adalah-» persegi panjang.
Dengan ini, Anda harus menggunakan subclassing untuk situasi apa pun di mana Anda merasa «is-a» adalah hubungan yang bermakna untuk kode Anda.
Dalam kasus spesifik Anda dari subkelas dengan apa-apa selain konstruktor, Anda harus menganalisisnya dari perspektif «is-a». Jelaslah bahwa SystemDataHelperBuilder «is-a» DataHelperBuilder, jadi pass pertama menyarankan itu adalah penggunaan yang valid.
Pertanyaan yang harus Anda jawab adalah apakah ada kode lain yang memanfaatkan hubungan «is-a» ini. Apakah ada kode lain yang mencoba membedakan SystemDataHelperBuilders dari populasi umum DataHelperBuilders? Jika demikian, hubungannya cukup masuk akal, dan Anda harus menjaga subkelasnya.
Namun, jika tidak ada kode lain yang melakukan ini, maka apa yang Anda miliki adalah hubungan yang bisa berupa hubungan «adalah-a», atau bisa diterapkan dengan pabrik. Dalam hal ini, Anda bisa memilih jalan yang lain, tetapi saya akan merekomendasikan hubungan Factory daripada hubungan «is-a». Ketika menganalisis kode Berorientasi Objek yang ditulis oleh individu lain, hierarki kelas adalah sumber utama informasi. Ini memberikan hubungan «is-a» yang mereka perlukan untuk memahami kode. Dengan demikian, menempatkan perilaku ini ke dalam subkelas mempromosikan perilaku dari "sesuatu yang akan ditemukan seseorang jika mereka menggali metode superclass" menjadi "sesuatu yang semua orang akan melihat sebelum mereka bahkan mulai menyelam ke dalam kode." Mengutip Guido van Rossum, "kode dibaca lebih sering daripada yang tertulis," jadi mengurangi keterbacaan kode Anda untuk pengembang yang akan datang adalah harga yang harus dibayar. Saya akan memilih untuk tidak membayarnya, jika saya bisa menggunakan pabrik sebagai gantinya.
sumber
Subtipe tidak pernah "salah" selama Anda selalu bisa mengganti instance dari supertype-nya dengan instance dari subtipe dan semuanya masih berfungsi dengan benar. Ini memeriksa selama subclass tidak pernah mencoba untuk melemahkan jaminan yang dibuat supertype. Itu bisa membuat jaminan yang lebih kuat (lebih spesifik), jadi dalam hal itu intuisi rekan kerja Anda benar.
Mengingat bahwa, subkelas yang Anda buat mungkin tidak salah (karena saya tidak tahu persis apa yang mereka lakukan, saya tidak bisa mengatakannya dengan pasti.) Anda harus waspada terhadap masalah kelas dasar yang rapuh, dan Anda juga harus pertimbangkan apakah suatu
interface
akan menguntungkan Anda, tetapi itu berlaku untuk semua penggunaan warisan.sumber
Saya pikir kunci untuk menjawab pertanyaan ini adalah dengan melihat yang satu ini, skenario penggunaan tertentu.
Warisan digunakan untuk menegakkan opsi konfigurasi basis data, seperti string koneksi.
Ini adalah pola anti karena kelas melanggar Prinsip Tanggung Jawab Tunggal --- Mengkonfigurasi dirinya sendiri dengan sumber spesifik konfigurasi itu dan tidak "Melakukan satu hal, dan melakukannya dengan baik." Konfigurasi kelas tidak boleh dimasukkan ke dalam kelas itu sendiri. Untuk mengatur string koneksi database, sering kali saya melihat ini terjadi pada level aplikasi selama beberapa peristiwa terjadi ketika aplikasi dijalankan.
sumber
Saya menggunakan konstruktor hanya subkelas cukup teratur untuk mengekspresikan konsep yang berbeda.
Sebagai contoh, pertimbangkan kelas berikut:
Kami kemudian dapat membuat subkelas:
Anda kemudian dapat menggunakan CapitalizeAndPrintOutputter di mana saja dalam kode Anda dan Anda tahu persis apa jenis outputter itu. Selain itu, pola ini sebenarnya membuatnya sangat mudah untuk menggunakan wadah DI untuk membuat kelas ini. Jika wadah DI tahu tentang PrintOutputMethod dan Capitalizer, itu bahkan dapat secara otomatis membuat CapitalizeAndPrintOutputter untuk Anda.
Menggunakan pola ini benar-benar sangat fleksibel dan bermanfaat. Ya, Anda dapat menggunakan metode pabrik untuk melakukan hal yang sama, tetapi kemudian (setidaknya dalam C #) Anda kehilangan beberapa kekuatan dari sistem tipe dan tentu saja menggunakan wadah DI menjadi lebih rumit.
sumber
Pertama-tama saya ingin perhatikan bahwa ketika kode ditulis, sub-kelas sebenarnya tidak lebih spesifik daripada induknya, karena dua bidang yang diinisialisasi keduanya dapat diselesaikan dengan kode klien.
Sebuah instance dari subclass hanya dapat dibedakan menggunakan downcast.
Sekarang, jika Anda memiliki desain di mana
DatabaseEngine
danConnectionString
properti diimplementasikan kembali oleh subclass, yang diaksesConfiguration
untuk melakukannya, maka Anda memiliki kendala yang lebih masuk akal memiliki subclass.Karena itu, "anti-pola" terlalu kuat untuk seleraku; tidak ada yang salah di sini.
sumber
Dalam contoh yang Anda berikan, pasangan Anda benar. Dua pembangun pembantu data adalah strategi. Satu lebih spesifik daripada yang lain, tetapi keduanya dapat dipertukarkan begitu diinisialisasi.
Pada dasarnya jika klien dari datahelperbuilder adalah untuk menerima systemdatahelperbuilder, tidak ada yang akan rusak. Pilihan lain adalah memiliki systemdatahelperbuilder menjadi contoh statis dari kelas datahelperbuilder.
sumber