Iterasi kelas yang mewakili koleksi: IEnumerable <T> vs metode kustom


9

Saya sering menemukan diri saya perlu mengimplementasikan kelas yang merupakan enumerasi / koleksi sesuatu. Pertimbangkan untuk thread ini dengan contoh buat dari IniFileContentyang 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 IniFileContentantarmuka, 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();
}


2
Memilih untuk menutup ini karena pertanyaan yang diperbarui meminta pendapat tentang dua gaya pengkodean dan sekarang sepenuhnya berdasarkan pendapat.
David Arno

1
@DavidArno Saya tidak setuju dalam arti bahwa menanyakan apakah sesuatu itu praktik yang baik tidak sepenuhnya didasarkan pada pendapat tetapi juga pada fakta, pengalaman, dan norma.
Terlihat

Tidak ada pola yang bekerja untuk saya: ketergantungan pada kelas beton, ketergantungan keras pada sistem file, konvensi pembuangan ambigu (saya percaya Anda mungkin memiliki kebocoran, Pak), semantik berlawanan menggunakan newuntuk 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 .
John Wu

@ JohnWu Tolong pertimbangkan ini adalah contoh yang dibuat-buat (dipilih dengan canggung aku akui). Jika Anda ingin menjawab pertanyaan ini (tidak berusaha bersikap kasar juga) pertimbangkan untuk fokus pada keputusan desain mengimplementasikan IEnumerable atau tidak untuk kelas yang menjadi IniFileContent.
Terlihat

Jawaban:


13

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.


Saya memilih untuk mengimplementasikan IEnumerable<string>karena membuat penggunaannya nyaman

Kenyamanan seharusnya tidak melebihi kebenaran.

Saya ingin tahu apakah MyFilekelas Anda akan mengandung lebih banyak logika daripada ini; karena itu akan mempengaruhi kebenaran jawaban ini. Saya terutama tertarik pada:

.Where(l => ...) //some business logic for filtering

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.


public class MyFile : IEnumerable<string>

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 MyFilekelas Anda tidak akan pernah mengandung lebih banyak logika daripada hanya enumerasi baris ini (dan Wheresatu - 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 sebagai FileContent. Itu mempertahankan sintaks yang Anda inginkan:

foreach(var line in new FileContent(@"C:\Folder\File.txt"))

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 MyFilekelas Anda akan mengandung lebih banyak logika, situasinya berubah. Ada beberapa cara ini bisa terjadi:

  • Anda mulai menggunakan kelas ini untuk mewakili metadata file, bukan hanya isinya.

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.

  • The 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 FileTypedari sekadar a FileContent. Kedua perilaku perlu dipisahkan, atau digabungkan menggunakan komposisi (yang mendukung MyFile2pendekatan Anda ), atau lebih disukai keduanya (kelas yang terpisah untuk FileTypedan FileContentperilaku, dan kemudian keduanya dikomposisikan ke dalam MyFilekelas).


Saran yang sama sekali berbeda.

Seperti berdiri, baik Anda MyFiledan MyFile2ada 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.

Selain itu, saya tidak mengerti mengapa Anda memilih untuk membuat kelas Anda sealed. Jika ada, saya berharap pewarisan akan menjadi fitur terbesarnya: kelas turunan yang berbeda dengan logika penyaringan yang berbeda (dengan asumsi bahwa itu lebih kompleks daripada perubahan nilai sederhana, karena warisan tidak boleh digunakan hanya untuk mengubah nilai tunggal)

Saya akan menulis ulang seluruh kelas Anda sebagai:

foreach(var line in File.ReadLines(...).Where(l => ...))

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 mengimplementasikan File.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:

public static IEnumerable<string> GetFilteredFileContent(string filePath)
{
    return File.ReadLines(filePath).Where(l => ...);
}

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:

public static class MyFile
{
    public static Func<string, bool> IgnoreComments = 
                  (l => !l.StartsWith("#"));

    public static Func<string, bool> OnlyTakeComments = 
                  (l => l.StartsWith("#"));

    public static Func<string, bool> IgnoreLinesWithTheLetterE = 
                  (l => !l.ToLower().contains("e"));

    public static Func<string, bool> OnlyTakeLinesWithTheLetterE = 
                  (l => l.ToLower().contains("e"));

    public static IEnumerable<string> ReadLines(string filePath, params Func<string, bool>[] filters)
    {
        var lines = File.ReadLines(filePath).Where(l => ...);

        foreach(var filter in filters)
            lines = lines.Where(filter);

        return lines;
    }
}

Dan penggunaannya:

MyFile.ReadLines("path", MyFile.IgnoreComments, MyFile.OnlyTakeLinesWithTheLetterE);

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.


melihat new X().Y()rasanya ada yang salah dengan desain kelas)

Dalam pendekatan Anda, Anda bisa membuatnya new X().Yyang 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.


Saya sangat menghargai tanggapan Anda dan sebagian besar pemikiran Anda yang membuat Anda mengubah nama kelas menjadi 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.
Terlihat

