Kapan harus Lancar dalam C #?

78

Dalam banyak hal saya benar-benar menyukai gagasan antarmuka Fluent, tetapi dengan semua fitur modern C # (inisialisasi, lambdas, parameter bernama) Saya mendapati diri saya berpikir, "apakah itu layak?", Dan "Apakah ini pola yang tepat untuk menggunakan?". Adakah yang bisa memberi saya, jika bukan praktik yang diterima, setidaknya pengalaman mereka sendiri atau matriks keputusan tentang kapan harus menggunakan pola Fasih?

Kesimpulan:

Beberapa aturan praktis yang baik dari jawaban sejauh ini:

  • Antarmuka yang lancar sangat membantu ketika Anda memiliki lebih banyak tindakan daripada setter, karena panggilan mendapat manfaat lebih banyak dari pass-through konteks.
  • Antarmuka yang lancar harus dianggap sebagai lapisan di atas api, bukan satu-satunya cara penggunaan.
  • Fitur-fitur modern seperti lambdas, inisialisasi, dan parameter bernama, dapat bekerja bahu-membahu untuk membuat antarmuka yang lancar lebih ramah.

Berikut adalah contoh dari apa yang saya maksudkan dengan fitur-fitur modern yang membuatnya merasa kurang dibutuhkan. Ambil contoh, (mungkin contoh buruk) antarmuka Fasih yang memungkinkan saya untuk membuat Karyawan seperti:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

Dapat dengan mudah ditulis dengan inisialisasi seperti:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

Saya juga bisa menggunakan parameter bernama dalam konstruktor dalam contoh ini.

Andrew Hanlon
sumber
1
Pertanyaan yang bagus, tapi saya pikir ini lebih merupakan pertanyaan wiki
Ivo
Pertanyaan Anda ditandai "fasih-nhibernate". Jadi, apakah Anda mencoba memutuskan apakah akan membuat antarmuka yang lancar, atau apakah akan menggunakan konfigurasi nhibernate vs XML yang lancar?
Ilya Kogan
1
Memilih untuk bermigrasi ke Programmers.SE
Matt Ellen
@Ilya Kogan, saya pikir ini sebenarnya ditandai "fasih-antarmuka" yang merupakan tag umum untuk pola antarmuka fasih. Pertanyaan ini bukan tentang nhibernate, tetapi seperti yang Anda katakan hanya apakah akan membuat antarmuka yang lancar. Terima kasih.
1
Posting ini mengilhami saya untuk memikirkan cara untuk menggunakan pola ini di C. Upaya saya dapat ditemukan di situs saudara Code Review .
otto

Jawaban:

28

Menulis antarmuka yang lancar (saya sudah mencoba - coba itu) membutuhkan lebih banyak usaha, tetapi memang ada hasil karena jika Anda melakukannya dengan benar, maksud dari kode pengguna yang dihasilkan lebih jelas. Ini pada dasarnya merupakan bentuk domain spesifik.

Dengan kata lain, jika kode Anda dibaca lebih banyak daripada yang tertulis (dan kode apa yang tidak?), Maka Anda harus mempertimbangkan membuat antarmuka yang lancar.

Antarmuka yang lancar lebih banyak tentang konteks, dan jauh lebih dari sekedar cara untuk mengkonfigurasi objek. Seperti yang Anda lihat di tautan di atas, saya menggunakan API fasih-ish untuk mencapai:

  1. Konteks (jadi ketika Anda biasanya melakukan banyak tindakan dalam urutan dengan hal yang sama, Anda dapat membuat rantai tindakan tanpa harus menyatakan konteks Anda berulang-ulang).
  2. Discoverability (ketika Anda pergi ke objectA.intellisense memberi Anda banyak petunjuk. Dalam kasus saya di atas, plm.Led.memberi Anda semua opsi untuk mengendalikan LED bawaan, dan plm.Network.memberi Anda hal-hal yang dapat Anda lakukan dengan antarmuka jaringan. plm.Network.X10.Memberi Anda subset dari tindakan jaringan untuk perangkat X10 Anda tidak akan mendapatkan ini dengan inisialisasi konstruktor (kecuali jika Anda ingin harus membangun objek untuk setiap jenis tindakan yang berbeda, yang tidak idiomatis).
  3. Reflection (tidak digunakan dalam contoh di atas) - kemampuan untuk mengambil lulus dalam ekspresi LINQ dan memanipulasinya adalah alat yang sangat kuat, terutama di beberapa helper API yang saya buat untuk unit test. Saya dapat menyampaikan ekspresi pengambil properti, membuat sejumlah besar ekspresi berguna, mengkompilasi dan menjalankannya, atau bahkan menggunakan pengambil properti untuk mengatur konteks saya.

Satu hal yang biasanya saya lakukan adalah:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

Saya tidak melihat bagaimana Anda dapat melakukan hal seperti itu juga tanpa antarmuka yang lancar.

Sunting 2 : Anda juga dapat membuat peningkatan keterbacaan yang sangat menarik, seperti:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();
Scott Whitlock
sumber
Terima kasih atas jawabannya, namun saya menyadari alasan untuk menggunakan Fluent, tetapi saya mencari alasan yang lebih konkret untuk menggunakannya pada sesuatu seperti contoh baru saya di atas.
Andrew Hanlon
Terima kasih atas balasan yang diperpanjang. Saya pikir Anda telah menguraikan dua aturan praktis yang baik: 1) Gunakan Fasih ketika Anda memiliki banyak panggilan yang mendapat manfaat dari konteks 'pass-through'. 2) Pikirkan tentang Fasih ketika Anda memiliki lebih banyak panggilan dari pada setter.
Andrew Hanlon
2
@ ya, saya tidak melihat apa pun di balasan ini tentang "lebih banyak panggilan daripada setel". Apakah Anda bingung dengan pernyataannya tentang "kode [yang] lebih banyak dibaca daripada yang tertulis"? Itu bukan tentang pengambil properti / setter, ini tentang manusia membaca kode vs manusia menulis kode - tentang membuat kode mudah bagi manusia untuk membaca, karena kita biasanya membaca baris kode yang diberikan jauh lebih sering daripada kita memodifikasinya.
Joe White
@ Jo White, saya mungkin harus mengulangi istilah 'panggilan' untuk 'tindakan'. Tapi gagasan itu masih berlaku. Terima kasih.
Andrew Hanlon
Refleksi untuk pengujian itu jahat!
Adronius
24

