Pohon Ekspresi Pengembangan Perusahaan

Bagi sebagian besar pengembang, penggunaan pohon ekspresi terbatas pada ekspresi lambda di LINQ. Seringkali kita tidak mementingkan bagaimana teknologi bekerja “di bawah tenda”.

Pada artikel ini saya akan menunjukkan kepada Anda teknik-teknik canggih untuk bekerja dengan pohon ekspresi: menghilangkan duplikasi kode dalam LINQ, pembuatan kode, metaprogramming, transpilation, otomatisasi uji.

Anda akan belajar cara menggunakan pohon ekspresi secara langsung, perangkap apa yang telah disiapkan teknologi, dan cara menyiasatinya.



Di bawah potongan - video dan teks transkrip laporan saya dengan DotNext 2018 Piter.


Nama saya Maxim Arshinov, saya adalah salah satu pendiri perusahaan outsourcing Grup Hi-Tech. Kami terlibat dalam pengembangan perangkat lunak untuk bisnis, dan hari ini saya akan berbicara tentang bagaimana teknologi pohon ekspresi digunakan dalam pekerjaan sehari-hari dan bagaimana teknologi itu mulai membantu kami.

Saya tidak pernah secara khusus ingin mempelajari struktur internal pohon ekspresi, sepertinya ini adalah semacam teknologi internal untuk .NET Team untuk LINQ untuk bekerja, dan pemrogram aplikasi tidak perlu mengetahui API-nya. Ternyata ada beberapa masalah terapan yang perlu diselesaikan. Agar saya menyukai solusinya, saya harus naik ke usus.

Seluruh cerita ini membentang dalam waktu, ada proyek yang berbeda, kasus yang berbeda. Sesuatu keluar, dan saya menyelesaikannya, tetapi saya akan membiarkan diri saya mengorbankan kebenaran sejarah demi presentasi yang lebih artistik, jadi semua contohnya akan menggunakan model subjek yang sama - toko online.



Bayangkan kita semua sedang menulis toko online. Ini memiliki produk dan tanda centang "Dijual" di panel admin. Kami hanya akan menampilkan produk-produk yang memiliki tanda centang ini ditandai pada bagian publik.



Kami mengambil beberapa DbContext atau NHibernate, kami menulis Where (), kami menampilkan IsForSale.

Semuanya baik-baik saja, tetapi aturan bisnis tidak sama sehingga kami menulisnya sekali dan untuk semua. Mereka berkembang seiring waktu. Seorang manajer datang dan mengatakan bahwa kita masih harus memantau saldo dan hanya menampilkan barang-barang yang memiliki saldo di bagian publik, tidak melupakan tanda centang.



Kami dengan mudah menambahkan properti seperti itu. Sekarang aturan bisnis kami dienkapsulasi, kami dapat menggunakannya kembali.



Mari kita coba edit LINQ. Apakah semuanya baik-baik saja di sini?
Tidak, ini tidak akan berfungsi, karena IsAvailable tidak memetakan dengan cara apa pun ke database, ini adalah kode kami, dan penyedia kueri tidak tahu bagaimana menguraikannya.



Kita dapat memberitahunya bahwa properti kita memiliki kisah seperti itu. Tapi sekarang lambda ini digandakan baik dalam ekspresi LINQ dan properti.

Where(x => x.IsForSale && x.InStock > 0) IsAvailable => IsForSale && InStock > 0; 

Jadi, kali berikutnya lambda ini berubah, kita harus melakukan Ctrl + Shift + F pada proyek. Tentu, kita semua tidak akan menemukan - bug dan waktu. Saya ingin menghindari ini.



Kita bisa pergi dari sisi ini dan meletakkan ToList () lain di depan Where (). Ini adalah keputusan yang buruk, karena jika ada sejuta barang di database, semua orang naik ke RAM dan menyaring di sana.



Jika Anda memiliki tiga produk di toko, solusinya bagus, tetapi di E-commerce biasanya ada lebih banyak. Ini bekerja hanya karena, meskipun kesamaan lambda satu sama lain, mereka memiliki tipe yang sama sekali berbeda. Dalam kasus pertama, ini adalah delegasi Fungsi, dan yang kedua, pohon ekspresi. Terlihat sama, tipenya berbeda, bytecode sama sekali berbeda.



Untuk berpindah dari ekspresi ke delegasi, Anda hanya perlu memanggil metode Kompilasi (). API ini menyediakan .NET: ada ekspresi - dikompilasi, menerima delegasi.

Tetapi bagaimana cara kembali? Apakah ada sesuatu di .NET untuk berpindah dari delegasi ke pohon ekspresi? Jika Anda terbiasa dengan LISP, misalnya, maka ada mekanisme kutipan yang memungkinkan kode untuk ditafsirkan sebagai struktur data, tetapi dalam. NET tidak.

Ekspresikan atau delegasi?


Mengingat bahwa kita memiliki dua jenis lambda, kita dapat berfilsafat apa yang utama: pohon ekspresi atau delegasi.

 // so slo-oooooo-ow var delegateLambda = expressionLambda.Compile(); 

