Tema abstraksi dan segala macam pola yang indah adalah dasar yang baik untuk pengembangan holivar dan perselisihan abadi: di satu sisi, kita mengikuti arus utama, semua jenis kata-kata modis dan kode bersih, di sisi lain, kita memiliki praktik dan kenyataan yang selalu mendikte aturan mereka sendiri.
Apa yang harus dilakukan jika abstraksi mulai “bocor”, bagaimana menggunakan chip bahasa dan apa yang dapat Anda peras dari pola “spesifikasi” - lihat di bawah potongan.
Jadi, mari kita mulai bisnis. Artikel ini akan berisi bagian-bagian berikut: sebagai permulaan, kita akan memeriksa apa pola "spesifikasi" dan mengapa penerapannya pada sampel basis data murni menyebabkan kesulitan.
Selanjutnya, kita beralih ke pohon ekspresi, yang merupakan alat yang sangat kuat, dan melihat bagaimana mereka dapat membantu kita.
pada akhirnya, saya akan menunjukkan penerapan "spesifikasi" saya pada steroid.
Mari kita mulai dengan hal-hal dasar. Saya pikir semua orang telah mendengar tentang pola "spesifikasi", tetapi bagi mereka yang belum mendengar, inilah definisi dari
Wikipedia :
"Spesifikasi" dalam pemrograman adalah pola desain yang dengannya representasi aturan logika bisnis dapat diubah menjadi rantai objek yang terhubung oleh operasi logika Boolean.
Templat ini menyoroti spesifikasi (aturan) dalam logika bisnis yang cocok untuk "digabungkan" dengan yang lain. Objek logika bisnis mewarisi fungsinya dari kelas agregat abstrak CompositeSpecification, yang hanya berisi satu metode IsSatisfiedBy yang mengembalikan nilai Boolean. Setelah instantiasi, objek dirantai bersama dengan objek lain. Hasilnya, tanpa kehilangan fleksibilitas dalam menyiapkan logika bisnis, kami dapat dengan mudah menambahkan aturan baru.
Dengan kata lain, spesifikasi adalah objek yang mengimplementasikan antarmuka berikut (membuang metode untuk membangun rantai):
public interface ISpecification { bool IsSatisfiedBy(object candidate); }
Semuanya sederhana dan jelas di sini. Tapi sekarang mari kita lihat contoh dari dunia nyata, di mana, selain domain, ada infrastruktur yang juga orang yang kejam: mari kita beralih ke kasus menggunakan ORM, DBMS dan spesifikasi untuk menyaring data dalam database.
Agar tidak berdasar dan tidak menunjukkan jari, kami mengambil sebagai contoh area subjek berikut: misalkan kami sedang mengembangkan MMORPG, kami memiliki pengguna, setiap pengguna memiliki 1 atau lebih karakter, dan setiap karakter memiliki serangkaian item ( kami membuat asumsi bahwa item tersebut unik untuk setiap pengguna), dan untuk setiap item, pada gilirannya, rune perbaikan dapat diterapkan. Secara total, dalam bentuk diagram (kami akan mempertimbangkan kelas ReadCharacter sesaat kemudian ketika kita berbicara tentang kueri yang bersarang):

