Minggu lalu, saya menulis sebuah artikel tentang kegunaan dari templat Repositori untuk entitas Eloquent , tetapi saya berjanji untuk memberi tahu bagaimana menggunakannya sebagian dengan manfaat. Untuk melakukan ini, saya akan mencoba menganalisis bagaimana template ini biasanya digunakan dalam proyek. Metode minimum yang diperlukan untuk repositori:
<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); }
Namun, dalam proyek nyata, jika diputuskan untuk menggunakan repositori, mereka sering menambahkan metode untuk memilih catatan:
<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Metode-metode ini dapat diimplementasikan melalui lingkup Eloquent, tetapi membebani kelas entitas dengan tanggung jawab untuk mengambil sendiri bukanlah ide yang baik dan menempatkan tanggung jawab ini ke dalam kelas repositori tampaknya logis. Benarkah begitu? Saya secara khusus membagi antarmuka ini secara visual menjadi dua bagian. Bagian pertama dari metode akan digunakan dalam operasi tulis.
Operasi penulisan standar adalah:
- membangun objek baru dan memanggil PostRepository :: save
- PostRepository :: getById , manipulasi entitas dan panggil PostRepository :: save
- panggil PostRepository :: delete
Tidak ada metode pengambilan sampel dalam operasi penulisan. Dalam operasi baca, hanya metode * get yang digunakan. Jika Anda membaca tentang Prinsip Segregasi Antarmuka (huruf I dalam SOLID ), akan menjadi jelas bahwa antarmuka kami terlalu besar dan memenuhi setidaknya dua tanggung jawab yang berbeda. Sudah waktunya untuk membaginya menjadi dua. Metode getById diperlukan di keduanya, tetapi jika aplikasi menjadi lebih rumit, implementasinya akan berbeda. Ini akan kita lihat nanti. Saya menulis tentang kegunaan dari bagian tulis di artikel sebelumnya, jadi dalam hal ini saya hanya melupakannya.
Membaca bagian bagi saya sepertinya tidak begitu berguna, karena bahkan untuk Eloquent dapat ada beberapa implementasi. Bagaimana cara menyebutkan kelas? Anda bisa ReadPostRepository , tetapi sudah ada sedikit hubungannya dengan template Repository . Anda dapat dengan mudah mengirim Pertanyaan :
<?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Implementasinya menggunakan Eloquent cukup sederhana:
<?php final class EloquentPostQueries implements PostQueries { public function getById($id): Post { return Post::findOrFail($id); } public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->limit() ->get(); } public function getTopPosts() { return Post::orderBy('rating', 'desc') ->limit() ->get(); } public function getUserPosts($userId) { return Post::whereUserId($userId) ->orderBy('created_at', 'desc') ->get(); } }
Antarmuka harus dikaitkan dengan implementasi, misalnya di AppServiceProvider :
<?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, EloquentPostQueries::class); } }
Kelas ini sudah berguna. Dia menyadari tanggung jawabnya dengan membongkar pengontrol atau kelas entitas. Di controller, dapat digunakan seperti ini:
<?php final class PostsController extends Controller { public function lastPosts(PostQueries $postQueries) { return view('posts.last', [ 'posts' => $postQueries->getLastPosts(), ]); } }
Metode PostsController :: lastPosts hanya meminta dirinya untuk beberapa implementasi dari PostsQueries dan bekerja dengannya. Di penyedia, kami mengaitkan PostQueries dengan kelas EloquentPostQueries dan kelas ini akan diganti menjadi pengontrol.
Mari kita bayangkan aplikasi kita menjadi sangat populer. Ribuan pengguna per menit membuka halaman dengan publikasi terbaru. Publikasi paling populer juga sangat sering dibaca. Basis data tidak mengatasi beban seperti itu dengan sangat baik, oleh karena itu mereka menggunakan solusi standar - cache. Selain database, nugget data tertentu disimpan dalam repositori yang dioptimalkan untuk operasi tertentu - memcached atau redis .
Logika cache biasanya tidak terlalu rumit, tetapi mengimplementasikannya dalam EloquentPostQueries tidak terlalu benar (setidaknya karena Prinsip Tanggung Jawab Tunggal ). Adalah jauh lebih alami untuk menggunakan template dekorator dan mengimplementasikan caching sebagai dekorasi tindakan utama:
<?php use Illuminate\Contracts\Cache\Repository; final class CachedPostQueries implements PostQueries { const LASTS_DURATION = 10; private $base; private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->base = $base; $this->cache = $cache; } public function getLastPosts() { return $this->cache->remember('last_posts', self::LASTS_DURATION, function(){ return $this->base->getLastPosts(); }); }
Abaikan antarmuka Repositori di konstruktor. Untuk beberapa alasan, mereka memutuskan untuk memanggil antarmuka untuk caching di Laravel.
Kelas CachedPostQueries hanya mengimplementasikan caching. $ this-> cache-> ingat memeriksa untuk melihat apakah entri yang diberikan ada dalam cache dan jika tidak, ia memanggil panggilan balik dan menulis nilai yang dikembalikan ke cache. Tetap hanya untuk mengimplementasikan kelas ini dalam aplikasi. Kami membutuhkan semua kelas yang dalam aplikasi meminta implementasi antarmuka PostQueries untuk menerima turunan dari kelas CachedPostQueries . Namun, CachedPostQueries sendiri harus menerima kelas EloquentPostQueries sebagai parameter di konstruktor, karena tidak dapat bekerja tanpa implementasi "nyata". Ubah AppServiceProvider :
<?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, CachedPostQueries::class); $this->app->when(CachedPostQueries::class) ->needs(PostQueries::class) ->give(EloquentPostQueries::class); } }
Semua keinginan saya dijelaskan secara alami di penyedia. Jadi, kami menerapkan caching untuk permintaan kami hanya dengan menulis satu kelas dan mengubah konfigurasi wadah. Kode untuk sisa aplikasi tidak berubah.
Tentu saja, untuk implementasi penuh caching, masih perlu untuk menerapkan pembatalan sehingga artikel yang dihapus tidak menggantung di situs untuk beberapa waktu lagi, tetapi segera pergi (baru-baru ini menulis artikel tentang caching , ini dapat membantu secara detail).
Intinya: kami menggunakan bukan hanya satu, tetapi dua pola keseluruhan. Templat Segregasi Tanggung Jawab Kueri Perintah (CQRS) menawarkan pemisahan operasi baca dan tulis pada tingkat antarmuka. Saya datang kepadanya melalui Prinsip Segregasi Antarmuka , yang berarti bahwa saya terampil memanipulasi template dan prinsip dan menyimpulkan satu dari yang lain sebagai teorema :) Tentu saja, tidak setiap proyek membutuhkan abstraksi seperti pada sampel entitas, tetapi saya akan berbagi fokus dengan Anda. Pada tahap awal pengembangan aplikasi, Anda cukup membuat kelas PostQueries dengan implementasi reguler melalui Eloquent:
<?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); }
Ketika muncul kebutuhan untuk melakukan caching, Anda dapat dengan mudah membuat antarmuka (atau kelas abstrak) sebagai pengganti kelas PostQueries ini, salin implementasinya ke kelas EloquentPostQueries dan buka skema yang saya jelaskan sebelumnya. Sisa kode aplikasi tidak perlu diubah.
Namun, masih ada masalah dengan menggunakan entitas Post yang sama yang dapat memodifikasi data. Ini bukan CQRS.