Sekilas, jawabannya jelas: karena ada metode Compile () yang bagus, pohon ekspresi adalah yang utama. Dan kita harus menerima delegasi dengan menyusun ekspresi. Tetapi kompilasi adalah proses yang lambat, dan jika kita mulai melakukan ini di mana-mana, kita mendapatkan penurunan kinerja. Selain itu, kami akan menerimanya secara acak, di mana ekspresi harus dikompilasi menjadi delegasi, akan ada penurunan kinerja. Anda dapat menemukan tempat-tempat ini, tetapi mereka akan memengaruhi waktu respons server, dan secara acak.



Karena itu, mereka perlu di-cache entah bagaimana. Jika Anda mendengarkan ceramah tentang struktur data bersamaan, maka Anda tahu tentang ConcurrentDictionary (atau hanya tahu tentang itu). Saya akan menghilangkan detail tentang metode caching (dengan kunci, bukan kunci). Hanya saja ConcurrentDictionary memiliki metode GetOrAdd () yang sederhana, dan implementasi paling sederhana adalah memasukkannya ke ConcurrentDictionary dan menyimpannya. Pertama kali kita mendapatkan kompilasi, tetapi semuanya akan cepat, karena delegasi sudah dikompilasi.



Kemudian Anda dapat menggunakan metode ekstensi seperti itu, Anda dapat menggunakan dan memperbaiki kode kami dengan IsAvailable (), mendeskripsikan ekspresi, kompilasi properti IsAvailable () dan menyebutnya relatif terhadap objek saat ini.

Setidaknya ada dua paket yang mengimplementasikan ini: Microsoft.Linq.Translations and Signum Framework (kerangka kerja open source yang ditulis oleh perusahaan komersial). Baik di sana maupun di sana tentang kisah yang sama dengan kompilasi para delegasi. API yang sedikit berbeda, tetapi semuanya seperti yang saya tunjukkan pada slide sebelumnya.

Namun, ini bukan satu-satunya pendekatan, dan Anda dapat beralih dari delegasi ke ekspresi. Sudah lama ada artikel tentang Habre tentang Delegate Decompiler, di mana penulis mengklaim bahwa semua kompilasi itu buruk, karena untuk waktu yang lama.

Secara umum, delegasi sebelum ekspresi, dan Anda dapat beralih ke mereka dari delegasi. Untuk melakukan ini, penulis menggunakan methodBody.GetILAsByteArray (); dari Reflection, yang benar-benar mengembalikan seluruh kode IL metode sebagai array byte. Jika Anda memasukkannya lebih jauh ke Reflection, Anda bisa mendapatkan representasi objek dari case ini, lewati dengan loop dan buat pohon ekspresi. Dengan demikian, transisi balik juga dimungkinkan, tetapi harus dilakukan dengan tangan.



Agar tidak menjalankan semua properti, penulis menyarankan untuk menggantung atribut yang dihitung untuk menunjukkan bahwa properti ini perlu digarisbawahi. Sebelum permintaan, kita naik ke IsAvailable (), mengeluarkan kode-IL-nya, mengonversinya menjadi pohon ekspresi dan mengganti panggilan IsAvailable () dengan apa yang tertulis dalam pengambil ini. Ternyata inlining manual seperti itu.



Agar ini berfungsi, sebelum meneruskan semuanya ke ToList (), kami memanggil metode khusus Decompile (). Ini menyediakan dekorator untuk query asli dan inlining. Hanya setelah itu kami memberikan segalanya kepada penyedia permintaan, dan semuanya baik-baik saja dengan kami.



Satu-satunya masalah dengan pendekatan ini adalah Delegate Decompiler 0.23.0 tidak akan bergerak maju, tidak ada dukungan Core, dan penulis sendiri mengatakan bahwa ini adalah alpha yang dalam, ada banyak yang belum selesai, sehingga Anda tidak dapat menggunakannya dalam produksi. Meskipun kami akan kembali ke topik ini.

Operasi Boolean


Ternyata kami telah memecahkan masalah duplikasi kondisi tertentu.



Tetapi kondisinya seringkali perlu digabungkan dengan menggunakan logika Boolean. Kami memiliki IsForSale (), InStock ()> 0, dan di antara mereka kondisi "AND". Jika ada kondisi lain, atau kondisi "ATAU" diperlukan.



Dalam kasus "Dan," Anda dapat menipu dan membuang semua pekerjaan pada penyedia kueri, yaitu, menulis banyak Di mana () berturut-turut, ia tahu bagaimana melakukannya.



Jika "ATAU" diperlukan, ini tidak akan berhasil, karena WhereOr () tidak di LINQ, dan operator | | tidak kelebihan dengan ekspresi.

Spesifikasi


Jika Anda terbiasa dengan buku DDD Evans atau hanya tahu sesuatu tentang pola Spesifikasi, yaitu, pola desain yang dirancang khusus untuk ini. Ada beberapa aturan bisnis dan Anda ingin menggabungkan operasi dalam logika Boolean - mengimplementasikan Spesifikasi.



Spesifikasi adalah istilah seperti itu, pola lama dari Jawa. Dan di Jawa, terutama yang lama, tidak ada LINQ, jadi itu diterapkan di sana hanya dalam bentuk metode isSatisfiedBy (), yaitu, hanya delegasi, tetapi tidak ada pembicaraan tentang ekspresi. Ada implementasi di Internet yang disebut LinqSpecs , pada slide Anda akan melihatnya. Saya mengajukannya sedikit untuk diri saya sendiri, tetapi idenya milik perpustakaan.