@Flater, bahkan jika GetFilteredContent(string filename)dieksekusi dalam kode dengan nama file sebagian besar waktu, saya akan menempatkan tubuh utama dari pekerjaan dalam metode yang mengambil Streamatau TextReadersehingga membuat pengujian jauh lebih mudah. Jadi GetFilteredContent(string)akan menjadi pembungkus GetFilteredContent(TextReader reader). Tapi A setuju dengan penilaian Anda.
Berin Loritsch

4

Menurut saya, masalah dengan kedua pendekatan itu adalah:

  1. Anda mengenkapsulasi File.ReadLines, yang membuat pengujian unit lebih sulit dari yang seharusnya,
  2. Sebuah instance kelas baru harus dibuat setiap kali file tersebut disebutkan, hanya untuk menyimpan path sebagai _filepath.

Jadi saya sarankan membuatnya menjadi metode statis, baik yang lewat IEnumerable<string>atau Streammewakili konten file:

public static GetFilteredLines(IEnumerable<string> fileContents)
    => fileContents.Where(l => ...);

Kemudian disebut via:

var filteredLines = GetFilteredLines(File.ReadLines(filePath));

Ini menghindari menempatkan beban yang tidak perlu pada heap dan membuatnya jauh lebih mudah untuk unit test metode ini.


Saya setuju dengan Anda, namun itu sama sekali bukan jenis umpan balik yang saya harapkan (pertanyaan saya dirumuskan dengan buruk dalam pengertian itu). Lihat pertanyaan saya yang diedit.
Terlihat

@ Spotted, wow, apakah Anda benar-benar meremehkan jawaban atas pertanyaan Anda karena Anda mengajukan pertanyaan yang salah? Itu rendah.
David Arno

Ya saya lakukan sampai saya menyadari masalahnya adalah pertanyaan saya yang dirumuskan dengan buruk. Kecuali bahwa sekarang saya tidak dapat membatalkan downvote saya selama jawaban Anda tidak diedit ...: - / Harap terima permintaan maaf saya (atau dummy edit jawaban Anda sehingga saya dengan senang hati menghapus downvote saya).
Terlihat

@Spotted, masalah dengan pertanyaan Anda sekarang adalah bahwa Anda meminta pendapat tentang dua gaya pengkodean, menjadikan pertanyaan di luar topik. Bahkan dengan pertanyaan Anda yang diperbarui, jawaban saya tetap sama: kedua desain tersebut cacat karena dua alasan yang saya tentukan dan oleh karena itu tidak ada solusi yang baik dalam pandangan saya.
David Arno

Saya sepenuhnya setuju bahwa desain dalam contoh saya cacat tetapi bukan poin dari pertanyaan saya (karena ini adalah contoh yang dibuat-buat), itu hanya alasan untuk memperkenalkan kedua pendekatan ini.
Terlihat

2

Contoh dunia nyata yang Anda berikan, DueInvoicescocok 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:

public class Invoices
{
    // ... skip all the other stuff about Invoices

    public IEnumerable<Invoice> GetDueItems()
    {
         foreach(var line in File.ReadLines(_invoicesFile))
         {
             var invoice = ReadInvoiceFrom(line);
             if (invoice.PaymentDue <= DateTime.UtcNow)
             {
                 yield return invoice;
             }
         }
    }
}

The yield returnbekerja ketika Anda tidak bisa hanya membungkus query LINQ, atau embedding logika lebih mudah untuk mengikuti. Opsi lainnya adalah mengembalikan kueri LINQ:

public class Invoices
{
    // ... skip all the other stuff about invoices

    public IEnumerable<Invoice> GetDueItems()
    {
        return from Invoice invoice in GetAllItems()
               where invoice.PaymentDue <= DateTime.UtcNow
               select invoice;
    }
}

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 saya IEnumerator<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.


Alasan mengapa saya membuat kelas terpisah adalah yang InvoiceDtoberasal dari lapisan persisten dan dengan demikian hanyalah sekumpulan data, saya tidak ingin mengacaukannya dengan metode yang relevan dengan bisnis. Karenanya penciptaan DueInvoicedan DueInvoices.
Terlihat

@Spotted, Tidak harus kelas DTO itu sendiri. Heck, itu mungkin kelas yang statis dengan metode ekstensi. Yang saya sarankan adalah bahwa dalam kebanyakan kasus Anda dapat meminimalkan kode boilerplate Anda, dan membuat API dapat dicerna pada saat yang sama.
Berin Loritsch

0

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:

public IEnumerable<string> GetFilteredContents() { ... }

foreach(string line in myFile.GetFilteredContents() { ... }

Anda benar tentang hubungan antara file dan kontennya. Contohnya tidak dipikirkan dengan baik. Saya melakukan beberapa pengeditan untuk pertanyaan awal saya untuk menajamkan pikiran saya.
Terlihat

Terima permintaan maaf saya untuk downvote yang tidak dapat dibenarkan, namun saya tidak dapat menghapusnya kecuali jika Anda mengedit jawaban Anda. : - /
Terlihat
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.