Scott Hanselman berbicara tentang ini dalam Episode 260 podcastnya Hanselmin dengan Jonathan Carter. Mereka menjelaskan bahwa antarmuka yang lancar lebih mirip UI pada API. Anda seharusnya tidak menyediakan antarmuka yang lancar sebagai satu-satunya titik akses, tetapi sebaliknya menyediakannya sebagai semacam kode-UI di atas "antarmuka API biasa".

Jonathan Carter juga berbicara sedikit tentang desain API di blog-nya .

Kristof Claes
sumber
Terima kasih banyak untuk tautan info dan UI di atas API adalah cara yang bagus untuk melihatnya.
Andrew Hanlon
14

Antarmuka yang lancar adalah fitur yang sangat kuat untuk diberikan dalam konteks kode Anda, ketika don dengan alasan "benar".

Jika tujuan Anda adalah membuat rantai kode satu-garis besar-besaran sebagai semacam pseudo-black-box, maka Anda mungkin menggonggong pohon yang salah. Jika di sisi lain Anda menggunakannya untuk menambah nilai ke antarmuka API Anda dengan menyediakan sarana untuk membuat panggilan metode panggilan dan meningkatkan keterbacaan kode, maka dengan banyak perencanaan dan upaya yang baik, saya pikir upaya ini sepadan.

Saya akan menghindari mengikuti apa yang tampaknya menjadi "pola" yang umum ketika membuat antarmuka yang lancar, di mana Anda memberi nama semua metode lancar Anda "dengan" -sesuatu, karena merampas antarmuka API yang berpotensi baik dari konteksnya, dan karena itu nilai intrinsiknya .

Kuncinya adalah memikirkan sintaks yang lancar sebagai implementasi spesifik dari bahasa khusus Domain. Sebagai contoh yang sangat bagus dari apa yang saya bicarakan, lihat StoryQ, yang menggunakan kelancaran sebagai sarana untuk mengekspresikan DSL dengan cara yang sangat berharga dan fleksibel.

S.Robins
sumber
Terima kasih atas jawabannya, tidak ada kata terlambat untuk memberikan respons yang dipikirkan dengan matang.
Andrew Hanlon
Saya tidak keberatan awalan 'dengan' untuk metode. Ini membedakan mereka dari metode lain yang tidak mengembalikan objek untuk dirantai. Misalnya position.withX(5)versusposition.getDistanceToOrigin()
LegendLength
5

Catatan awal: Saya mengambil masalah dengan satu asumsi dalam pertanyaan, dan menarik kesimpulan spesifik saya (pada akhir posting ini) dari itu. Karena ini mungkin tidak menghasilkan jawaban yang lengkap dan mencakup, saya menandainya sebagai CW.

Employees.CreateNew().WithFirstName("Peter")…

Dapat dengan mudah ditulis dengan inisialisasi seperti:

Employees.Add(new Employee() { FirstName = "Peter",  });

Di mata saya, dua versi ini harus berarti dan melakukan hal yang berbeda.

  • Berbeda dengan versi non-fasih, versi fasih menyembunyikan fakta bahwa yang baru Employeejuga Adddiedit ke koleksi Employees- itu hanya menunjukkan bahwa objek baru adalah Created.

  • Arti ….WithX(…)ambigu, terutama untuk orang yang berasal dari F #, yang memiliki withkata kunci untuk ekspresi objek : Mereka mungkin menafsirkan obj.WithX(x)sebagai objek baru yang berasal dari objyang identik dengan objkecuali untuk Xpropertinya, yang nilainya x. Di sisi lain, dengan versi kedua, jelas bahwa tidak ada objek turunan dibuat, dan bahwa semua properti ditentukan untuk objek asli.