Di sini, semua operator Boolean kelebihan beban, operator benar dan salah kelebihan beban sehingga kedua operator “&&” dan “||” bekerja, tanpa mereka hanya satu ampersand yang akan berfungsi.



Selanjutnya, kami menambahkan pernyataan implisit yang membuat kompiler berasumsi bahwa spesifikasi adalah ekspresi dan delegasi. Di mana saja di mana Ekspresi <> atau Fungsi <> harus masuk ke fungsi, Anda dapat melewati spesifikasi. Karena operator implisit kelebihan beban, kompiler akan mengurai dan mengganti properti Ekspresi atau IsSatisfiedBy.



IsSatisfiedBy () dapat diimplementasikan dengan caching ekspresi yang datang. Dalam kasus apa pun, ternyata kami berasal dari Ekspresi, sesuai dengan delegasi itu, kami telah menambahkan dukungan untuk operator Boolean. Sekarang semua ini bisa diatur. Aturan bisnis dapat dimasukkan ke dalam spesifikasi statis, dideklarasikan dan digabungkan.

 public static readonly Spec<Product> IsForSaleSpec = new Spec<Product>(x => x.IsForSale); public static readonly Spec<Product> IsInStockSpec = new Spec<Product>(x => x.InStock > 0); 



Setiap aturan bisnis ditulis hanya sekali, tidak akan hilang di mana pun, tidak akan diduplikasi, mereka dapat digabungkan. Orang yang datang ke proyek dapat melihat apa yang Anda miliki, kondisi apa, memahami model subjek.



Ada masalah kecil: Ekspresi tidak memiliki metode And (), Or (), dan Not (). Ini adalah metode penyuluhan, mereka harus dilaksanakan secara mandiri.



Upaya pertama implementasi adalah ini. Tentang pohon ekspresi, ada sedikit dokumentasi di Internet, dan semuanya tidak terperinci. Oleh karena itu, saya mencoba hanya untuk mengambil Ekspresi, menekan Ctrl + Space, melihat OrElse (), membacanya. Melewati dua Ekspresi untuk mengkompilasi dan mendapatkan lambda. Ini tidak akan berfungsi.



Faktanya adalah bahwa Ekspresi ini terdiri dari dua bagian: parameter dan tubuh. Yang kedua juga terdiri dari parameter dan badan. Dalam OrElse (), Anda harus melewati tubuh ekspresi, yaitu, tidak ada gunanya membandingkan lambda dengan "AND" dan "OR", ini tidak akan berhasil. Kami memperbaiki, tetapi tidak akan berfungsi lagi.

Tetapi jika terakhir kali ada NotSupportedException bahwa lambda tidak didukung, sekarang ada cerita aneh tentang parameter 1, parameter 2, "ada sesuatu yang salah, saya tidak akan bekerja".

C # 7.0 Singkatnya


Kemudian saya berpikir bahwa metode poke ilmiah tidak akan berhasil, saya perlu mencari tahu. Dia mulai mencari google dan menemukan situs buku Albahari " C # 7.0 in a Nutshell ".



Joseph Albahari, yang juga merupakan pengembang perpustakaan LINQKit dan LINQPad yang populer, hanya menjelaskan masalah ini. Anda tidak bisa hanya mengambil dan menggabungkan Ekspresi, dan jika Anda mengambil Ekspresi ajaib. Tingkatkan (), itu akan berhasil.

Pertanyaan: apa itu Expression.Invoke ()? Buka Google lagi. Itu menciptakan InvocationExpression yang menerapkan ekspresi delegasi atau lambda ke daftar argumen.



Jika saya membaca kode ini untuk Anda sekarang setelah kami menggunakan Expression.Invoke (), kami meneruskan parameter, maka hal yang sama ditulis dalam bahasa Inggris. Tidak semakin jelas. Ada beberapa Magic Expression.Invoke () yang karena alasan tertentu menyelesaikan masalah ini dengan parameter. Kita harus percaya, tidak perlu dipahami.



Pada saat yang sama, jika Anda mencoba untuk memberi makan Ekspresi gabungan seperti EF, itu akan jatuh lagi dan mengatakan bahwa Ekspresi.Invoke () tidak didukung. Omong-omong, EF Core mulai mendukung, tetapi EF 6 tidak berlaku. Tapi Albahari hanya menawarkan untuk menulis AsExpandable (), dan semuanya berfungsi.



Dan Anda dapat mengganti di sub-kueri Ekspresi di mana kami membutuhkan delegasi. Untuk membuatnya cocok, kita menulis Compile (), tetapi pada saat yang sama, jika kita menulis AsExpandable (), seperti yang disarankan Albahari, Compile ini () tidak akan benar-benar terjadi, tetapi semuanya entah bagaimana akan dilakukan secara ajaib dengan benar.



Saya tidak percaya sepatah kata pun dan naik ke sumber. Apa metode AsExpandable ()? Ini memiliki query dan QueryOptimizer. Kami akan meninggalkan yang kedua dari kurung, karena tidak menarik, tetapi hanya menempelkan Ekspresi: jika ada 3 + 5, itu akan menempatkan 8.



