Ketika merencanakan program saya, saya sering memulai dengan rantai pemikiran seperti:
Tim sepak bola hanyalah daftar pemain sepakbola. Karena itu, saya harus mewakilinya dengan:
var football_team = new List<FootballPlayer>();
Urutan daftar ini mewakili urutan pemain terdaftar dalam daftar.
Tetapi saya menyadari kemudian bahwa tim juga memiliki properti lain, selain daftar pemain semata, yang harus dicatat. Misalnya, total skor berjalan musim ini, anggaran saat ini, warna seragam, a string
mewakili nama tim, dll.
Jadi saya berpikir:
Oke, tim sepak bola seperti daftar pemain, tetapi selain itu, ia memiliki nama (a
string
) dan total skor berjalan (anint
). .NET tidak menyediakan kelas untuk menyimpan tim sepak bola, jadi saya akan membuat kelas sendiri. Struktur yang paling mirip dan relevan saat ini adalahList<FootballPlayer>
, jadi saya akan mewarisi darinya:class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
Tetapi ternyata suatu pedoman mengatakan Anda tidak seharusnya mewarisiList<T>
. Saya benar-benar bingung dengan pedoman ini dalam dua hal.
Kenapa tidak?
Tampaknya List
entah bagaimana dioptimalkan untuk kinerja . Bagaimana? Masalah kinerja apa yang akan saya sebabkan jika saya memperpanjang List
? Apa tepatnya yang akan pecah?
Alasan lain yang saya lihat adalah yang List
disediakan oleh Microsoft, dan saya tidak punya kendali atasnya, jadi saya tidak bisa mengubahnya nanti, setelah mengekspos "API publik" . Tetapi saya berjuang untuk memahami hal ini. Apa itu API publik dan mengapa saya harus peduli? Jika proyek saya saat ini tidak dan sepertinya tidak pernah memiliki API publik ini, dapatkah saya dengan aman mengabaikan panduan ini? Jika saya memang mewarisi List
dan ternyata saya memerlukan API publik, kesulitan apa yang akan saya miliki?
Mengapa itu penting? Daftar adalah daftar. Apa yang mungkin bisa berubah? Apa yang mungkin ingin saya ubah?
Dan terakhir, jika Microsoft tidak ingin saya mewarisi List
, mengapa mereka tidak membuat kelas sealed
?
Apa lagi yang harus saya gunakan?
Rupanya, untuk koleksi khusus, Microsoft telah menyediakan Collection
kelas yang seharusnya diperluas List
. Tetapi kelas ini sangat telanjang, dan tidak memiliki banyak hal berguna, sepertiAddRange
misalnya. Jawaban jvitor83 memberikan alasan kinerja untuk metode tertentu, tetapi bagaimana lambat AddRange
tidak lebih baik daripada tidak AddRange
?
Mewarisi dari Collection
jauh lebih banyak pekerjaan daripada mewarisi List
, dan saya tidak melihat manfaatnya. Tentunya Microsoft tidak akan mengatakan kepada saya untuk melakukan pekerjaan ekstra tanpa alasan, jadi saya tidak dapat menahan perasaan bahwa saya entah bagaimana salah memahami sesuatu, dan mewarisi Collection
sebenarnya bukanlah solusi yang tepat untuk masalah saya.
Saya telah melihat saran seperti penerapan IList
. Tidak, tidak. Ini adalah puluhan baris kode boilerplate yang tidak menguntungkan saya.
Terakhir, beberapa menyarankan membungkus List
sesuatu:
class FootballTeam
{
public List<FootballPlayer> Players;
}
Ada dua masalah dengan ini:
Itu membuat kode saya verbose tidak perlu. Sekarang saya harus menelepon,
my_team.Players.Count
bukan hanyamy_team.Count
. Untungnya, dengan C # saya dapat mendefinisikan pengindeks untuk membuat pengindeksan transparan, dan meneruskan semua metode internalList
... Tapi itu banyak kode! Apa yang saya dapat untuk semua pekerjaan itu?Itu tidak masuk akal. Tim sepak bola tidak "memiliki" daftar pemain. Ini adalah daftar pemain. Anda tidak mengatakan "John McFootballer telah bergabung dengan pemain SomeTeam". Anda mengatakan "John telah bergabung dengan SomeTeam". Anda tidak menambahkan huruf ke "karakter string", Anda menambahkan huruf ke string. Anda tidak menambahkan buku ke buku perpustakaan, Anda menambahkan buku ke perpustakaan.
Saya menyadari bahwa apa yang terjadi "di bawah tenda" dapat dikatakan sebagai "menambahkan X ke daftar internal Y", tetapi ini sepertinya cara berpikir yang sangat kontra-intuitif tentang dunia.
Pertanyaan saya (dirangkum)
Apa C # cara yang benar mewakili struktur data, yang, "logis" (yang mengatakan, "untuk pikiran manusia") hanya list
dari things
dengan beberapa lonceng dan peluit?
Apakah mewarisi dari List<T>
selalu tidak dapat diterima? Kapan itu bisa diterima? Kenapa / mengapa tidak? Apa yang harus dipertimbangkan oleh seorang programmer, ketika memutuskan apakah akan mewarisi List<T>
atau tidak?
string
diperlukan untuk melakukan semua yangobject
dapat dilakukan dan banyak lagi .Jawaban:
Ada beberapa jawaban bagus di sini. Saya akan menambahkan kepada mereka poin-poin berikut.
Tanyakan kepada sepuluh orang yang bukan programmer komputer yang akrab dengan keberadaan sepak bola untuk mengisi formulir kosong:
Apakah ada yang mengatakan "daftar pemain sepak bola dengan beberapa lonceng dan peluit", atau mereka semua "tim olahraga" katakanlah atau "klub" atau "organisasi"? Gagasan Anda bahwa tim sepak bola adalah jenis daftar pemain tertentu ada di pikiran manusia Anda dan pikiran manusia Anda sendiri.
List<T>
adalah sebuah mekanisme . Tim sepak bola adalah objek bisnis - yaitu, objek yang mewakili beberapa konsep yang ada dalam domain bisnis program. Jangan campur itu! Tim sepak bola adalah sejenis tim; ia memiliki daftar, daftar adalah daftar pemain . Daftar nama bukanlah daftar pemain tertentu . Daftar nama adalah daftar pemain. Jadi buatlah properti yang disebutRoster
aList<Player>
. Dan buatReadOnlyList<Player>
saat Anda sedang melakukannya, kecuali jika Anda percaya bahwa semua orang yang tahu tentang tim sepak bola dapat menghapus pemain dari daftar.Tidak dapat diterima oleh siapa? Saya? Tidak.
Ketika Anda sedang membangun mekanisme yang memperluas
List<T>
mekanisme .Apakah saya membangun mekanisme atau objek bisnis ?
Anda menghabiskan lebih banyak waktu mengetik pertanyaan Anda bahwa Anda perlu menulis metode penerusan untuk anggota yang relevan
List<T>
sebanyak lima puluh kali lipat. Anda jelas tidak takut pada kata kerja, dan kami berbicara tentang sejumlah kecil kode di sini; ini hanya beberapa menit.MEMPERBARUI
Saya memikirkannya lagi dan ada alasan lain untuk tidak menjadikan tim sepak bola sebagai daftar pemain. Bahkan mungkin ide yang buruk untuk memodelkan tim sepak bola sebagai memiliki daftar pemain juga. Masalah dengan tim sebagai / memiliki daftar pemain adalah bahwa apa yang Anda dapatkan adalah potret tim pada saat tertentu . Saya tidak tahu apa kasus bisnis Anda untuk kelas ini, tetapi jika saya memiliki kelas yang mewakili tim sepak bola, saya ingin mengajukan pertanyaan seperti "berapa banyak pemain Seahawks yang melewatkan pertandingan karena cedera antara tahun 2003 dan 2013?" atau "Pemain Denver apa yang sebelumnya bermain untuk tim lain memiliki rekor lari yard-over-year terbesar?" atau " Apakah Piggers berjalan sepanjang tahun ini? "
Artinya, sebuah tim sepak bola bagi saya tampaknya dimodelkan dengan baik sebagai kumpulan fakta sejarah seperti ketika seorang pemain direkrut, cedera, pensiun, dll. Jelas daftar pemain saat ini adalah fakta penting yang mungkin harus menjadi depan dan- pusat, tetapi mungkin ada hal menarik lainnya yang ingin Anda lakukan dengan objek ini yang memerlukan perspektif yang lebih historis.
sumber
IEnumerable<T>
- a ?Wow, pos Anda memiliki banyak pertanyaan dan poin. Sebagian besar alasan yang Anda dapatkan dari Microsoft tepat pada titik. Mari kita mulai dengan segalanya
List<T>
List<T>
adalah sangat optimal. Penggunaan utamanya adalah untuk digunakan sebagai anggota pribadi dari suatu objek.class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
. Sekarang semudah melakukannyavar list = new MyList<int, string>();
.IList<T>
jika Anda membutuhkan konsumen untuk memiliki daftar yang diindeks. Ini memungkinkan Anda mengubah implementasi di dalam kelas nanti.Collection<T>
sangat umum karena ini adalah konsep generik ... namanya mengatakan itu semua; itu hanya koleksi. Ada versi yang lebih tepat sepertiSortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
, dll yang masing-masing melaksanakanIList<T>
tapi tidakList<T>
.Collection<T>
memungkinkan anggota (yaitu Tambah, Hapus, dll.) ditimpa karena mereka virtual.List<T>
tidak.Jika saya menulis kode ini, kelas mungkin akan terlihat seperti ini:
sumber
List<T>
diperluas sebagai kelas dalam pribadi yang menggunakan obat generik untuk menyederhanakan penggunaan dan deklarasiList<T>
. Jika ekstensi itu hanyaclass FootballRoster : List<FootballPlayer> { }
, maka ya saya bisa menggunakan alias sebagai gantinya. Saya kira perlu dicatat bahwa Anda dapat menambahkan anggota tambahan / fungsionalitas, tetapi terbatas karena Anda tidak dapat menimpa anggota pentingList<T>
.Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
Nah, itu alasan bagus untuk tidak mewarisi dari Daftar. Jawaban yang lain memiliki filosofi yang terlalu banyak.Kode sebelumnya berarti: sekelompok pria dari jalanan bermain sepak bola, dan mereka memiliki nama. Sesuatu seperti:
Bagaimanapun, kode ini (dari jawaban saya)
Berarti: ini adalah tim sepak bola yang memiliki manajemen, pemain, admin, dll. Sesuatu seperti:
Ini adalah bagaimana logika Anda disajikan dalam gambar ...
sumber
private readonly List<T> _roster = new List<T>(44);
System.Linq
namespace.Ini adalah contoh klasik dari komposisi vs warisan .
Dalam kasus khusus ini:
Apakah tim daftar pemain dengan perilaku yang ditambahkan
atau
Apakah tim objek sendiri yang kebetulan berisi daftar pemain.
Dengan memperpanjang Daftar, Anda membatasi diri dengan beberapa cara:
Anda tidak dapat membatasi akses (misalnya, menghentikan orang mengubah daftar). Anda mendapatkan semua metode Daftar apakah Anda membutuhkan / menginginkan semuanya atau tidak.
Apa yang terjadi jika Anda ingin memiliki daftar hal lain juga. Misalnya, tim memiliki pelatih, manajer, penggemar, peralatan, dll. Beberapa dari mereka mungkin daftar dalam hak mereka sendiri.
Anda membatasi opsi Anda untuk warisan. Misalnya Anda mungkin ingin membuat objek Tim generik, dan kemudian memiliki BaseballTeam, FootballTeam, dll. Yang diwarisi dari itu. Untuk mewarisi dari Daftar, Anda perlu melakukan warisan dari Tim, tetapi itu berarti semua jenis tim dipaksa untuk memiliki implementasi daftar yang sama.
Komposisi - termasuk objek yang memberikan perilaku yang Anda inginkan di dalam objek Anda.
Warisan - objek Anda menjadi turunan dari objek yang memiliki perilaku yang Anda inginkan.
Keduanya memiliki kegunaannya, tetapi ini adalah kasus yang jelas di mana komposisi lebih disukai.
sumber
Seperti yang semua orang tunjukkan, tim pemain bukanlah daftar pemain. Kesalahan ini dibuat oleh banyak orang di mana saja, mungkin pada berbagai tingkat keahlian. Seringkali masalahnya halus dan kadang-kadang sangat kotor, seperti dalam kasus ini. Desain seperti itu buruk karena melanggar Prinsip Pergantian Liskov . Internet memiliki banyak artikel bagus yang menjelaskan konsep ini misalnya, http://en.wikipedia.org/wiki/Liskov_substitution_principle
Singkatnya, ada dua aturan yang harus dilestarikan dalam hubungan Orangtua / Anak di antara kelas:
Dengan kata lain, Orangtua adalah definisi yang diperlukan dari seorang anak, dan seorang anak adalah definisi yang cukup dari Orangtua.
Berikut adalah cara untuk memikirkan solusi yang ada dan menerapkan prinsip di atas yang seharusnya membantu seseorang menghindari kesalahan seperti itu. Seseorang harus menguji hipotesisnya dengan memverifikasi jika semua operasi kelas induk valid untuk kelas turunan baik secara struktural maupun semantik.
Seperti yang Anda lihat, hanya karakteristik pertama dari daftar yang berlaku untuk tim. Karenanya tim bukan daftar. Daftar akan menjadi detail implementasi tentang bagaimana Anda mengelola tim Anda, sehingga hanya digunakan untuk menyimpan objek pemain dan dimanipulasi dengan metode kelas Tim.
Pada titik ini saya ingin berkomentar bahwa kelas Tim seharusnya, menurut pendapat saya, bahkan tidak diimplementasikan menggunakan Daftar; itu harus diimplementasikan menggunakan struktur data Set (HashSet, misalnya) dalam banyak kasus.
sumber
Bagaimana jika
FootballTeam
memiliki tim cadangan bersama dengan tim utama?Bagaimana Anda memodelkan itu dengan?
Hubungannya jelas memiliki dan tidak adalah .
atau
RetiredPlayers
?Sebagai aturan praktis, jika Anda ingin mewarisi dari koleksi, beri nama kelas
SomethingCollection
.Apakah secara
SomethingCollection
semantik Anda masuk akal? Hanya lakukan ini jika jenis Anda adalah koleksiSomething
.Dalam hal
FootballTeam
itu tidak terdengar benar. ATeam
lebih dari aCollection
. ATeam
dapat memiliki pelatih, pelatih, dll seperti yang ditunjukkan oleh jawaban lainnya.FootballCollection
terdengar seperti kumpulan bola kaki atau mungkin koleksi perlengkapan sepak bola.TeamCollection
, kumpulan tim.FootballPlayerCollection
terdengar seperti kumpulan pemain yang akan menjadi nama yang valid untuk kelas yang mewarisi dariList<FootballPlayer>
jika Anda benar-benar ingin melakukan itu.Benar
List<FootballPlayer>
-benar tipe yang sangat baik untuk dihadapi. MungkinIList<FootballPlayer>
jika Anda mengembalikannya dari suatu metode.Singkatnya
Bertanya pada diri sendiri
Apakah
X
aY
? atau MemilikiX
sebuahY
?Apakah nama kelas saya berarti apa adanya?
sumber
DefensivePlayers
,OffensivePlayers
atauOtherPlayers
, mungkin sah berguna untuk memiliki jenis yang dapat digunakan oleh kode yang mengharapkanList<Player>
tetapi anggota juga termasukDefensivePlayers
,OffsensivePlayers
atauSpecialPlayers
tipeIList<DefensivePlayer>
,IList<OffensivePlayer>
danIList<Player>
. Orang dapat menggunakan objek terpisah untuk menembolok daftar yang terpisah, tetapi merangkumnya dalam objek yang sama dengan daftar utama akan tampak lebih bersih [gunakan pembatalan daftar pencacah ...Desain> Implementasi
Metode dan properti apa yang Anda paparkan adalah keputusan desain. Apa kelas dasar yang Anda warisi adalah detail implementasi. Saya merasa perlu mengambil langkah mundur ke yang pertama.
Objek adalah kumpulan data dan perilaku.
Jadi pertanyaan pertama Anda seharusnya:
Ingatlah bahwa pewarisan menyiratkan hubungan "isa" (adalah), sedangkan komposisi menyiratkan hubungan "memiliki" (hasa). Pilih yang tepat untuk situasi Anda dalam pandangan Anda, mengingat ke mana hal-hal mungkin terjadi ketika aplikasi Anda berkembang.
Pertimbangkan berpikir dalam antarmuka sebelum Anda berpikir dalam tipe konkret, karena beberapa orang merasa lebih mudah untuk menempatkan otak mereka dalam "mode desain" seperti itu.
Ini bukan sesuatu yang dilakukan setiap orang secara sadar pada level ini dalam koding sehari-hari. Tetapi jika Anda merenungkan topik semacam ini, Anda melangkah di perairan desain. Menyadari hal itu bisa membebaskan.
Pertimbangkan Spesifikasi Desain
Lihatlah Daftar <T> dan IList <T> di MSDN atau Visual Studio. Lihat metode dan properti apa yang mereka tampilkan. Apakah semua metode ini terlihat seperti sesuatu yang seseorang ingin lakukan terhadap FootballTeam dalam pandangan Anda?
Apakah footballTeam.Reverse () masuk akal bagi Anda? Apakah footballTeam.ConvertAll <TOutput> () terlihat seperti sesuatu yang Anda inginkan?
Ini bukan pertanyaan jebakan; jawabannya mungkin benar-benar "ya". Jika Anda menerapkan / mewarisi Daftar <Player> atau IList <Player>, Anda terjebak dengannya; jika itu ideal untuk model Anda, lakukanlah.
Jika Anda memutuskan ya, itu masuk akal, dan Anda ingin objek Anda dapat diperlakukan sebagai kumpulan / daftar pemain (perilaku), dan karena itu Anda ingin menerapkan ICollection atau IList, dengan segala cara melakukannya. Secara alami:
Jika Anda ingin objek Anda berisi koleksi / daftar pemain (data), dan karena itu Anda ingin koleksi atau daftar menjadi properti atau anggota, dengan segala cara melakukannya. Secara alami:
Anda mungkin merasa bahwa Anda ingin orang hanya dapat menyebutkan satu set pemain, daripada menghitungnya, menambah atau menghapusnya. IEnumerable <Player> adalah opsi yang benar-benar valid untuk dipertimbangkan.
Anda mungkin merasa bahwa tidak ada antarmuka ini yang berguna dalam model Anda sama sekali. Ini lebih kecil kemungkinannya (IEnumerable <T> berguna dalam banyak situasi) tetapi masih mungkin.
Siapa pun yang mencoba memberi tahu Anda bahwa salah satu di antaranya salah dan pasti salah dalam setiap kasus adalah salah arah. Siapa pun yang mencoba memberi tahu Anda secara kategoris dan definitif benar dalam setiap kasus adalah kesalahan.
Pindah ke Implementasi
Setelah Anda memutuskan data dan perilaku, Anda dapat membuat keputusan tentang implementasi. Ini termasuk kelas konkret yang Anda andalkan melalui pewarisan atau komposisi.
Ini mungkin bukan langkah besar, dan orang sering mengacaukan desain dan implementasi karena sangat mungkin untuk menjalankan semuanya di kepala Anda dalam satu atau dua detik dan mulai mengetik.
Eksperimen Pikiran
Contoh buatan: seperti yang orang lain katakan, sebuah tim tidak selalu "hanya" kumpulan pemain. Apakah Anda memelihara koleksi skor pertandingan untuk tim? Apakah tim dapat dipertukarkan dengan klub, sesuai model Anda? Jika demikian, dan jika tim Anda adalah kumpulan pemain, mungkin itu juga kumpulan staf dan / atau kumpulan skor. Kemudian Anda berakhir dengan:
Meskipun desain, pada titik ini di C # Anda tidak akan dapat menerapkan semua ini dengan mewarisi dari Daftar <T>, karena C # "hanya" mendukung pewarisan tunggal. (Jika Anda sudah mencoba ini di C ++, Anda mungkin menganggap ini sebagai Good Thing.) Menerapkan satu koleksi melalui warisan dan satu melalui komposisi cenderung terasa kotor. Dan properti seperti Count menjadi membingungkan bagi pengguna kecuali Anda menerapkan ILIst <Player> .Count dan IList <StaffMember> .Count dll. Secara eksplisit, dan kemudian mereka hanya menyakitkan daripada membingungkan. Anda bisa melihat ke mana arahnya; tetapi perasaan saat memikirkan jalan ini mungkin memberi tahu Anda merasa salah untuk menuju ke arah ini (dan benar atau salah, kolega Anda mungkin juga jika Anda menerapkannya dengan cara ini!)
Jawaban Singkat (Terlambat)
Pedoman tentang tidak mewarisi dari kelas koleksi tidak spesifik C #, Anda akan menemukannya dalam banyak bahasa pemrograman. Itu diterima kebijaksanaan bukan hukum. Salah satu alasannya adalah bahwa dalam praktiknya komposisi dianggap sering menang atas warisan dalam hal kelengkapan, penerapan, dan pemeliharaan. Ini lebih umum dengan objek dunia / domain nyata untuk menemukan hubungan "hasa" yang berguna dan konsisten daripada hubungan "isa" yang berguna dan konsisten kecuali Anda tenggelam dalam abstrak, terutama saat waktu berlalu dan data serta perilaku yang tepat dari objek dalam kode perubahan. Ini seharusnya tidak menyebabkan Anda selalu mengesampingkan pewarisan dari kelas koleksi; tapi mungkin sugestif.
sumber
Pertama-tama, itu berkaitan dengan kegunaan. Jika Anda menggunakan warisan,
Team
kelas akan mengekspos perilaku (metode) yang dirancang murni untuk manipulasi objek. Misalnya,AsReadOnly()
atauCopyTo(obj)
metode tidak masuk akal untuk objek tim. Alih-alihAddRange(items)
metode yang Anda mungkin ingin metode yang lebih deskriptifAddPlayers(players)
.Jika Anda ingin menggunakan LINQ, mengimplementasikan antarmuka umum seperti
ICollection<T>
atauIEnumerable<T>
akan lebih masuk akal.Seperti disebutkan, komposisi adalah cara yang tepat untuk melakukannya. Cukup terapkan daftar pemain sebagai variabel pribadi.
sumber
Tim sepak bola bukan daftar pemain sepak bola. Tim sepak bola terdiri dari daftar pemain sepak bola!
Ini secara logis salah:
dan ini benar:
sumber
Itu tergantung pada konteksnya
Ketika Anda mempertimbangkan tim Anda sebagai daftar pemain, Anda memproyeksikan "ide" tim bola kaki ke satu aspek: Anda mengurangi "tim" menjadi orang-orang yang Anda lihat di lapangan. Proyeksi ini hanya benar dalam konteks tertentu. Dalam konteks yang berbeda, ini mungkin sepenuhnya salah. Bayangkan Anda ingin menjadi sponsor tim. Jadi, Anda harus berbicara dengan manajer tim. Dalam konteks ini tim diproyeksikan ke daftar manajernya. Dan kedua daftar ini biasanya tidak tumpang tindih. Konteks lain adalah saat ini versus mantan pemain, dll.
Semantik yang tidak jelas
Jadi masalah dengan mempertimbangkan tim sebagai daftar pemainnya adalah semantiknya tergantung pada konteksnya dan bahwa itu tidak dapat diperpanjang ketika konteksnya berubah. Selain itu sulit untuk mengekspresikan, konteks mana yang Anda gunakan.
Kelas bisa diperluas
Ketika Anda menggunakan kelas dengan hanya satu anggota (misalnya
IList activePlayers
), Anda dapat menggunakan nama anggota (dan juga komentarnya) untuk membuat konteksnya lebih jelas. Ketika ada konteks tambahan, Anda hanya menambahkan anggota tambahan.Kelas lebih kompleks
Dalam beberapa kasus mungkin terlalu sulit untuk membuat kelas tambahan. Setiap definisi kelas harus dimuat melalui classloader dan akan di-cache oleh mesin virtual. Ini akan membuat Anda kehilangan kinerja dan memori. Ketika Anda memiliki konteks yang sangat spesifik, mungkin boleh saja mempertimbangkan tim sepak bola sebagai daftar pemain. Tetapi dalam kasus ini, Anda harus benar-benar hanya menggunakan a
IList
, bukan kelas yang berasal darinya.Kesimpulan / Pertimbangan
Ketika Anda memiliki konteks yang sangat spesifik, tidak masalah untuk mempertimbangkan tim sebagai daftar pemain. Sebagai contoh di dalam metode itu sepenuhnya OK untuk menulis:
Saat menggunakan F #, bahkan bisa OK untuk membuat singkatan jenis:
Tetapi ketika konteksnya lebih luas atau bahkan tidak jelas, Anda seharusnya tidak melakukan ini. Ini terutama terjadi ketika Anda membuat kelas baru yang konteksnya di mana ia dapat digunakan di masa depan tidak jelas. Tanda peringatan adalah ketika Anda mulai menambahkan atribut tambahan ke kelas Anda (nama tim, pelatih, dll.). Ini adalah tanda yang jelas bahwa konteks di mana kelas akan digunakan tidak tetap dan akan berubah di masa depan. Dalam hal ini Anda tidak dapat menganggap tim sebagai daftar pemain, tetapi Anda harus membuat model daftar pemain (saat ini aktif, tidak cedera, dll.) Sebagai atribut tim.
sumber
Biarkan saya menulis ulang pertanyaan Anda. sehingga Anda dapat melihat subjek dari perspektif yang berbeda.
Ketika saya perlu mewakili tim sepak bola, saya mengerti bahwa itu pada dasarnya adalah sebuah nama. Seperti: "Eagles"
Kemudian saya menyadari tim juga memiliki pemain.
Mengapa saya tidak bisa hanya memperpanjang jenis string sehingga juga menyimpan daftar pemain?
Titik masuk Anda ke masalah ini sewenang-wenang. Cobalah untuk berpikir apa yang tim memiliki (sifat), bukan apa yang .
Setelah Anda melakukannya, Anda bisa melihat apakah itu berbagi properti dengan kelas lain. Dan pikirkan tentang warisan.
sumber
Hanya karena saya pikir jawaban lain cukup banyak bersinggungan dengan apakah tim sepak bola "is-a"
List<FootballPlayer>
atau "has-a"List<FootballPlayer>
, yang benar-benar tidak menjawab pertanyaan ini sebagaimana tertulis.OP terutama meminta klarifikasi tentang pedoman untuk mewarisi dari
List<T>
:Karena
List<T>
tidak memiliki metode virtual. Ini tidak terlalu menjadi masalah dalam kode Anda sendiri, karena Anda biasanya dapat menonaktifkan implementasinya dengan sedikit rasa sakit - tetapi bisa menjadi masalah yang jauh lebih besar di API publik.API publik adalah antarmuka yang Anda tampilkan ke pemrogram pihak ketiga. Pikirkan kode kerangka kerja. Dan ingat bahwa pedoman yang direferensikan adalah ".NET Framework Design Guidelines" dan bukan ".NET Application Design Guidelines". Ada perbedaan, dan - secara umum - desain API publik jauh lebih ketat.
Cukup banyak, ya. Anda mungkin ingin mempertimbangkan alasan di baliknya untuk melihat apakah itu berlaku untuk situasi Anda, tetapi jika Anda tidak membangun API publik maka Anda tidak perlu terlalu khawatir tentang masalah API seperti versi (yang, ini adalah bagian).
Jika Anda menambahkan API publik di masa mendatang, Anda perlu mengambil abstrak API Anda dari implementasi Anda (dengan tidak mengekspos Anda
List<T>
secara langsung) atau melanggar pedoman dengan kemungkinan rasa sakit di masa depan yang mungkin timbul.Bergantung pada konteksnya, tetapi karena kita menggunakan
FootballTeam
sebagai contoh - bayangkan Anda tidak dapat menambahkanFootballPlayer
jika itu akan menyebabkan tim melewati batasan gaji. Cara yang mungkin untuk menambahkannya adalah seperti:Ah ... tetapi Anda tidak bisa
override Add
karena itu bukanvirtual
(untuk alasan kinerja).Jika Anda berada dalam suatu aplikasi (yang, pada dasarnya, berarti bahwa Anda dan semua penelepon Anda dikompilasi bersama) maka Anda sekarang dapat mengubah untuk menggunakan
IList<T>
dan memperbaiki segala kesalahan kompilasi:tetapi, jika Anda secara terbuka terpapar ke pihak ke-3 Anda baru saja membuat perubahan melanggar yang akan menyebabkan kesalahan kompilasi dan / atau runtime.
TL; DR - pedomannya adalah untuk API publik. Untuk API pribadi, lakukan apa yang Anda inginkan.
sumber
Apakah memungkinkan orang untuk mengatakan
masuk akal sama sekali? Jika tidak maka itu seharusnya bukan Daftar.
sumber
myTeam.subTeam(3, 5);
subList
bahkan tidak akan mengembalikanTeam
lagi.Itu tergantung pada perilaku objek "tim" Anda. Jika berperilaku seperti koleksi, mungkin OK untuk mewakilinya terlebih dahulu dengan Daftar biasa. Kemudian Anda mungkin mulai memperhatikan bahwa Anda terus menggandakan kode yang tertera pada daftar; pada titik ini Anda memiliki opsi untuk membuat objek FootballTeam yang membungkus daftar pemain. Kelas FootballTeam menjadi rumah bagi semua kode yang tertera di daftar pemain.
Enkapsulasi. Klien Anda tidak perlu tahu apa yang terjadi di dalam FootballTeam. Untuk semua klien Anda tahu, itu mungkin dilaksanakan dengan melihat daftar pemain di basis data. Mereka tidak perlu tahu, dan ini meningkatkan desain Anda.
Tepat :) Anda akan mengatakan footballTeam.Add (john), bukan footballTeam.List.Add (john). Daftar internal tidak akan terlihat.
sumber
Ada banyak jawaban bagus di sini, tetapi saya ingin menyentuh sesuatu yang tidak saya lihat disebutkan: Desain berorientasi objek adalah tentang memberdayakan objek .
Anda ingin merangkum semua aturan Anda, pekerjaan tambahan dan detail internal di dalam objek yang sesuai. Dengan cara ini objek lain yang berinteraksi dengan yang ini tidak perlu khawatir tentang itu semua. Bahkan, Anda ingin melangkah lebih jauh dan secara aktif mencegah objek lain melewati bagian dalam ini.
Ketika Anda mewarisi dari
List
, semua objek lain dapat melihat Anda sebagai Daftar. Mereka memiliki akses langsung ke metode untuk menambah dan menghapus pemain. Dan Anda akan kehilangan kendali; sebagai contoh:Misalkan Anda ingin membedakan ketika seorang pemain pergi dengan mengetahui apakah mereka pensiun, mengundurkan diri, atau dipecat. Anda bisa menerapkan
RemovePlayer
metode yang membutuhkan input enum yang sesuai. Namun, dengan mewarisi dariList
, Anda tidak akan dapat mencegah akses langsung keRemove
,RemoveAll
dan bahkanClear
. Akibatnya, Anda benar-benar melemahkanFootballTeam
kelas Anda .Pikiran tambahan tentang enkapsulasi ... Anda mengemukakan kekhawatiran berikut:
Anda benar, itu akan menjadi kata yang tidak perlu bagi semua klien untuk menggunakan tim Anda. Namun, masalah itu sangat kecil jika dibandingkan dengan fakta bahwa Anda telah terpapar
List Players
pada semuanya sehingga mereka dapat bermain-main dengan tim Anda tanpa persetujuan Anda.Anda selanjutnya berkata:
Anda salah tentang bit pertama: Jatuhkan kata 'daftar', dan sebenarnya jelas bahwa sebuah tim memiliki pemain.
Namun, Anda memukul kuku di kepala dengan yang kedua. Anda tidak ingin klien menelepon
ateam.Players.Add(...)
. Anda ingin mereka memanggilateam.AddPlayer(...)
. Dan implementasi Anda akan (mungkin antara lain) memanggil secaraPlayers.Add(...)
internal.Semoga Anda bisa melihat betapa pentingnya enkapsulasi terhadap tujuan memberdayakan objek Anda. Anda ingin memungkinkan setiap kelas untuk melakukan tugasnya dengan baik tanpa takut gangguan dari objek lain.
sumber
Ingat, "Semua model salah, tetapi beberapa berguna." - George EP Box
Tidak ada "cara yang benar", hanya yang bermanfaat.
Pilih satu yang bermanfaat bagi Anda dan / pengguna Anda. Itu dia.
Berkembang secara ekonomi, jangan over-engineer. Semakin sedikit kode yang Anda tulis, semakin sedikit kode yang perlu Anda debug.(baca edisi berikut).- Diedit
Jawaban terbaik saya adalah ... itu tergantung. Mewarisi dari Daftar akan mengekspos klien kelas ini dengan metode yang mungkin tidak boleh diungkapkan, terutama karena FootballTeam terlihat seperti entitas bisnis.
- Edisi 2
Saya dengan tulus tidak ingat dengan apa yang saya maksudkan pada komentar "jangan over-engineer". Sementara saya percaya bahwa pola pikir KISS adalah panduan yang baik, saya ingin menekankan bahwa mewarisi kelas bisnis dari Daftar akan menciptakan lebih banyak masalah daripada yang dipecahkan, karena kebocoran abstraksi .
Di sisi lain, saya percaya ada sejumlah kasus di mana hanya untuk mewarisi dari Daftar berguna. Seperti yang saya tulis di edisi sebelumnya, itu tergantung. Jawaban untuk setiap kasus sangat dipengaruhi oleh pengetahuan, pengalaman dan preferensi pribadi.
Terima kasih kepada @ kai karena telah membantu saya berpikir lebih tepat tentang jawabannya.
sumber
List
akan mengekspos klien kelas ini ke metode yang mungkin tidak boleh diekspos " adalah persis apa yang membuat mewarisi dariList
mutlak tidak-tidak. Setelah terekspos, mereka secara bertahap disalahgunakan dan salah digunakan seiring waktu. Menghemat waktu sekarang dengan "berkembang secara ekonomi" dapat dengan mudah menyebabkan sepuluh kali lipat penghematan yang hilang di masa depan: men-debug pelanggaran dan akhirnya refactoring untuk memperbaiki warisan. Prinsip YAGNI juga dapat dianggap sebagai makna: Anda Tidak Akan Perlu semua metode dariList
, jadi jangan memaparkannya.List
karena cepat dan mudah dan dengan demikian akan menyebabkan lebih sedikit bug jelas salah, itu hanya desain yang buruk dan desain yang buruk menyebabkan lebih banyak bug daripada "over engineering".Ini mengingatkan saya pada tradeoff "Is a" versus "have". Terkadang lebih mudah dan lebih masuk akal untuk mewarisi langsung dari kelas super. Lain kali lebih masuk akal untuk membuat kelas mandiri dan menyertakan kelas yang akan Anda warisi sebagai variabel anggota. Anda masih dapat mengakses fungsionalitas kelas tetapi tidak terikat dengan antarmuka atau kendala lain yang mungkin berasal dari mewarisi dari kelas.
Yang kamu lakukan Seperti banyak hal ... itu tergantung pada konteksnya. Panduan yang akan saya gunakan adalah bahwa untuk mewarisi dari kelas lain benar-benar harus ada hubungan "adalah". Jadi jika Anda menulis kelas yang disebut BMW, itu bisa mewarisi dari Mobil karena BMW benar-benar mobil. Kelas Kuda dapat mewarisi dari kelas Mamalia karena kuda sebenarnya adalah mamalia dalam kehidupan nyata dan fungsi Mamalia apa pun harus relevan dengan Kuda. Tetapi dapatkah Anda mengatakan bahwa tim adalah daftar? Dari apa yang bisa saya katakan, sepertinya bukan Tim yang benar-benar "adalah" Daftar. Jadi dalam hal ini, saya akan memiliki Daftar sebagai variabel anggota.
sumber
Rahasia kotor saya: Saya tidak peduli apa yang orang katakan, dan saya melakukannya. .NET Framework disebarkan dengan "XxxxCollection" (UIElementCollection untuk contoh teratas di kepala saya).
Jadi apa yang menghentikan saya berkata:
Ketika saya menemukannya lebih baik daripada
Selain itu, PlayerCollection saya dapat digunakan oleh kelas lain, seperti "Klub" tanpa duplikasi kode.
Praktik terbaik kemarin, mungkin bukan yang akan datang. Tidak ada alasan di balik praktik terbaik, sebagian besar hanya kesepakatan luas di antara masyarakat. Alih-alih bertanya kepada komunitas apakah itu akan menyalahkan Anda ketika Anda bertanya pada diri sendiri, apa yang lebih mudah dibaca dan dikelola?
atau
Betulkah. Apakah Anda ragu? Sekarang mungkin Anda harus bermain dengan kendala teknis lainnya yang mencegah Anda menggunakan Daftar <T> dalam kasus penggunaan nyata Anda. Tapi jangan tambahkan kendala yang seharusnya tidak ada. Jika Microsoft tidak mendokumentasikan alasannya, maka itu pasti merupakan "praktik terbaik" yang datang entah dari mana.
sumber
Collection<T>
tidak sama denganList<T>
itu mungkin memerlukan sedikit kerja untuk memverifikasi dalam proyek besar-ish.team.ByName("Nicolas")
berarti"Nicolas"
adalah nama tim.Apa yang dikatakan pedoman adalah bahwa API publik tidak boleh mengungkapkan keputusan desain internal apakah Anda menggunakan daftar, set, kamus, pohon atau apa pun. "Tim" belum tentu daftar. Anda dapat mengimplementasikannya sebagai daftar tetapi pengguna API publik Anda harus menggunakan kelas Anda berdasarkan kebutuhan untuk mengetahui. Ini memungkinkan Anda mengubah keputusan dan menggunakan struktur data yang berbeda tanpa memengaruhi antarmuka publik.
sumber
class FootballTeam : List<FootballPlayers>
, pengguna kelas saya akan dapat memberi tahu saya ' telah diwarisi dariList<T>
dengan menggunakan refleksi, melihatList<T>
metode yang tidak masuk akal bagi sebuah tim sepak bola, dan mampu menggunakanFootballTeam
dalamList<T>
, jadi saya akan menjadi mengungkapkan rincian implementasi untuk klien (tidak perlu).Ketika mereka mengatakan
List<T>
"dioptimalkan", saya pikir mereka ingin berarti itu tidak memiliki fitur seperti metode virtual yang sedikit lebih mahal. Jadi masalahnya adalah bahwa setelah Anda mengeksposList<T>
di Anda API umum , Anda kehilangan kemampuan untuk menegakkan aturan bisnis atau menyesuaikan fungsi nanti. Tetapi jika Anda menggunakan kelas bawaan ini sebagai internal dalam proyek Anda (sebagai lawan dari kemungkinan terpapar ke ribuan pelanggan / mitra / tim Anda sebagai API) maka mungkin OK jika menghemat waktu Anda dan itu adalah fungsi yang ingin Anda gunakan. duplikat. Keuntungan dari mewarisi dariList<T>
adalah Anda menghilangkan banyak kode pembungkus bisu yang tidak akan pernah dikustomisasi di masa mendatang. Juga jika Anda ingin kelas Anda secara eksplisit memiliki semantik yang sama persisList<T>
untuk kehidupan API Anda maka mungkin juga OK.Saya sering melihat banyak orang melakukan banyak pekerjaan tambahan hanya karena aturan FxCop mengatakan demikian atau blog seseorang mengatakan itu adalah praktik "buruk". Sering kali, ini mengubah kode untuk merancang pola keanehan palooza. Seperti halnya banyak pedoman, perlakukan sebagai pedoman yang dapat memiliki pengecualian.
sumber
Walaupun saya tidak memiliki perbandingan yang kompleks seperti kebanyakan jawaban ini, saya ingin membagikan metode saya untuk menangani situasi ini. Dengan memperluas
IEnumerable<T>
, Anda dapat mengizinkanTeam
kelas Anda untuk mendukung ekstensi permintaan Linq, tanpa secara terbuka memperlihatkan semua metode dan propertiList<T>
.sumber
Jika pengguna kelas Anda membutuhkan semua metode dan properti ** yang dimiliki Daftar, Anda harus mengambil kelas Anda darinya. Jika mereka tidak membutuhkannya, lampirkan Daftar dan buat pembungkus untuk metode yang benar-benar dibutuhkan pengguna kelas Anda.
Ini adalah aturan ketat, jika Anda menulis API publik , atau kode lain apa pun yang akan digunakan oleh banyak orang. Anda dapat mengabaikan aturan ini jika Anda memiliki aplikasi kecil dan tidak lebih dari 2 pengembang. Ini akan menghemat waktu Anda.
Untuk aplikasi kecil, Anda juga dapat mempertimbangkan untuk memilih bahasa lain yang tidak terlalu ketat. Ruby, JavaScript - apa pun yang memungkinkan Anda menulis lebih sedikit kode.
sumber
Saya hanya ingin menambahkan bahwa Bertrand Meyer, penemu Eiffel dan desain dengan kontrak, akan
Team
mewarisi dariList<Player>
tanpa banyak mengedipkan kelopak mata.Dalam bukunya, Object-Oriented Software Construction , ia membahas implementasi sistem GUI di mana jendela persegi panjang dapat memiliki jendela anak. Dia hanya memiliki
Window
warisan dari keduanyaRectangle
danTree<Window>
menggunakan kembali implementasi.Namun, C # bukan Eiffel. Yang terakhir mendukung banyak pewarisan dan penggantian nama fitur . Di C #, ketika Anda subkelas, Anda mewarisi antarmuka dan implementasi. Anda dapat mengesampingkan implementasinya, tetapi konvensi pemanggilan disalin langsung dari superclass. Namun, di Eiffel, Anda dapat memodifikasi nama metode publik, sehingga Anda dapat mengganti nama
Add
danRemove
menjadiHire
danFire
di AndaTeam
. Jika instance dariTeam
disadap kembali keList<Player>
, pemanggil akan menggunakanAdd
danRemove
memodifikasinya, tetapi metode virtual AndaHire
danFire
akan dipanggil.sumber
Saya pikir saya tidak setuju dengan generalisasi Anda. Sebuah tim bukan hanya kumpulan pemain. Sebuah tim memiliki lebih banyak informasi tentangnya - nama, lambang, kumpulan staf manajemen / admin, kumpulan kru pelatihan, kemudian koleksi pemain. Jadi, dengan benar, kelas FootballTeam Anda harus memiliki 3 koleksi dan tidak dengan sendirinya menjadi koleksi; jika ingin memodelkan dunia nyata dengan benar.
Anda dapat mempertimbangkan kelas PlayerCollection yang seperti StringCollection Khusus menawarkan beberapa fasilitas lain - seperti validasi dan pemeriksaan sebelum objek ditambahkan atau dihapus dari toko internal.
Mungkin, gagasan atasan PlayerCollection sesuai dengan pendekatan pilihan Anda?
Dan FootballTeam dapat terlihat seperti ini:
sumber
Lebih suka Antarmuka daripada Kelas
Kelas harus menghindari berasal dari kelas dan bukannya mengimplementasikan antarmuka minimal yang diperlukan.
Pewarisan memecah Enkapsulasi
Berasal dari istirahat kelas enkapsulasi :
Di antara hal-hal lain ini mempersulit untuk memperbaiki kode Anda.
Kelas adalah Detail Implementasi
Kelas adalah detail implementasi yang harus disembunyikan dari bagian lain dari kode Anda. Singkatnya
System.List
adalah implementasi spesifik dari tipe data abstrak, yang mungkin atau mungkin tidak sesuai sekarang dan di masa depan.Secara konseptual, fakta bahwa
System.List
tipe data disebut "daftar" adalah sedikit ikan haring merah. ASystem.List<T>
adalah kumpulan pesanan yang dapat diubah yang mendukung operasi diamortisasi O (1) untuk menambah, menyisipkan, dan menghapus elemen, dan operasi O (1) untuk mengambil jumlah elemen atau mendapatkan dan mengatur elemen berdasarkan indeks.Semakin Kecil Antarmuka, semakin Fleksibel Kode
Saat mendesain struktur data, semakin sederhana antarmuka, semakin fleksibel kodenya. Lihat saja seberapa kuat LINQ untuk demonstrasi ini.
Cara Memilih Antarmuka
Ketika Anda berpikir "daftar" Anda harus mulai dengan mengatakan pada diri sendiri, "Saya perlu mewakili koleksi pemain baseball". Jadi katakanlah Anda memutuskan untuk memodelkan ini dengan kelas. Apa yang harus Anda lakukan pertama adalah memutuskan apa jumlah minimal antarmuka yang perlu diekspos oleh kelas ini.
Beberapa pertanyaan yang dapat membantu memandu proses ini:
IEnumerable<T>
IReadonlyList<T>
.ICollection<T>
ISet<T>
?IList<T>
.Dengan cara ini Anda tidak akan menggabungkan bagian lain dari kode ke detail implementasi dari koleksi pemain baseball Anda dan akan bebas untuk mengubah cara penerapannya selama Anda menghormati antarmuka.
Dengan mengambil pendekatan ini, Anda akan menemukan bahwa kode menjadi lebih mudah dibaca, refactor, dan digunakan kembali.
Catatan tentang Menghindari Boilerplate
Menerapkan antarmuka dalam IDE modern harus mudah. Klik kanan dan pilih "Implementasikan Antarmuka". Kemudian teruskan semua implementasi ke kelas anggota jika perlu.
Yang mengatakan, jika Anda menemukan Anda sedang menulis banyak boilerplate, itu berpotensi karena Anda mengekspos lebih banyak fungsi daripada yang seharusnya. Itu adalah alasan yang sama Anda tidak boleh mewarisi dari kelas.
Anda juga dapat mendesain antarmuka yang lebih kecil yang masuk akal untuk aplikasi Anda, dan mungkin hanya beberapa fungsi ekstensi pembantu untuk memetakan antarmuka tersebut ke yang lain yang Anda butuhkan. Ini adalah pendekatan yang saya ambil di
IArray
antarmuka saya sendiri untuk pustaka LinqArray .sumber
Masalah dengan serialisasi
Satu aspek hilang. Kelas yang mewarisi dari Daftar <> tidak dapat diserialisasi dengan benar menggunakan XmlSerializer . Dalam hal itu DataContractSerializer harus digunakan sebagai gantinya, atau implementasi serialisasi sendiri diperlukan.
Bacaan lebih lanjut: Ketika sebuah kelas diwarisi dari Daftar <>, XmlSerializer tidak membuat serialisasi atribut lainnya
sumber
Mengutip Eric Lippert:
Misalnya, Anda bosan dengan tidak adanya
AddRange
metode diIList<T>
:sumber