….WithManager().With
  • Ini ….With…memiliki arti lain: mengalihkan "fokus" inisialisasi properti ke objek yang berbeda. Fakta bahwa API Anda lancar memiliki dua arti berbeda untuk Withmembuatnya sulit untuk menafsirkan dengan benar apa yang terjadi ... yang mungkin mengapa Anda menggunakan lekukan dalam contoh Anda untuk menunjukkan makna yang dimaksud dari kode itu. Akan lebih jelas seperti ini:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the `Manager` property appears inside the parentheses,
    //                     like with `WithName`, where the `Name` is also inside the parentheses 

Kesimpulan: "Menyembunyikan" fitur bahasa yang cukup sederhana new T { X = x },, dengan API yang lancar ( Ts.CreateNew().WithX(x)) jelas dapat dilakukan, tetapi:

  1. Harus diperhatikan bahwa pembaca kode fasih yang dihasilkan masih mengerti apa tepatnya itu. Artinya, API yang lancar harus transparan dalam arti, dan tidak ambigu. Merancang API semacam itu mungkin lebih banyak pekerjaan daripada yang diperkirakan (mungkin harus diuji untuk kemudahan penggunaan dan penerimaan), dan / atau ...

  2. mendesainnya mungkin lebih banyak pekerjaan daripada yang diperlukan: Dalam contoh ini, API yang lancar menambahkan sangat sedikit "kenyamanan pengguna" di atas API yang mendasarinya (fitur bahasa). Orang bisa mengatakan bahwa API yang lancar harus membuat fitur API / bahasa yang mendasarinya "lebih mudah digunakan"; yaitu, itu harus menghemat banyak usaha programmer. Jika itu hanya cara lain untuk menulis hal yang sama, itu mungkin tidak sepadan, karena itu tidak membuat hidup programmer lebih mudah, tetapi hanya membuat kerja perancang lebih keras (lihat kesimpulan # 1 tepat di atas).

  3. Kedua poin di atas diam-diam menganggap bahwa API lancar adalah lapisan di atas API atau fitur bahasa yang ada. Asumsi ini mungkin merupakan pedoman lain yang baik: API yang lancar dapat menjadi cara ekstra untuk melakukan sesuatu, bukan satu-satunya cara. Artinya, mungkin ide yang baik untuk menawarkan API yang lancar sebagai pilihan "memilih".

stakx
sumber
1
Terima kasih telah meluangkan waktu untuk menambah pertanyaan saya. Saya akan mengakui bahwa contoh yang saya pilih kurang dipikirkan. Pada saat itu saya benar-benar ingin menggunakan antarmuka yang lancar untuk API permintaan yang saya kembangkan. Saya terlalu disederhanakan. Terima kasih telah menunjukkan kesalahan, dan untuk poin kesimpulan yang bagus.
Andrew Hanlon
2

Saya suka gaya fasih, itu mengekspresikan niat sangat jelas. Dengan contoh objek initaliser yang Anda miliki setelah, Anda harus memiliki setter properti publik untuk menggunakan sintaks itu, Anda tidak dengan gaya fasih. Mengatakan itu, dengan contoh Anda, Anda tidak mendapatkan banyak dari setter publik karena Anda hampir pergi untuk metode set / get gaya java-esque.

Yang membawa saya ke poin kedua, saya tidak yakin apakah saya akan menggunakan gaya fasih seperti yang Anda miliki, dengan banyak setter properti, saya mungkin akan menggunakan versi kedua untuk itu, saya merasa lebih baik ketika Anda memiliki banyak kata kerja untuk dirangkai, atau setidaknya banyak melakukan daripada pengaturan.

Ian
sumber
Terima kasih atas balasan Anda, saya pikir Anda telah menyatakan aturan praktis yang baik: Lancar lebih baik dengan banyak panggilan melalui banyak setter.
Andrew Hanlon
1

Saya tidak terbiasa dengan istilah antarmuka lancar , tetapi mengingatkan saya pada beberapa API yang saya gunakan termasuk LINQ .

Secara pribadi saya tidak melihat bagaimana fitur-fitur modern C # akan mencegah kegunaan dari pendekatan semacam itu. Saya lebih suka mengatakan mereka berjalan beriringan. Misalnya lebih mudah untuk mencapai antarmuka seperti itu dengan menggunakan metode ekstensi .

Mungkin jelaskan jawaban Anda dengan contoh konkret tentang bagaimana antarmuka yang lancar dapat diganti dengan menggunakan salah satu fitur modern yang Anda sebutkan.

Steven Jeuris
sumber
1
Terima kasih atas jawabannya - Saya telah menambahkan contoh dasar untuk membantu memperjelas pertanyaan saya.
Andrew Hanlon