Sangat menarik bahwa metode Expand () dipanggil kemudian, setelah itu queryOptimizer, dan kemudian semuanya dilewatkan ke penyedia kueri entah bagaimana diperbaiki setelah metode Expand ().



Kami membukanya, ini adalah Pengunjung, di dalamnya kami melihat Kompilasi non-asli (), yang mengkompilasi sesuatu yang lain. Saya tidak akan memberi tahu Anda apa sebenarnya, meskipun ini memiliki arti tertentu, tetapi kami menghapus satu kompilasi dan menggantinya dengan yang lain. Hebat, tapi itu memukul pemasaran level 80 karena dampak kinerja tidak ke mana-mana.

Mencari alternatif


Saya pikir ini tidak akan berhasil dan mulai mencari solusi lain. Dan menemukannya. Ada Pete Montgomery yang juga menulis tentang masalah ini dan mengklaim bahwa Albahari memalsukan.

Pete berbicara dengan pengembang EF, dan mereka mengajarinya untuk menggabungkan semuanya tanpa Ekspresi.Evoke (). Idenya sangat sederhana: penyergapan dilakukan dengan parameter. Faktanya adalah bahwa dengan Ekspresi kombinasi ada parameter dari ekspresi pertama dan parameter yang kedua. Mereka tidak cocok. Mayat-mayat itu direkatkan bersama, tetapi parameternya tetap menggantung di udara. Mereka perlu dibalut dengan cara yang benar.

Untuk melakukan ini, Anda perlu mengkompilasi kamus dengan melihat parameter ekspresi, jika lambda bukan dari satu parameter. Kami membuat kamus, dan kami mengikat kembali semua parameter yang kedua ke parameter yang pertama, sehingga parameter awal memasukkan Ekspresi, melewati seluruh tubuh yang kami tempelkan bersama.

Metode sederhana seperti ini memungkinkan Anda untuk menyingkirkan semua penyergapan dengan Expression.Invoke (). Apalagi dalam implementasi Pete Montgomery, ini dibuat lebih keren. Ini memiliki metode Tulis () yang memungkinkan Anda untuk menggabungkan ekspresi apa pun.



Kami mengambil ekspresi dan melalui AndJuga kami terhubung, berfungsi tanpa Dapat Diperluas (). Implementasi inilah yang digunakan dalam operasi Boolean.

Spesifikasi dan unit


Semuanya baik-baik saja sampai menjadi jelas bahwa agregat ada di alam. Bagi mereka yang tidak terbiasa, saya akan menjelaskan: jika Anda memiliki model domain dan Anda mewakili semua entitas yang terkait satu sama lain dalam bentuk pohon, maka pohon yang digantung secara terpisah adalah agregat. Urutan bersama dengan item pesanan akan disebut agregat, dan esensi pesanan adalah akar agregasi.



Jika, selain produk, masih ada kategori dengan aturan bisnis yang diumumkan untuk mereka dalam bentuk spesifikasi, bahwa ada peringkat tertentu yang harus lebih dari 50, seperti yang dikatakan pemasar dan kami ingin menggunakannya seperti itu, maka ini bagus.



Tetapi jika kita ingin menarik barang keluar dari kategori yang baik, sekali lagi itu buruk, karena jenis kita tidak cocok. Spesifikasi untuk kategori, tetapi produk diperlukan.



Sekali lagi, kita perlu menyelesaikan masalah. Opsi pertama: ganti Select () dengan SelectMany (). Saya tidak suka dua hal di sini. Pertama, saya tidak tahu bagaimana dukungan SelectMany () diimplementasikan di semua penyedia kueri populer. Kedua, jika seseorang menulis penyedia kueri, hal pertama yang akan dia lakukan adalah menulis lemparan tidak menerapkan pengecualian dan SelectMany (). Dan poin ketiga: orang berpikir bahwa SelectMany () adalah fungsionalitas, atau bergabung, biasanya tidak terkait dengan permintaan SELECT.

Komposisi


Saya ingin menggunakan Select (), bukan SelectMany ().



Sekitar waktu yang sama, saya membaca tentang teori kategori, tentang komposisi fungsional dan berpikir bahwa jika ada spesifikasi dari produk di bool di bawah ini, ada beberapa fungsi yang dapat beralih dari produk ke kategori, ada spesifikasi mengenai kategori, kemudian mengganti yang pertama berfungsi sebagai argumen kedua, kita mendapatkan apa yang kita butuhkan, spesifikasi mengenai produk. Sama persis dengan komposisi fungsional berfungsi, tetapi untuk pohon ekspresi.



Maka akan mungkin untuk menulis metode Where () sehingga perlu untuk beralih dari produk ke kategori dan menerapkan spesifikasi untuk entitas terkait ini. Sintaks semacam itu untuk selera subjektif saya terlihat cukup dimengerti.

 public static IQueryable<T> Where<T, TParam>(this IQueryable<T> queryable, Expression<Func<T, TParam>> prop, Expression<Func<TParam, bool>> where) { return queryable.Where(prop.Compose(where)); } 

Dengan metode Compose (), ini juga dapat dilakukan dengan mudah. Kami mengambil Ekspresi input dari produk dan menggabungkannya dengan spesifikasi untuk produk dan hanya itu.