Tidak ada yang mengganggu untuk mendapatkan entitas Post dari PostQueries , memodifikasinya dan menyimpan perubahan dengan sederhana -> save () . Dan itu akan berhasil.
Setelah beberapa saat, tim akan beralih ke replikasi master-slave dalam database dan PostQueries akan bekerja dengan replika-baca. Operasi penulisan pada replika baca biasanya diblokir. Kesalahan akan terbuka, tetapi Anda harus bekerja keras untuk memperbaiki semua tiang tembok tersebut.
Solusi yang jelas adalah dengan memisahkan bagian baca dan tulis sepenuhnya. Anda dapat terus menggunakan Eloquent, tetapi dengan membuat kelas untuk model read-only. Contoh: https://github.com/adelf/freelance-example/blob/master/app/ReadModels/ReadModel.php Semua operasi modifikasi data diblokir. Buat model baru, misalnya ReadPost (Anda dapat meninggalkan Post , tetapi memindahkannya ke namespace lain):
<?php final class ReadPost extends ReadModel { protected $table = 'posts'; } interface PostQueries { public function getById($id): ReadPost; }
Model-model seperti itu dapat hanya-baca dan dapat dengan aman di-cache.

Pilihan lain: abaikan Eloquent. Mungkin ada beberapa alasan untuk ini:
- Semua bidang tabel hampir tidak pernah dibutuhkan. Untuk permintaan Posting terakhir , hanya kolom id , judul , dan, misalnya, publish_at biasanya diperlukan. Meminta beberapa publikasi yang berat hanya akan memberikan beban yang tidak perlu pada basis data atau cache. Fasih hanya dapat memilih bidang yang diperlukan, tetapi semua ini sangat implisit. Pelanggan PostQueries tidak tahu persis bidang mana yang dipilih tanpa masuk ke dalam implementasi.
- Caching menggunakan serialisasi secara default. Kelas fasih mengambil terlalu banyak ruang dalam bentuk serial. Jika untuk entitas sederhana ini tidak terlalu terlihat, maka untuk entitas kompleks dengan hubungan ini menjadi masalah (bagaimana Anda bahkan menyeret objek ukuran megabyte dari cache?). Pada satu proyek, kelas biasa dengan bidang publik di cache membutuhkan ruang 10 kali lebih sedikit daripada opsi Eloquent (ada banyak subentitas kecil). Anda dapat melakukan cache atribut hanya ketika melakukan caching, tetapi ini akan sangat mempersulit prosesnya.
Contoh sederhana bagaimana ini terlihat:
<?php final class PostHeader { public int $id; public string $title; public DateTime $publishedAt; } final class Post { public int $id; public string $title; public string $text; public DateTime $publishedAt; } interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Tentu saja, semua ini tampak seperti kerumitan logika yang berlebihan. "Ambil lingkup Eloquent dan semuanya akan baik-baik saja. Kenapa harus semua ini?" Untuk proyek yang lebih sederhana, ini benar. Sama sekali tidak perlu menemukan kembali lingkup. Tetapi ketika proyeknya besar dan beberapa pengembang terlibat dalam pengembangan, yang sering berubah (mereka berhenti dan yang baru datang), aturan mainnya menjadi sedikit berbeda. Kode harus dilindungi secara tertulis sehingga pengembang baru setelah beberapa tahun tidak dapat melakukan kesalahan. Tentu saja, tidak mungkin untuk sepenuhnya mengecualikan probabilitas seperti itu, tetapi perlu untuk mengurangi probabilitasnya. Selain itu, ini adalah dekomposisi sistem yang umum. Anda dapat mengumpulkan semua dekorator dan kelas caching untuk membatalkan cache ke "modul caching" tertentu, sehingga menyimpan sisa aplikasi informasi tentang caching. Saya harus menggeledah melalui permintaan besar yang dikelilingi oleh panggilan cache. Itu menghalangi. Apalagi jika caching logic tidak sesederhana yang dijelaskan di atas.