Saya sering menemukan diri saya perlu mengimplementasikan kelas yang merupakan enumerasi / koleksi sesuatu. Pertimbangkan untuk thread ini dengan contoh buat dari IniFileContent
yang merupakan pencacahan / kumpulan garis.
Alasan kelas ini harus ada dalam basis kode saya adalah bahwa saya ingin menghindari logika bisnis yang tersebar di semua tempat (= merangkum where
) dan saya ingin melakukannya dengan cara yang paling berorientasi objek mungkin.
Biasanya saya akan mengimplementasikannya seperti di bawah ini:
public sealed class IniFileContent : IEnumerable<string>
{
private readonly string _filepath;
public IniFileContent(string filepath) => _filepath = filepath;
public IEnumerator<string> GetEnumerator()
{
return File.ReadLines(_filepath)
.Where(l => !l.StartsWith(";"))
.GetEnumerator();
}
public IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Saya memilih untuk menerapkan IEnumerable<string>
karena membuat penggunaannya nyaman:
foreach(var line in new IniFileContent(...))
{
//...
}
Namun saya bertanya-tanya apakah hal itu "membayangi" niat kelas? Ketika seseorang melihat IniFileContent
antarmuka, ia hanya akan melihat Enumerator<string> GetEnumerator()
. Saya pikir itu membuat tidak jelas layanan mana yang sebenarnya disediakan kelas.
Pertimbangkan implementasi kedua ini:
public sealed class IniFileContent2
{
private readonly string _filepath;
public IniFileContent2(string filepath) => _filepath = filepath;
public IEnumerable<string> Lines()
{
return File.ReadLines(_filepath)
.Where(l => !l.StartsWith(";"));
}
}
Yang digunakan kurang nyaman (omong-omong, melihat new X().Y()
terasa seperti ada yang salah dengan desain kelas):
foreach(var line in new IniFileContent2(...).Lines())
{
//...
}
Tetapi dengan antarmuka yang IEnumerable<string> Lines()
jelas membuat jelas apa yang sebenarnya dapat dilakukan oleh kelas ini.
Implementasi mana yang akan Anda bina dan mengapa? Tersirat, apakah itu praktik yang baik untuk mengimplementasikan IEnumerable untuk mewakili enumerasi sesuatu?
Saya tidak mencari jawaban tentang cara:
- unit uji kode ini
- membuat fungsi statis, bukan kelas
- membuat kode ini lebih rentan terhadap evolusi logika bisnis di masa depan
- mengoptimalkan kinerja
Lampiran
Berikut adalah jenis kode nyata yang tinggal di basis kode saya yang mengimplementasikanIEnumerable
public class DueInvoices : IEnumerable<DueInvoice>
{
private readonly IEnumerable<InvoiceDto> _invoices;
private readonly IEnumerable<ReminderLevel> _reminderLevels;
public DueInvoices(IEnumerable<InvoiceDto> invoices, IEnumerable<ReminderLevel> reminderLevels)
{
_invoices = invoices;
_reminderLevels = reminderLevels;
}
public IEnumerator<DueInvoice> GetEnumerator() => _invoices.Where(invoice => invoice.DueDate < DateTime.Today && !invoice.Paid)
.Select(invoice => new DueInvoice(invoice, _reminderLevels))
.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
new
untuk kasus penggunaan ini, kesulitan dalam pengujian unit, ambigu ketika saya / O pengecualian dapat terjadi, dll. Maaf, saya tidak berusaha bersikap kasar. Saya pikir saya sudah terlalu terbiasa dengan manfaat injeksi ketergantungan .IniFileContent
.Jawaban:
Saya meninjau pendekatan Anda sebelum menyarankan pendekatan yang sama sekali berbeda. Saya lebih suka pendekatan yang berbeda tetapi tampaknya penting untuk menjelaskan mengapa pendekatan Anda memiliki kekurangan.
Kenyamanan seharusnya tidak melebihi kebenaran.
Saya ingin tahu apakah
MyFile
kelas Anda akan mengandung lebih banyak logika daripada ini; karena itu akan mempengaruhi kebenaran jawaban ini. Saya terutama tertarik pada:karena jika ini cukup kompleks atau dinamis, Anda menyembunyikan logika itu di kelas yang namanya tidak mengungkapkan bahwa itu menyaring kontennya sebelum menyajikannya kepada konsumen.
Sebagian dari saya berharap / berasumsi bahwa logika filter ini dimaksudkan untuk dikodekan (mis. Filter sederhana yang mengabaikan baris yang dikomentari, misalnya seperti bagaimana file .ini menganggap baris yang diawali dengan
#
menjadi komentar) dan bukan aturan khusus file.Ada sesuatu yang sangat kisi tentang memiliki singular (
File
) mewakili jamak (IEnumerable
). File adalah entitas tunggal. Ini terdiri lebih dari sekedar kontennya. Ini juga berisi metadata (nama file, ekstensi, tanggal pembuatan, ubah tanggal, ...).Manusia lebih dari jumlah anak-anaknya. Mobil lebih dari jumlah bagian-bagiannya. Sebuah lukisan lebih dari sekadar koleksi cat dan kanvas. Dan file lebih dari sekadar kumpulan baris.
Jika saya berasumsi bahwa
MyFile
kelas Anda tidak akan pernah mengandung lebih banyak logika daripada hanya enumerasi baris ini (danWhere
satu - satunya menerapkan filter hardcode statis sederhana), maka apa yang Anda dapatkan di sini adalah penggunaan nama "file" yang membingungkan dan peruntukan yang dimaksud . Ini dapat dengan mudah diperbaiki dengan mengganti nama kelas sebagaiFileContent
. Itu mempertahankan sintaks yang Anda inginkan:Ini juga lebih masuk akal dari sudut pandang semantik. Konten file dapat dipecah menjadi beberapa baris terpisah. Ini masih mengasumsikan bahwa konten file adalah teks dan bukan biner, tetapi itu cukup adil.
Namun, jika
MyFile
kelas Anda akan mengandung lebih banyak logika, situasinya berubah. Ada beberapa cara ini bisa terjadi:Ketika Anda mulai melakukan ini, maka file tersebut mewakili file dalam direktori , yang lebih dari sekadar isinya.
Pendekatan yang benar di sini adalah apa yang telah Anda lakukan
MyFile2
.Where()
Filter mulai memiliki logika penyaring rumit yang tidak hardcoded, misalnya ketika file yang berbeda mulai disaring berbeda.Ketika Anda mulai melakukan ini, file mulai memiliki identitasnya sendiri, karena mereka memiliki filter kustom sendiri. Ini berarti bahwa kelas Anda lebih
FileType
dari sekadar aFileContent
. Kedua perilaku perlu dipisahkan, atau digabungkan menggunakan komposisi (yang mendukungMyFile2
pendekatan Anda ), atau lebih disukai keduanya (kelas yang terpisah untukFileType
danFileContent
perilaku, dan kemudian keduanya dikomposisikan ke dalamMyFile
kelas).Saran yang sama sekali berbeda.
Seperti berdiri, baik Anda
MyFile
danMyFile2
ada murni untuk memberi Anda pembungkus di sekitar.Where(l => ...)
filter Anda . Kedua, Anda secara efektif membuat kelas untuk membungkus metode statis (File.ReadLines()
), yang bukan pendekatan yang bagus.Saya akan menulis ulang seluruh kelas Anda sebagai:
Satu-satunya kelemahan dari pendekatan yang disederhanakan ini adalah Anda harus mengulangi
Where()
filter setiap kali Anda ingin mengakses konten file. Saya setuju bahwa itu tidak diinginkan.Namun, tampaknya berlebihan bahwa ketika Anda ingin membuat
Where(l => ...)
pernyataan yang dapat digunakan kembali , Anda kemudian juga memaksa kelas itu untuk mengimplementasikanFile.ReadLines(...)
. Anda mengumpulkan lebih banyak dari yang sebenarnya Anda butuhkan.Alih-alih mencoba untuk membungkus metode statis di kelas kustom, saya pikir itu jauh lebih cocok jika Anda membungkusnya dengan metode statis sendiri:
Dengan asumsi Anda memiliki filter yang berbeda, Anda dapat meneruskan filter yang sesuai sebagai parameter. Saya akan menunjukkan kepada Anda sebuah contoh yang dapat menangani beberapa filter, yang seharusnya dapat menangani semua yang Anda perlukan sambil memaksimalkan penggunaan kembali:
Dan penggunaannya:
Ini hanya contoh pabrik yang dimaksudkan untuk membuktikan bahwa metode statis lebih masuk akal daripada membuat kelas di sini.
Jangan terjebak pada spesifikasi penerapan filter. Anda dapat mengimplementasikannya sesuka Anda (saya pribadi suka parametrizing
Func<>
karena sifatnya yang dapat diperluas dan kemampuan beradaptasi dengan refactoring). Tetapi karena Anda sebenarnya bukan contoh dari filter yang ingin Anda gunakan, saya membuat beberapa asumsi untuk menunjukkan kepada Anda contoh yang bisa diterapkan.Dalam pendekatan Anda, Anda bisa membuatnya
new X().Y
yang kurang kisi-kisi.Namun, saya berpikir bahwa ketidaksukaan Anda
new X().Y()
membuktikan bahwa Anda merasa seperti kelas tidak dibenarkan di sini, tetapi sebuah metode adalah; yang hanya bisa diwakili tanpa kelas dengan menjadi statis.sumber
FileContent
. Ini menunjukkan betapa buruknya contoh saya. Juga sangat buruk bagaimana saya merumuskan pertanyaan saya yang sama sekali gagal mengumpulkan umpan balik seperti yang saya harapkan. Saya telah mengeditnya dengan harapan untuk membuat maksud saya lebih jelas.GetFilteredContent(string filename)
dieksekusi dalam kode dengan nama file sebagian besar waktu, saya akan menempatkan tubuh utama dari pekerjaan dalam metode yang mengambilStream
atauTextReader
sehingga membuat pengujian jauh lebih mudah. JadiGetFilteredContent(string)
akan menjadi pembungkusGetFilteredContent(TextReader reader)
. Tapi A setuju dengan penilaian Anda.Menurut saya, masalah dengan kedua pendekatan itu adalah:
File.ReadLines
, yang membuat pengujian unit lebih sulit dari yang seharusnya,_filepath
.Jadi saya sarankan membuatnya menjadi metode statis, baik yang lewat
IEnumerable<string>
atauStream
mewakili konten file:Kemudian disebut via:
Ini menghindari menempatkan beban yang tidak perlu pada heap dan membuatnya jauh lebih mudah untuk unit test metode ini.
sumber
Contoh dunia nyata yang Anda berikan,
DueInvoices
cocok dengan konsep bahwa ini adalah kumpulan faktur yang saat ini jatuh tempo. Saya mengerti sepenuhnya bagaimana contoh-contoh yang dibuat-buat dapat membuat orang sibuk dengan istilah-istilah yang Anda gunakan vs. konsep yang Anda coba sampaikan. Saya telah berada di ujung frustrasi itu sendiri beberapa kali.Yang mengatakan, jika tujuan kelas adalah untuk menjadi
IEnumerable<T>
, dan tidak memberikan logika lain, saya harus mengajukan pertanyaan apakah Anda memerlukan seluruh kelas atau hanya dapat memberikan metode dari kelas lain. Sebagai contoh:The
yield return
bekerja ketika Anda tidak bisa hanya membungkus query LINQ, atau embedding logika lebih mudah untuk mengikuti. Opsi lainnya adalah mengembalikan kueri LINQ:Dalam kedua kasus ini Anda tidak perlu kelas pembungkus penuh. Anda hanya perlu memberikan metode dan iterator pada dasarnya ditangani untuk Anda.
Satu-satunya waktu di mana saya membutuhkan kelas penuh untuk menangani iterasi adalah ketika saya harus mengeluarkan gumpalan dari database dalam permintaan yang berjalan lama. Utilitas adalah untuk ekstraksi satu kali sehingga kami dapat memigrasi data di tempat lain. Ada beberapa keanehan yang saya temui dengan database ketika saya mencoba untuk streaming konten menggunakan
yield return
. Tapi itu hilang ketika saya benar-benar menerapkan kebiasaan sayaIEnumerator<T>
untuk kontrol yang lebih baik ketika sumber daya dibersihkan. Ini adalah pengecualian daripada aturannya.Jadi singkatnya, saya sarankan untuk tidak mengimplementasikan
IEnumerable<T>
secara langsung jika masalah Anda dapat diselesaikan dengan salah satu cara yang dijelaskan dalam kode di atas. Simpan overhead untuk membuat enumerator secara eksplisit ketika Anda tidak bisa menyelesaikan masalah dengan cara lain.sumber
InvoiceDto
berasal dari lapisan persisten dan dengan demikian hanyalah sekumpulan data, saya tidak ingin mengacaukannya dengan metode yang relevan dengan bisnis. Karenanya penciptaanDueInvoice
danDueInvoices
.Pikirkan konsep. Apa hubungan antara file dan isinya? Itu hubungan "memiliki" , bukan hubungan "adalah" .
Akibatnya, kelas file harus memiliki metode / properti untuk mengembalikan konten. Dan itu masih mudah untuk dipanggil:
sumber