Sekarang Anda dapat menulis Where (). Ini akan berfungsi jika Anda memiliki mesin dengan panjang berapa pun. Kategori memiliki Kategori Super dan sejumlah properti lebih lanjut yang dapat diganti.

"Karena kita memiliki alat untuk komposisi fungsional, dan karena kita dapat mengompilasinya, dan karena kita dapat merangkainya secara dinamis, itu berarti ada bau meta-pemrograman!" Saya pikir.

Proyeksi


Di mana kita bisa menerapkan meta-pemrograman sehingga kita harus menulis lebih sedikit kode.



Opsi pertama adalah proyeksi. Mencabut seluruh entitas seringkali terlalu mahal. Paling sering kita menyebarkannya ke depan, bersambung JSON. Tetapi itu tidak membutuhkan keseluruhan esensi bersama dengan agregat. Anda dapat melakukan ini dengan LINQ seefisien mungkin dengan menulis Select () seperti itu secara manual. Tidak sulit, tetapi membosankan.



Sebagai gantinya, saya sarankan semua orang menggunakan ProjectToType (). Setidaknya ada dua perpustakaan yang dapat melakukan ini: Automapper dan Mapster. Untuk beberapa alasan, banyak orang tahu bahwa AutoMapper dapat melakukan pemetaan dalam memori, tetapi tidak semua orang tahu bahwa ia memiliki Queryable Extensions, ia juga memiliki Expression, dan dapat membangun ekspresi SQL. Jika Anda masih menulis pertanyaan manual dan Anda menggunakan LINQ, karena Anda tidak memiliki kendala kinerja yang sangat serius, maka tidak ada gunanya melakukannya dengan tangan Anda, ini adalah pekerjaan mesin, bukan orangnya.

Penyaringan


Jika kita dapat melakukan ini dengan proyeksi, mengapa tidak melakukannya untuk memfilter.



Ini kodenya juga. Filter masuk. Banyak aplikasi bisnis terlihat seperti ini: filter datang, tambahkan Where (), filter lain datang, tambahkan Where (). Ada berapa banyak filter, begitu banyak dan ulangi. Tidak ada yang rumit, tetapi banyak sekali copy-paste.



Jika kita, sebagai AutoMapper, melakukannya, menulis AutoFilter, Project dan Filter sehingga dia melakukan semuanya sendiri, itu akan menjadi kode yang lebih keren.



Ini tidak rumit. Ambil Expression.Property, berjalanlah melalui DTO dan pada intinya. Kami menemukan properti umum yang disebut identik. Jika mereka disebut sama, itu tampak seperti filter.

Selanjutnya, Anda perlu memeriksa nol, gunakan konstanta untuk menarik nilai dari DTO, gantilah dengan ekspresi dan tambahkan konversi jika Anda memiliki Int dan NullableInt atau Nullable lainnya sehingga jenisnya cocok. Dan masukkan, misalnya, Equals (), filter yang memeriksa kesetaraan.



Kemudian kumpulkan lambda dan pergi ke setiap properti: jika ada banyak, kumpulkan baik melalui "DAN" atau "ATAU", tergantung pada cara kerja filter untuk Anda.



Hal yang sama dapat dilakukan untuk menyortir, tetapi sedikit lebih rumit, karena metode OrderBy () memiliki dua obat generik, jadi Anda harus mengisinya dengan tangan Anda, gunakan Refleksi untuk membuat metode OrderBy () dari dua obat generik, masukkan jenis entitas yang kami ambil, jenis sortasi Properti Secara umum, Anda juga bisa melakukan ini, tidak sulit.

Muncul pertanyaan: di mana harus meletakkan Where () - di tingkat entitas, ketika spesifikasi diumumkan atau setelah proyeksi, dan di sana-sini itu akan berfungsi.



Benar dan benar, karena spesifikasinya adalah, menurut definisi, aturan bisnis, dan kita harus menghargai dan menghargainya dan tidak membuat kesalahan dengan itu. Ini adalah lapisan satu dimensi. Dan filter lebih banyak tentang UI, yang berarti mereka memfilter oleh DTO. Oleh karena itu, Anda dapat meletakkan dua Di mana (). Ada beberapa kemungkinan pertanyaan mengenai seberapa baik penyedia kueri akan menangani ini dengan baik, tetapi saya percaya bahwa solusi ORM tetap menulis SQL yang buruk, dan itu tidak akan jauh lebih buruk. Jika ini sangat penting bagi Anda, maka cerita ini sama sekali bukan tentang Anda.



Seperti yang mereka katakan, lebih baik melihat sekali daripada mendengar seratus kali.
Sekarang toko memiliki tiga produk: Snickers, Subaru Impreza dan Mars. Toko aneh. Mari kita coba cari Snickers. Ada. Mari kita lihat apa yang seratus rubel. Juga Snickers. Dan untuk 500? Perbesar, tidak ada apa-apa. Dan untuk 100500 Subaru Impreza. Hebat, hal yang sama berlaku untuk penyortiran.