Model ini secara longgar terhubung dengan dunia nyata, dan itu juga berisi bidang yang mencerminkan beberapa koneksi dengan ORM yang digunakan, tetapi ini akan cukup bagi kita untuk menunjukkan pekerjaan.
Misalkan kita ingin menyaring semua karakter yang dibuat setelah tanggal yang ditentukan.
Untuk melakukan ini, kami menulis spesifikasi bentuk berikut:
public class CreatedAfter: ISpecification { private readonly DateTime _target; public CreatedAfter(DateTime target) { _target = target; } bool IsSatisfiedBy(object candidate) { var character = candidate as Character; if(character == null) return false; return character.CreatedAt > target; } }
Nah, kemudian, untuk menerapkan spesifikasi ini, kami melakukan hal berikut (selanjutnya saya akan mempertimbangkan kode berbasis NHibernate):
var characters = await session.Query<Character>().ToListAsync(); var filter = new CreatedAfter(new DateTime(2020, 1, 1)); var newCharacters = characters.Where(x => filter.IsSatisfiedBy(x)).ToArray();
Selama basis kami kecil, semuanya akan bekerja dengan indah dan cepat, tetapi jika permainan kami menjadi lebih atau kurang populer dan mendapatkan beberapa puluh ribu pengguna, semua pesona ini akan memakan memori, waktu dan uang, dan lebih baik untuk menembak binatang ini segera. karena dia bukan penyewa. Pada catatan sedih ini, kami akan menunda spesifikasi dan sedikit mengubah praktik saya.
Sekali waktu, dalam satu proyek yang sangat, sangat jauh, saya memiliki kelas dalam kode saya yang berisi logika untuk mengambil data dari database. Mereka terlihat seperti ini:
public class ICharacterDal { IEnumerable<Character> GetCharactersCreatedAfter(DateTime date); IEnumerable<Character> GetCharactersCreatedBefore(DateTime date); IEnumerable<Character> GetCharactersCreatedBetween(DateTime from, DateTime to); ... }
dan penggunaannya:
var dal = new CharacterDal(); var createdCharacters = dal.GetCharactersCreatedAfter(new DateTime(2020, 1, 1));
Di dalam kelas ada logika untuk bekerja dengan DBMS (pada waktu itu ADO.NET).
Segalanya tampak menyenangkan, tetapi dengan perluasan proyek, kelas-kelas ini juga tumbuh, berubah menjadi objek yang sulit dirawat. Selain itu, ada aftertaste yang tidak menyenangkan - tampaknya menjadi aturan bisnis, tetapi mereka disimpan di tingkat infrastruktur, karena mereka terikat dengan implementasi tertentu.
Pendekatan ini digantikan oleh repositori
IQueryable <T> , yang memungkinkan untuk mengambil semua aturan secara langsung ke lapisan domain.
public interface IRepository<T> { T Get(object id); IQueryable<T> List(); void Delete(T obj); void Save(T obj); }
yang digunakan seperti ini:
var repository = new Repository(); var targetDate = new DateTime(2020, 1, 1); var createdUsers = await repository.List().Where(x => x.CreatedAd > targetDate).ToListAsync();
Sedikit lebih bagus, tetapi masalahnya adalah bahwa aturan merayap di sepanjang kode, dan pemeriksaan yang sama dapat terjadi di ratusan tempat, dan mudah untuk membayangkan apa yang dapat mengakibatkan perubahan persyaratan.
Pendekatan ini menyembunyikan masalah lain - jika Anda tidak mematerialisasikan kueri, yaitu kesempatan untuk memenuhi beberapa pertanyaan ke database, alih-alih satu, yang, tentu saja, mempengaruhi kinerja sistem.
Dan di sini di salah satu proyek, seorang rekan menyarankan menggunakan
perpustakaan yang menyarankan penerapan pola "spesifikasi" berdasarkan pohon ekspresi.
Singkatnya, berdasarkan pustaka ini, kami memfilmkan spesifikasi yang memungkinkan kami membuat filter untuk entitas dan membuat filter yang lebih kompleks berdasarkan gabungan aturan sederhana. Misalnya, kami memiliki spesifikasi untuk karakter yang dibuat setelah tahun baru dan ada spesifikasi untuk memilih karakter dengan item tertentu - kemudian dengan menggabungkan aturan ini kami dapat membuat permintaan untuk daftar karakter yang dibuat setelah tahun baru dan memiliki item yang ditentukan. Dan jika di masa depan kita akan mengubah aturan untuk menentukan karakter baru (misalnya, kita akan menggunakan tanggal tahun baru Cina), maka kita akan memperbaikinya hanya dalam spesifikasi itu sendiri dan tidak perlu mencari semua penggunaan logika ini dengan kode!
Proyek ini berhasil diselesaikan, dan pengalaman menggunakan pendekatan ini sangat berhasil. Tapi saya tidak mau diam, dan ada beberapa masalah dalam implementasinya, yaitu:
- menempelkan operator ATAU tidak bekerja;
- serikat pekerja hanya berfungsi untuk kueri yang berisi filter bertipe Where, tapi saya ingin aturan yang lebih kaya (kueri bersarang, lewati / ambil, dapatkan proyeksi);
- kode spesifikasi tergantung pada ORM yang dipilih;
- tidak mungkin menggunakan fitur ORM, seperti ini menyebabkan dimasukkannya dependensi di dalamnya dalam lapisan logika bisnis (misalnya, tidak mungkin dilakukan pengambilan).
Hasil dari penyelesaian masalah ini adalah kerangka kerja
Singularis.Spesifikasi , yang terdiri dari beberapa majelis:
- Singularis.Specification.Definition - mendefinisikan objek spesifikasi, dan juga berisi antarmuka IQuery yang dengannya aturan tersebut dibentuk.
- Singularis.Specification.Executor. * - mengimplementasikan repositori dan objek untuk mengeksekusi spesifikasi untuk ORM tertentu (saat ini didukung oleh ef.core dan NHibernate, sebagai bagian dari percobaan saya juga melakukan implementasi untuk mongodb, tetapi kode ini tidak masuk ke produksi).
Mari kita lihat lebih dekat implementasinya.
Antarmuka spesifikasi mendefinisikan properti publik yang berisi aturan spesifikasi:
public interface ISpecification { IQuery Query { get; } Type ResultType { get; } } public interface ISpefication<T>: ISpecification { }
Selain itu, antarmuka berisi properti
ResultType , yang mengembalikan tipe entitas yang diperoleh sebagai hasil dari kueri.
Implementasinya terkandung dalam kelas
spesifikasi <T> , yang mengimplementasikan properti
ResultType , menghitungnya berdasarkan aturan yang disimpan dalam Query, serta dua metode:
Source () dan
Source <TSource> () . Metode-metode ini berfungsi untuk membentuk sumber aturan.
Source () membuat aturan dengan tipe yang cocok dengan argumen dari kelas spesifikasi, dan
Source <TSource> () memungkinkan Anda untuk membuat aturan untuk kelas arbitrer (digunakan saat membuat kueri bertingkat).
Selain itu, ada juga kelas
SpecificationExtension , yang berisi metode ekstensi untuk permintaan chaining.
Dua jenis bergabung didukung: gabungan (dapat dianggap bergabung dengan kondisi "DAN") dan bergabung dengan kondisi "ATAU".
Mari kita kembali ke contoh kita dan menerapkan dua aturan kita:
public class CreatedAfter: Specification<Character> { public CreatedAfter(DateTime target) { Query = Source().Where(x => x.CreatedAt > target); } } public class CreatedBefore: Specification<Character> { public CreatedBefore(DateTime target) { Query = Source().Where(x => x.CreatedAt < target); } }
dan temukan semua pengguna yang memenuhi kedua aturan:
var specification = new CreatedAfter(new DateTime(2019, 1, 1).Combine(new CreatedBefore(new DateTime(2020, 1, 1)); var users = repository.List(specification);
Menggabungkan dengan metode
Combine mendukung aturan arbitrer. Yang utama adalah bahwa jenis sisi kiri yang dihasilkan bertepatan dengan tipe input sisi kanan. Dengan demikian, Anda dapat membuat aturan yang berisi proyeksi, lewati / ambil untuk paginasi, aturan sortir, ambil, dll.
Aturan Or lebih ketat - itu hanya mendukung rantai yang berisi kondisi penyaringan mana. Pertimbangkan penggunaan contoh: kami menemukan semua karakter yang dibuat sebelum 2000 atau setelah 2020:
var specification = new CreatedAfter(new DateTime(2020, 1, 1).Or(new CreatedBefore(new DateTime(2000, 1, 1)); var users = repository.List(specification );
Antarmuka
IQuery sebagian besar mengulangi antarmuka
IQueryable , jadi seharusnya tidak ada pertanyaan khusus. Marilah kita memikirkan metode tertentu saja:
Fetch / ThenFetch - memungkinkan Anda untuk memasukkan data terkait dalam kueri yang dihasilkan untuk tujuan optimasi. Tentu saja, ini sedikit bengkok ketika kita memiliki fitur implementasi infrastruktur yang memengaruhi aturan bisnis, tetapi, seperti yang saya katakan, kenyataannya adalah abstraksi yang keras dan murni - ini adalah hal yang agak teoretis.
Di mana -
IQuery mendeklarasikan dua kelebihan metode ini, yang satu hanya mengambil ekspresi lambda untuk memfilter dalam bentuk
Ekspresi <Func <T, bool >> , dan yang kedua juga mengambil parameter tambahan
IQueryContext , yang memungkinkan Anda untuk mengeksekusi subqueries bersarang. Mari kita lihat sebuah contoh.
Kami memiliki kelas ReadCharacter dalam model - misalkan model kami disajikan sebagai bagian baca yang berisi data yang didenormalkan dan berfungsi untuk umpan balik cepat, dan bagian tulis yang berisi tautan, data yang dinormalkan, dll. Kami ingin menampilkan semua karakter yang digunakan pengguna untuk mengirim email pada domain tertentu.
public class CharactersForUserWithEmailDomain: Specification<ReadCharacter> { public CharactersForUserWithEmailDomain(string domain) { var usersQuery = Source<User>(x => x.Email.Contains(domain)).Projection(x => x.Id); Query = Source().Where((x, ctx) => ctx.GetQueryResult<int>(usersQuery).Contains(x.Id)); } }
Sebagai hasil dari eksekusi, kueri sql berikut akan dihasilkan:
select readcharac0_.id as id1_3_, readcharac0_.UserId as userid2_3_, readcharac0_.Name as name3_3_ from ReadCharacters readcharac0_ where readcharac0_.UserId in ( select user1_.Id from Users user1_ where user1_.Email like ('%'+@p0+'%') ); @p0 = '@inmagna.ca' [Type: String (4000:0:0)]
Untuk memenuhi semua aturan yang luar biasa ini, antarmuka
IRepository didefinisikan , yang memungkinkan Anda untuk menerima item dengan pengidentifikasi, menerima satu (yang pertama cocok) atau daftar objek sesuai dengan spesifikasi, dan juga menyimpan dan menghapus item dari repositori.
Dengan definisi pertanyaan, kami menemukan, sekarang tetap mengajar ORM kami untuk memahami hal ini.
Untuk melakukan ini, kita akan menganalisis perakitan
Singularis.Infrastructure.NHibernate (untuk ef.core semuanya terlihat sama, hanya dengan spesifik dari ef.core).
Jalur akses data adalah objek Repositori, yang mengimplementasikan antarmuka
IRepository . Dalam hal menerima objek dengan pengidentifikasi, serta untuk memodifikasi penyimpanan (menyimpan / menghapus), kelas ini mengakhiri sesi dan menyembunyikan implementasi spesifik dari lapisan bisnis. Dalam hal bekerja dengan spesifikasi, ia membentuk objek yang dapat
IQuery yang mencerminkan permintaan kami dalam hal
IQuery , dan kemudian mengeksekusinya pada objek sesi.
Sihir utama dan kode paling jelek terletak pada kelas yang bertanggung jawab untuk mengubah
IQuery menjadi
IQueryable - SpecificationExecutor. Kelas ini berisi banyak refleksi, yang memanggil metode Queryable atau metode ekstensi ORM tertentu (EagerFetchingExtensionsMethods untuk NHiberante).
Perpustakaan ini secara aktif digunakan dalam proyek-proyek kami (jujur, perpustakaan yang sudah diperbarui digunakan untuk proyek-proyek kami, tetapi secara bertahap semua perubahan ini akan dibuat tersedia untuk umum) terus-menerus mengalami perubahan. Hanya beberapa minggu yang lalu, versi berikutnya dirilis, yang beralih ke metode asinkron, bug diperbaiki di executor untuk e.core, tes dan sampel ditambahkan. Ada kemungkinan bahwa perpustakaan berisi kesalahan dan seratus tempat untuk optimasi - itu lahir sebagai proyek sampingan dalam kerangka kerja pada proyek-proyek utama, jadi saya akan dengan senang hati menyarankan saran untuk perbaikan. Selain itu, Anda tidak boleh terburu-buru menggunakannya - kemungkinan bahwa dalam kasus khusus Anda ini tidak perlu atau tidak dapat diterapkan.
Kapan layak menggunakan solusi yang dijelaskan? Mungkin lebih mudah untuk memulai dari pertanyaan "kapan seharusnya tidak":
- highload - jika Anda membutuhkan kinerja tinggi, penggunaan ORM sendiri menimbulkan pertanyaan. Meskipun, tentu saja, tidak ada yang melarang menerapkan pelaksana yang akan menerjemahkan permintaan menjadi SQL dan mengeksekusi mereka ...
- proyek yang sangat kecil - ini sangat subyektif, tetapi Anda harus mengakui bahwa menarik ORM dan seluruh kebun binatang yang menyertainya ke dalam proyek "daftar tugas" tampak seperti menembak burung pipit dari meriam.
Bagaimanapun, yang menguasai membaca sampai akhir - terima kasih atas waktu Anda. Saya berharap umpan balik untuk pengembangan di masa depan!
Saya hampir lupa - kode proyek tersedia di GitHub'e -
https://github.com/SingularisLab/singularis.specificationSidang tersedia untuk diunduh melalui nuget