Di mana bahaya dimulai? Misalkan Anda sangat yakin bahwa Anda akan mengembangkan suatu proyek, mengikuti konsep atau pendekatan tertentu. Dalam situasi kami, ini adalah DI, meskipun Pemrograman Reaktif, misalnya, mungkin juga ada di tempatnya. Adalah logis bahwa untuk mencapai tujuan Anda, Anda akan beralih ke solusi yang sudah jadi (dalam contoh kami, wadah DI Zenject). Anda akan membiasakan diri dengan dokumentasi dan mulai membangun kerangka kerja aplikasi menggunakan fungsi utama. Jika pada tahap pertama menggunakan solusi Anda tidak memiliki sensasi yang tidak menyenangkan, maka kemungkinan besar itu akan bertahan pada proyek Anda selama seumur hidup. Saat Anda bekerja dengan fungsi dasar solusi (wadah), Anda mungkin memiliki pertanyaan atau keinginan untuk membuat beberapa fungsi lebih indah atau efektif. Tentunya, pertama-tama Anda akan beralih ke "fitur" yang lebih maju dari solusi (wadah) untuk ini. Dan pada tahap ini, situasi berikut mungkin timbul: Anda sudah tahu dengan baik dan percaya pada solusi yang dipilih, karena yang banyak mungkin tidak berpikir tentang bagaimana ideologis mengoreksi penggunaan fungsional satu atau lainnya dalam solusi mungkin, atau transisi ke solusi lain sudah cukup mahal dan tidak pantas ( mis. tenggat waktu semakin dekat). Pada tahap inilah situasi paling berbahaya mungkin muncul - fungsionalitas solusi digunakan dengan sedikit perhatian, atau dalam kasus yang jarang terjadi, hanya pada mesin (tanpa pertimbangan).
Siapa yang bisa tertarik dengan ini?
Artikel ini akan bermanfaat bagi mereka yang terbiasa dengan DI dan para pemula DI. Untuk memahami pengetahuan dasar yang cukup tentang pola apa yang digunakan oleh DI, tujuan DI dan fungsi yang dilakukan wadah IoC. Ini bukan tentang seluk-beluk implementasi Zenject, tetapi tentang penerapan bagian dari fungsinya. Artikel ini hanya bergantung pada dokumentasi resmi Zenject dan contoh kode dari itu, serta pada buku Mark Siman "Dependency Injection in .NET," yang merupakan karya lengkap klasik tentang topik teori DI. Semua kutipan dalam artikel ini adalah kutipan dari buku Mark Siman. Terlepas dari kenyataan bahwa kita akan berbicara tentang wadah tertentu, artikel ini mungkin bermanfaat bagi mereka yang menggunakan wadah lain.
Tujuan artikel ini adalah untuk menunjukkan bagaimana alat yang tujuannya adalah untuk membantu Anda menerapkan DI pada proyek Anda dapat membawa Anda ke arah yang sama sekali berbeda, mendorong Anda untuk membuat kesalahan yang mengikat kode Anda, mengurangi testability kode, dan umumnya menghalangi Anda dari semua keuntungan yang dapat memberikan kamu DI.
Penafian : Tujuan artikel ini bukan untuk mengkritik Zenject atau penulisnya. Zenject dapat digunakan untuk tujuan yang dimaksudkan dan berfungsi sebagai alat yang sangat baik untuk menerapkan DI, asalkan Anda tidak akan menggunakan set lengkap fungsinya, setelah menetapkan beberapa batasan untuk Anda sendiri.
Pendahuluan
Zenject adalah wadah injeksi ketergantungan sumber terbuka yang ditujukan untuk menggunakan mesin permainan Unity3D, yang berfungsi pada sebagian besar platform yang didukung oleh Unity3D. Perlu dicatat bahwa Zenject juga dapat digunakan untuk aplikasi C # yang dikembangkan tanpa Unity3D. Wadah ini cukup populer di kalangan pengembang Unity, didukung dan dikembangkan secara aktif. Selain itu, Zenject memiliki semua fungsi wadah DI yang diperlukan.
Saya menggunakan Zenject di 3 proyek Unity besar, dan juga berkomunikasi dengan sejumlah besar pengembang yang menggunakannya. Alasan penulisan artikel ini adalah pertanyaan yang sering diajukan:
- Apakah menggunakan Zenject merupakan solusi yang baik?
- Apa yang salah dengan Zenject?
- Kesulitan apa yang muncul saat menggunakan Zenject?
Dan juga beberapa proyek di mana penggunaan Zenject tidak mengarah pada solusi masalah konektivitas kode yang kuat dan arsitektur yang gagal, tetapi sebaliknya memperburuk situasi.
Mari kita lihat mengapa pengembang memiliki pertanyaan dan masalah seperti itu. Anda dapat menjawab sebagai berikut:
Ironisnya, wadah DI sendiri cenderung dependensi yang stabil. ... Ketika Anda memutuskan untuk mengembangkan aplikasi Anda berdasarkan wadah DI tertentu, Anda berisiko dibatasi pada pilihan ini untuk seluruh siklus hidup aplikasi.
Perlu dicatat bahwa dengan penggunaan wadah yang tepat dan terbatas, beralih ke menggunakan wadah lain dalam aplikasi (atau menolak untuk menggunakan wadah demi β
implementasi untuk kaum miskin β) sangat mungkin dan tidak akan memakan banyak waktu. Benar, dalam situasi seperti itu, kecil kemungkinan Anda akan membutuhkannya.
Sebelum Anda mulai membongkar fungsionalitas Zenject yang berpotensi berbahaya, masuk akal untuk menyegarkan kembali beberapa aspek dasar DI secara dangkal.
Aspek pertama adalah
tujuan wadah DI. Mark Siman menulis yang berikut dalam bukunya tentang hal ini:
Wadah DI adalah pustaka perangkat lunak yang dapat mengotomatisasi banyak tugas yang dilakukan saat merakit objek dan mengelola siklus hidupnya.
Jangan berharap wadah DI secara ajaib mengubah kode yang digabungkan menjadi kode yang longgar. Wadah dapat meningkatkan efisiensi penggunaan DI, tetapi penekanan dalam aplikasi harus ditempatkan terutama pada penggunaan pola dan bekerja dengan DI.
Aspek kedua adalah
pola DI . Mark Siman mengidentifikasi empat pola utama, diurutkan berdasarkan frekuensi dan kebutuhan penggunaannya:
- Implementasi konstruktor - Bagaimana kita dapat menjamin bahwa ketergantungan yang diperlukan akan selalu tersedia untuk kelas yang sedang dikembangkan?
- Implementasi properti - Bagaimana saya bisa mengaktifkan DI sebagai opsi di kelas jika ada default lokal yang sesuai?
- Implementasi metode - Bagaimana saya bisa menyuntikkan dependensi ke dalam kelas jika mereka berbeda untuk setiap operasi?
- Konteks sekitar - Bagaimana kita bisa membuat ketergantungan tersedia di setiap modul tanpa menyertakan aspek lintas aplikasi dalam setiap komponen API?
Pertanyaan-pertanyaan yang ditunjukkan di sebelah nama pola sepenuhnya menggambarkan ruang lingkup mereka. Pada saat yang sama, artikel tidak akan membahas Implementasi Konstruktor (karena praktis tidak ada keluhan tentang implementasinya di Zenject) dan Konteks Ambient (implementasinya tidak di dalam wadah, tetapi Anda dapat dengan mudah mengimplementasikannya berdasarkan fungsionalitas yang ada).
Sekarang Anda dapat langsung menuju fungsionalitas Zenject yang berpotensi berbahaya.
Fungsionalitas berbahaya.
Implementasikan Properties
Ini adalah pola DI paling umum kedua, setelah implementasi konstruktor, tetapi lebih jarang digunakan. Diimplementasikan dalam Zenject sebagai berikut:
public class Foo { [Inject] public IBar Bar { get; private set; } }
Selain itu, Zenject juga memiliki konsep seperti "Injeksi Lapangan". Mari kita lihat mengapa dalam Zenject semua fungsi ini adalah yang paling berbahaya.
- Atribut digunakan untuk menunjukkan wadah bidang mana yang akan disematkan. Ini adalah solusi yang sepenuhnya dapat dipahami, dari sudut pandang kesederhanaan dan logika implementasi wadah itu sendiri. Namun, kami melihat atribut (dan juga namespace) dalam kode kelas. Itu, setidaknya secara tidak langsung, tetapi kelas mulai tahu dari mana ia mendapat ketergantungan. Plus, kami mulai mengencangkan kode kelas ke wadah. Dengan kata lain, kita tidak bisa lagi menolak untuk menggunakan Zenject tanpa memanipulasi kode kelas.
- Pola itu sendiri digunakan dalam situasi di mana ketergantungan memiliki standar lokal. Artinya, ini adalah ketergantungan opsional, dan jika wadah tidak dapat menyediakannya, maka tidak akan ada kesalahan dalam proyek, dan semuanya akan berfungsi. Namun, menggunakan Zenject, Anda selalu mendapatkan dependensi ini - dependensi menjadi tidak opsional.
- Karena dependensi dalam hal ini bukan opsional, ia mulai merusak seluruh logika implementasi konstruktor, karena hanya dependensi yang diperlukan yang harus diperkenalkan di sana. Dengan menerapkan dependensi non-opsional melalui properti, Anda mendapatkan kesempatan untuk membuat dependensi melingkar dalam kode. Mereka tidak akan begitu jelas, karena di Zenject, implementasi konstruktor pertama kali terpenuhi, dan kemudian implementasi properti, dan Anda tidak akan menerima peringatan dari wadah.
- Menggunakan wadah DI menyiratkan penerapan pola Akar Komposisi, namun, menggunakan atribut untuk mengkonfigurasi implementasi properti mengarah pada fakta bahwa Anda mengonfigurasi kode tidak hanya dalam Komposisi Root, tetapi juga sesuai kebutuhan di setiap kelas.
Pabrik (dan MemoryPool)
Dokumentasi Zenject memiliki seluruh
bagian tentang pabrik. Fungsi ini diimplementasikan pada tingkat wadah itu sendiri, dan dimungkinkan juga untuk membuat pabrik kustom Anda sendiri. Mari kita lihat contoh pertama dari dokumentasi:
public class Enemy { DiContainer Container; public Enemy(DiContainer container) { Container = container; } public void Update() { ... var player = Container.Resolve<Player>(); WalkTowards(player.Position); ... etc. } }
Sudah dalam contoh ini ada pelanggaran berat DI. Tapi ini lebih merupakan contoh bagaimana membuat pabrik yang sepenuhnya custom. Apa masalah utama di sini?
Wadah DI secara keliru dapat dianggap sebagai pencari lokasi layanan, tetapi itu hanya dapat digunakan sebagai mekanisme untuk menghubungkan grafik objek. Jika kita mempertimbangkan wadah dari sudut pandang ini, masuk akal untuk membatasi penggunaannya hanya ke akar tata letak. Pendekatan ini memiliki keuntungan penting yang menghilangkan ikatan antara wadah dan sisa kode aplikasi.
Mari kita lihat bagaimana pabrik βbuilt-inβ dari Zenject bekerja. Ada antarmuka IFactory untuk ini, implementasi yang membawa kita ke kelas PlaceholderFactory:
public abstract class PlaceholderFactory<TValue> : IPlaceholderFactory { [Inject] void Construct(IProvider provider, InjectContext injectContext)
Di dalamnya kita melihat parameter InjectContext yang memiliki banyak konstruktor, dari bentuk:
public InjectContext(DiContainer container, Type memberType) : this() { Container = container; MemberType = memberType; }
Dan lagi, kita mendapatkan transfer wadah itu sendiri sebagai ketergantungan ke kelas. Pendekatan ini merupakan pelanggaran berat terhadap DI dan sebagian transformasi wadah menjadi Pencari Layanan.
Selain itu, kelemahan dari solusi ini adalah bahwa wadah digunakan untuk membuat dependensi jangka pendek, dan seharusnya hanya membuat dependensi jangka panjang.
Untuk menghindari pelanggaran seperti itu, penulis wadah dapat sepenuhnya mengecualikan kemungkinan melewati wadah sebagai ketergantungan pada semua kelas terdaftar. Tidak akan sulit untuk menerapkan ini, mengingat bahwa seluruh wadah bekerja dengan cara refleksi dan analisis parameter metode dan konstruktor untuk membuat dan tata letak grafik objek aplikasi.
Implementasi Metode
Logika implementasi Metode di Zenject adalah sebagai berikut: pertama, di semua kelas, konstruktor diimplementasikan, kemudian properti diimplementasikan, dan akhirnya metode diimplementasikan. Pertimbangkan contoh implementasi yang disediakan dalam dokumentasi:
public class Foo { [Inject] public Init(IBar bar, Qux qux) { _bar = bar; _qux = qux; } }
Apa kerugiannya di sini:
- Anda dapat menulis sejumlah metode yang akan diimplementasikan dalam kerangka kerja satu kelas. Jadi, seperti halnya dengan implementasi properti, kami mendapatkan kesempatan untuk membuat sebanyak mungkin dependensi siklik.
- Seperti implementasi properti, implementasi metode diimplementasikan melalui atribut, yang mengaitkan kode Anda dengan kode wadah itu sendiri.
- Implementasi metode dalam Zenject hanya digunakan sebagai alternatif untuk konstruktor, yang nyaman dalam kasus kelas MonoBehavior, tetapi itu benar-benar bertentangan dengan teori yang dijelaskan oleh Mark Siman. Contoh klasik dari penerapan metode kanonik dapat dianggap sebagai penggunaan pabrik (metode pabrik).
- Jika ada beberapa metode yang diperkenalkan di kelas, atau selain metode ada juga konstruktor, ternyata dependensi yang diperlukan untuk kelas akan tersebar di tempat yang berbeda, yang akan mengganggu gambar secara keseluruhan. Yaitu, jika kelas 1 memiliki konstruktor, maka jumlah parameternya dapat dengan jelas menunjukkan apakah ada kesalahan desain di kelas dan apakah prinsip tanggung jawab tunggal dilanggar, dan jika dependensi tersebar dengan beberapa metode, oleh konstruktor, atau mungkin oleh beberapa properti, maka gambar tidak akan sejelas mungkin.
Oleh karena itu, keberadaan implementasi dari implementasi metode dalam wadah, yang bertentangan dengan teori DI, tidak memiliki nilai tambah tunggal. Dengan peringatan besar, nilai tambah hanya dapat dianggap sebagai kemungkinan menggunakan metode yang diterapkan, sebagai konstruktor untuk MonoBehaviour. Tapi ini adalah momen yang agak kontroversial, karena dari sudut pandang logika wadah, pola DI dan perangkat memori internal Unity3D, semua objek MonoBehaviour dalam aplikasi Anda dapat dianggap dikelola oleh sumber daya, dan dalam hal ini, akan jauh lebih efisien untuk mendelegasikan manajemen siklus hidup objek tersebut. bukan wadah DI, tetapi kelas pembantu (baik itu Pembungkus, ViewModel, Fasade, atau yang lainnya).
Binding global
Ini adalah fungsi tambahan yang cukup nyaman yang memungkinkan Anda untuk mengatur binder global yang dapat hidup terlepas dari transisi antar adegan. Anda dapat membaca lebih lanjut
di dokumentasi . Fungsi ini sangat nyaman dan sangat berguna. Perlu dicatat bahwa itu tidak melanggar pola dan prinsip DI, namun memiliki implementasi yang tidak jelas dan jelek. Intinya adalah bahwa Anda membuat jenis cetakan khusus, melampirkan skrip dengan konfigurasi wadah (Pemasang) ke dalamnya dan menyimpannya dalam folder proyek yang ditentukan secara ketat, tanpa kemampuan untuk bergerak ke suatu tempat dan tanpa tautan ke sana. Kerugian dari alat ini hanya terletak pada kesaksiannya. Ketika datang ke installer biasa, semuanya cukup sederhana: Anda memiliki objek di atas panggung, skrip installer menggantung di atasnya. Jika pengembang baru datang ke proyek, penginstal menjadi titik yang sangat baik untuk pencelupan dalam proyek. Berdasarkan satu penginstal, pengembang dapat membuat ide tentang modul apa proyek terdiri dan bagaimana grafik objek dibangun. Tetapi dengan penggunaan pengikat global, penginstal di atas panggung tidak lagi menjadi sumber informasi yang memadai. Tidak ada satu pun tautan ke pengikatan global dalam kode penginstal lain (ada di layar) dan oleh karena itu, Anda tidak melihat grafik penuh objek. Dan hanya selama analisis kelas Anda mengerti bahwa beberapa pengikat tidak cukup di installer di atas panggung. Sekali lagi saya akan membuat reservasi bahwa kelemahan ini murni kosmetik.
Pengidentifikasi
Kemampuan untuk mengatur ikatan spesifik untuk pengidentifikasi untuk mendapatkan dependensi tertentu dari serangkaian dependensi serupa di kelas. Contoh:
Container.Bind<IFoo>().WithId("foo").To<Foo1>().AsSingle(); Container.Bind<IFoo>().To<Foo2>().AsSingle(); public class Bar1 { [Inject(Id = "foo")] IFoo _foo; } public class Bar2 { [Inject] IFoo _foo; }
Fungsionalitas ini benar-benar bermanfaat secara situasi, dan menjadi opsi tambahan untuk implementasi properti. Namun, seiring dengan kenyamanan, ia mewarisi semua masalah yang diidentifikasi di bagian "Menerapkan Properti", menambahkan lebih banyak koherensi pada kode dengan memperkenalkan konstanta tertentu yang perlu Anda ingat ketika mengkonfigurasi kode Anda. Jika Anda secara tidak sengaja menghapus pengidentifikasi ini, Anda dapat dengan mudah mendapatkan yang tidak berfungsi dari aplikasi yang berfungsi.
Sinyal dan ITickable
Sinyal adalah analog dari mekanisme Agregator Acara yang dibangun di dalam wadah. Gagasan menerapkan fungsi ini tidak diragukan lagi mulia, karena ditujukan untuk mengurangi jumlah koneksi antara objek yang berkomunikasi melalui mekanisme acara-berlangganan. Contoh yang cukup banyak dapat ditemukan dalam
dokumentasi , namun, itu tidak akan ada dalam artikel, karena implementasi spesifik tidak masalah.
Dukungan untuk antarmuka ITickable - mengganti metode standar Perbarui, LateUpdate, dan FixedUpdate di Unity dengan mendelegasikan panggilan ke metode memperbarui objek dengan antarmuka ITickable ke wadah. Contohnya juga dalam
dokumentasi , dan implementasinya dalam konteks artikel juga tidak masalah.
Masalah Sinyal dan ITickable tidak menyangkut aspek implementasi mereka, akarnya terletak pada penggunaan efek samping wadah. Pada intinya, wadah tahu hampir semua kelas dan instans mereka dalam proyek, tetapi tanggung jawabnya adalah membuat grafik objek dan mengelola siklus hidup mereka. Menambahkan mekanisme seperti Sinyal, ITickable, dan sebagainya, kami menambahkan lebih banyak tanggung jawab ke wadah, dan semakin banyak kami melampirkan kode aplikasi ke dalamnya, menjadikannya bagian eksklusif dan tak tergantikan dari kode, praktis "objek ilahi".
Alih-alih output
Hal terpenting tentang wadah adalah untuk memahami bahwa penggunaan DI tidak tergantung pada penggunaan wadah DI. Sebuah aplikasi dapat dibangun dari banyak kelas dan modul yang digabungkan secara longgar, dan tidak satu pun dari modul ini yang tahu apa-apa tentang wadah.
Hati-hati saat menggunakan solusi out-of-the-box (kotak) atau plugin kecil. Gunakan dengan serius. Memang, hal-hal yang lebih muluk yang Anda andalkan (misalnya, mesin game dari skala Unity3D itu sendiri) dapat berdosa dengan kesalahan dan noda teoretis seperti itu. Dan ini, pada akhirnya, tidak akan memengaruhi pekerjaan solusi yang Anda gunakan, tetapi keberlanjutan, pekerjaan, dan kualitas produk akhir Anda. Saya harap semua orang yang telah membaca sampai akhir, artikel ini akan bermanfaat atau, setidaknya, tidak akan menyesal atas waktu yang dihabiskan untuk membacanya.