Sortir berdasarkan abjad dan berdasarkan harga. Kode di sana ditulis persis seperti sebelumnya. Filter ini berfungsi untuk semua kelas, apa pun. Jika Anda mencoba mencari berdasarkan nama, maka Subaru juga ada. Dan dalam presentasi saya adalah Equals (). Bagaimana bisa begitu? Faktanya adalah bahwa kode di sini dan di dalam presentasi sedikit berbeda. Saya berkomentar di baris tentang Equals () dan menambahkan beberapa sihir jalanan khusus. Jika kita memiliki tipe String, maka kita tidak perlu Equals (), tetapi panggil StartWith (), yang juga saya terima. Oleh karena itu, filter berbeda dibuat untuk baris.



Ini berarti bahwa di sini Anda dapat menekan Ctrl + Shift + R, pilih metode dan menulis tidak jika, tetapi beralih, atau Anda bahkan dapat menerapkan pola "Strategi" dan kemudian menjadi gila. Anda dapat mewujudkan keinginan apa pun tentang pengoperasian filter. Itu semua tergantung pada jenis yang Anda gunakan. Yang terpenting, filter akan bekerja sama.

Anda dapat menyetujui bahwa filter di semua elemen UI harus berfungsi seperti ini: string dicari dengan satu cara, uang dicari dengan cara lain. Koordinasikan semua ini, tulis sekali, semuanya akan dilakukan dengan benar di antarmuka yang berbeda, dan tidak ada pengembang lain yang akan merusaknya, karena kode ini tidak pada level aplikasi, tetapi di suatu tempat baik di perpustakaan eksternal atau di kernel Anda.

Validasi


Selain pemfilteran dan proyeksi, Anda dapat melakukan validasi. JS library TComb.validation datang dengan ide ini. TComb adalah singkatan dari Combinator Tipe dan didasarkan pada sistem tipe dan sebagainya. perbaikan, perbaikan.

 // null and undefined validate('a', t.Nil).isValid(); // => false validate(null, t.Nil).isValid(); // => true validate(undefined, t.Nil).isValid(); // => true // strings validate(1, t.String).isValid(); // => false validate('a', t.String).isValid(); // => true // numbers validate('a', t.Number).isValid(); // => false validate(1, t.Number).isValid(); // => true 

Pertama, primitif dinyatakan sesuai untuk semua jenis JS, dan tipe nill tambahan sesuai dengan yang tidak terdefinisi atau nol.

 // a predicate is a function with signature: (x) -> boolean var predicate = function (x) { return x >= 0; }; // a positive number var Positive = t.refinement(t.Number, predicate); validate(-1, Positive).isValid(); // => false validate(1, Positive).isValid(); // => true 

Kemudian kesenangan dimulai. Setiap jenis dapat ditingkatkan dengan predikat. Jika kita menginginkan angka lebih besar dari nol, maka kita menyatakan predikat x> = 0 dan melakukan validasi terhadap tipe Positif. Jadi, dari blok bangunan, Anda dapat mengumpulkan validasi apa pun. Kami perhatikan, mungkin, ada juga ekspresi lambda.



Panggilan itu diterima. Kami melakukan penyempurnaan yang sama, menulisnya dalam C #, menulis metode IsValid (), dan mengkompilasi dan mengeksekusi Ekspresi juga. Sekarang kami memiliki kesempatan untuk melakukan validasi.

 public class RefinementAttribute: ValidationAttribute { public IValidator<object> Refinement { get; } public RefinementAttribute(Type refinmentType) { Refinement = (IValidator<object>) Activator.CreateInstance(refinmentType); } public override bool IsValid(object value) => Refinement.Validate(value).IsValid(); } 

Kami mengintegrasikan dengan sistem DataAnnotations standar di ASP.NET MVC sehingga semuanya bekerja di luar kotak. Kami mendeklarasikan RefinementAttribute (), meneruskan tipe ke konstruktor. Faktanya adalah bahwa RefinementAttribute adalah generik, jadi Anda harus menggunakan tipe seperti ini karena Anda tidak dapat mendeklarasikan atribut generik di .NET, sayangnya.



Jadi tandai kelas pengguna dengan refinance. AdultRefinement, yang lebih dari 18.



Agar benar-benar bagus, mari kita buat validasi pada klien dan server sama. Pendukung NoJS menyarankan penulisan dukungan dan depan di JS. Ok, saya akan menulis kembali dan depan dalam C #, tidak apa-apa dan saya hanya memindahkannya ke JS. Javascriptists dapat menulis di JSX, ES6 dan menerjemahkannya ke dalam JavaScript. Kenapa kita tidak bisa? Kami menulis Pengunjung, melalui operator mana yang dibutuhkan dan menulis JavaScript.



Kasus validasi terpisah yang sering adalah ekspresi reguler, mereka juga perlu dibongkar. Jika Anda memiliki regexp, ambil StringBuilder, bangun regexp. Di sini saya menggunakan dua tanda seru, karena JS adalah bahasa yang diketik secara dinamis, ungkapan ini akan dilemparkan ke bool selalu, sehingga semuanya baik-baik saja dengan jenisnya. Mari kita lihat tampilannya.

 { predicate: “x=> (x >= 18)”, errorMessage: “For adults only» } 

Ini adalah pemurnian kami, yang berasal dari backend, predikat garis, seperti di JS tidak ada lambda dan pesan error “Khusus untuk orang dewasa”. Mari kita coba mengisi formulir. Tidak lulus. Kami melihat bagaimana itu dibuat.

Ini Bereaksi, kami meminta dari backend dari metode UserRefinment () Ekspresi dan errorMessage, buat pemurnian relatif terhadap angka, gunakan eval untuk mendapatkan lambda. Jika saya mengulang ini dan menghapus batasan jenis, ganti dengan nomor biasa, validasi akan jatuh pada JS. Masukkan unit, kirim. Saya tidak tahu apakah itu terlihat atau tidak, false disimpulkan di sini.



Kode ini waspada. Ketika kami mengirim onSubmit, beri tahu apa yang datang dari backend. Dan backend adalah kode sederhana.



Kami hanya mengembalikan Ok (ModelState.IsValid), kelas pengguna yang kami dapatkan dari formulir di JavaScript. Inilah atribut Penyempurnaan ini.

 usingnamespace DemoApp.Core { public class User: HasNameBase { [Refinement(typeof(AdultRefinement))] public int Age { get; set; } } } 

Artinya, validasi juga berfungsi pada backend, yang dinyatakan dalam lambda ini. Dan kami menerjemahkannya ke dalam JavaScript. Ternyata, kami menulis ekspresi lambda di C #, dan kode dieksekusi di sana dan di sana. Jawaban kami adalah NoJS, kami juga bisa melakukannya.

Pengujian




Biasanya Timlids yang lebih memperhatikan jumlah kesalahan dalam kode. Mereka yang menulis tes unit tahu perpustakaan Moq. Apakah Anda ingin menulis tiruan atau mendeklarasikan beberapa kelas - ada moq, ia memiliki sintaks yang lancar. Anda dapat melukis bagaimana Anda ingin dia berperilaku dan menyelipkan aplikasinya untuk pengujian.

Lambdas ini dalam moq juga Ekspresi, bukan delegasi. Dia menjalankan melalui pohon ekspresi, menerapkan logikanya, dan kemudian memberi makan di Castle.DynamicProxy. Dan dia menciptakan kelas yang diperlukan dalam runtime. Tetapi kita juga bisa melakukannya.



Seorang teman saya baru-baru ini bertanya apakah ada sesuatu seperti WCF di Core kami. Saya menjawab bahwa ada WebAPI. Dia ingin di WebAPI, seperti di WCF di WSDL untuk membangun proxy. Hanya ada kesombongan di WebAPI. Tapi kesombongan hanyalah teks, dan seorang teman tidak ingin menonton setiap kali ketika API berubah dan apa yang rusak. Ketika ada WCF, itu mengaktifkan WSDL, jika spesifikasi telah berubah di API, kompilasi rusak.

Ini masuk akal, karena enggan untuk mencari, dan kompiler dapat membantu. Dengan analogi dengan moq, Anda dapat mendeklarasikan metode GetResponse <> () generik dengan ProductController Anda, dan lambda yang masuk ke metode ini diparameterisasi oleh pengontrol. Yaitu, ketika Anda mulai menulis lambda, tekan Ctrl + Space dan lihat semua metode yang dimiliki pengontrol ini, asalkan ada pustaka, dll dengan kode. Ada Intellisense, tulis semua ini seolah-olah Anda memanggil controller.

Selanjutnya, seperti Moq, kami tidak akan menyebutnya, tetapi cukup membangun pohon ekspresi, melewatinya, menarik semua informasi perutean dari konfigurasi API. Dan alih-alih melakukan sesuatu dengan controller, yang tidak dapat kita jalankan, karena kita harus mengeksekusinya di server, kita hanya membuat permintaan POST atau GET yang kita butuhkan, dan sebaliknya kita deserialize yang diterima, karena dari Intellisense dan pohon ekspresi yang kita ketahui tentang semua tipe pengembalian. Ternyata, kami menulis kode tentang pengontrol, tetapi sebenarnya kami membuat permintaan Web.

Optimalisasi Refleksi

Segala sesuatu tentang meta-pemrograman memiliki banyak kesamaan dengan Refleksi.



Kita tahu bahwa Refleksi lambat, saya ingin menghindari ini. Di sini, juga, ada beberapa kasus yang baik untuk bekerja dengan Ekspresi. Yang pertama adalah aktivator CreateInstance. Anda tidak boleh menggunakannya sama sekali, karena ada Expression.New (), yang bisa Anda pakai ke lambda, kompilasi dan dapatkan konstruktornya.



Saya meminjam slide ini dari pembicara yang hebat dan musisi Vagif. Dia melakukan semacam tolok ukur di sebuah blog. Ini Activator, ini adalah Puncak Komunisme, Anda lihat betapa dia berusaha melakukan semuanya. Constructor_Invoke, sekitar setengahnya bagus. Dan di sebelah kiri adalah lambda Baru dan disusun. Ada sedikit peningkatan kinerja karena fakta bahwa ini adalah delegasi, bukan konstruktor, tetapi pilihannya jelas, jelas bahwa ini jauh lebih baik.



Hal yang sama dapat dilakukan dengan getter atau setter.



Ini dilakukan dengan sangat sederhana. Jika karena alasan tertentu Anda tidak nyaman dengan Fast Memember, Mark Gravelli atau Fast Reflect, jika Anda tidak ingin menarik ketergantungan ini, Anda dapat melakukan hal yang sama. Satu-satunya kesulitan adalah Anda perlu memonitor semua kompilasi ini, menyimpan dan menghangatkan cache di suatu tempat. Artinya, jika ada banyak ini, maka pada awalnya perlu untuk kompilasi sekali.



Karena ada konstruktor, pengambil dan penyetel, hanya ada perilaku, metode. Tetapi mereka juga dapat dikompilasi menjadi delegasi, dan Anda hanya akan mendapatkan kebun binatang delegasi besar yang harus Anda kelola. Mengetahui semua yang saya bicarakan, mungkin muncul ide seseorang bahwa jika ada banyak delegasi, banyak ekspresi, maka mungkin ada ruang untuk apa yang disebut DSL, Bahasa Kecil atau pola juru bahasa, monad gratis.

Ini semua adalah hal yang sama ketika untuk beberapa tugas kami datang dengan seperangkat perintah dan baginya kami menulis juru bahasa kami sendiri yang melakukan ini. Artinya, di dalam aplikasi kita menulis kompiler atau juru bahasa lain yang tahu bagaimana menggunakan perintah ini. Ini persis apa yang dilakukan di DLR, di bagian yang berfungsi dengan IronPython, bahasa IronRuby. Pohon Ekspresi di sana digunakan untuk mengeksekusi kode dinamis di CLR. Hal yang sama dapat dilakukan dalam aplikasi bisnis, tetapi sejauh ini kami belum melihat kebutuhan seperti itu dan ini masih di luar kurung.

Ringkasan


Sebagai kesimpulan, saya ingin berbicara tentang kesimpulan apa yang kami dapatkan setelah implementasi dan pengujian. Seperti yang saya katakan, ini terjadi pada proyek yang berbeda. Semua yang saya tulis, kami tidak gunakan di mana-mana, tetapi di suatu tempat jika perlu, beberapa hal digunakan.

Nilai tambah pertama adalah kemampuan untuk mengotomatisasi rutinitas. Jika Anda memiliki 100 ribu cetakan dengan penyaringan, pagination, dan semua itu. Mozart punya lelucon bahwa menggunakan dadu, waktu yang cukup dan segelas anggur merah, Anda dapat menulis waltz dalam jumlah berapa pun. Di sini dengan bantuan Pohon Ekspresi, sebuah meta-pemrograman kecil, Anda dapat menulis formulir dalam jumlah berapa pun.

Jumlah kode sangat berkurang, sebagai alternatif untuk pembuatan kode, jika Anda tidak menyukainya, karena Anda mendapatkan banyak kode, Anda tidak dapat menulisnya, meninggalkan semuanya dalam runtime.

Menggunakan kode seperti itu untuk tugas-tugas sederhana, kami selanjutnya mengurangi persyaratan untuk pemain, karena kode imperatif sangat sedikit dan tidak ada ruang untuk kesalahan juga. Setelah menarik sejumlah besar kode ke komponen yang dapat digunakan kembali, kami menghapus kelas kesalahan ini.

Di sisi lain, kami sangat meningkatkan persyaratan untuk kualifikasi desainer, karena pertanyaan muncul dari pengetahuan tentang bekerja dengan Ekspresi, Refleksi, optimalisasi mereka, tentang tempat-tempat di mana Anda dapat menembak diri sendiri. Ada banyak nuansa seperti itu, sehingga seseorang yang tidak terbiasa dengan API ini tidak akan segera mengerti mengapa Ekspresi tidak digabungkan. Perancang harus lebih dingin.

Dalam beberapa kasus, melalui Expression.Compile (), Anda dapat menangkap penurunan kinerja. Dalam contoh caching, saya memiliki batasan bahwa Ekspresi statis karena Kamus digunakan untuk caching. Jika seseorang tidak tahu bagaimana ini diatur secara internal, ia mulai tanpa berpikir melakukan hal itu, menyatakan spesifikasi di dalamnya tidak statis, metode cache tidak akan berfungsi, dan kami akan mendapatkan panggilan ke Compile () di tempat-tempat acak. Persis apa yang ingin saya hindari.

Minus yang paling tidak menyenangkan adalah bahwa kode berhenti terlihat seperti kode C #, menjadi kurang idiomatis, panggilan statis muncul, metode Where () tambahan aneh, beberapa operator implisit kelebihan beban. Ini bukan dalam dokumentasi MSDN, dalam contoh. Jika, misalnya, seseorang dengan sedikit pengalaman mendatangi Anda, yang tidak terbiasa masuk ke kode sumber jika terjadi sesuatu, ia kemungkinan besar akan bersujud sedikit untuk pertama kalinya, karena ini tidak cocok dengan gambar dunia, tidak ada contoh seperti itu di StackOverflow, tetapi dengan ini harus bekerja entah bagaimana.

Secara umum, ini semua yang ingin saya bicarakan hari ini. Banyak hal yang saya ceritakan, lebih detail, dengan detail ditulis di Habré. Kode perpustakaan diposting di github, tetapi memiliki satu kesalahan fatal - kurangnya dokumentasi.
22-23 DotNext 2018 Moscow . , ( ).

Source: https://habr.com/ru/post/id423891/


All Articles