Ketergantungan injeksi; praktik yang baik untuk mengurangi kode boilerplate

25

Saya punya pertanyaan sederhana, dan saya bahkan tidak yakin punya jawaban tetapi mari kita coba. Saya mengkode dalam C ++, dan menggunakan injeksi ketergantungan untuk menghindari keadaan global. Ini bekerja dengan sangat baik, dan saya tidak sering menjalankan perilaku yang tidak terduga / tidak terdefinisi.

Namun saya menyadari bahwa, seiring pertumbuhan proyek saya, saya menulis banyak kode yang saya anggap boilerplate. Lebih buruk: fakta bahwa ada lebih banyak kode boilerplate, daripada kode aktual membuatnya terkadang sulit untuk dipahami.

Tidak ada yang mengalahkan contoh yang baik jadi mari kita pergi:

Saya memiliki kelas yang disebut TimeFactory yang menciptakan objek Time.

Untuk perincian lebih lanjut (tidak yakin itu relevan): Objek waktu cukup kompleks karena Waktu dapat memiliki format yang berbeda, dan konversi di antara keduanya tidak linier, maupun langsung. Setiap "Waktu" berisi Sinkronisasi untuk menangani konversi, dan untuk memastikan mereka memiliki sinkronisasi yang sama, diinisialisasi dengan benar, saya menggunakan TimeFactory. The TimeFactory hanya memiliki satu instance dan aplikasi yang luas, sehingga akan memenuhi syarat untuk singleton tetapi, karena itu bisa berubah, saya tidak ingin menjadikannya singleton

Di aplikasi saya, banyak kelas perlu membuat objek Waktu. Terkadang kelas-kelas itu sangat bersarang.

Katakanlah saya memiliki kelas A yang berisi instance kelas B, dan seterusnya hingga kelas D. Kelas D perlu membuat objek Waktu.

Dalam implementasi saya yang naif, saya meneruskan TimeFactory ke konstruktor kelas A, yang meneruskannya ke konstruktor kelas B dan seterusnya hingga kelas D.

Sekarang, bayangkan saya memiliki beberapa kelas seperti TimeFactory dan beberapa hierarki kelas seperti yang di atas: Saya kehilangan semua fleksibilitas dan keterbacaan yang saya anggap menggunakan injeksi ketergantungan.

Saya mulai bertanya-tanya apakah tidak ada cacat desain utama di aplikasi saya ... Atau apakah ini kejahatan yang diperlukan dalam menggunakan injeksi ketergantungan?

Apa yang kamu pikirkan ?

Dinaiz
sumber
5
Saya memposting pertanyaan ini ke programmer daripada stackoverflow karena, seperti yang saya mengerti, programmer lebih untuk desain yang berhubungan dengan pertanyaan dan stackoverflow adalah untuk "ketika Anda berada di depan kompiler Anda" -pertanyaan. Maaf sebelumnya jika saya salah.
Dinaiz
2
pemrograman berorientasi aspek, juga baik untuk tugas ini - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov
Apakah pabrik kelas A untuk kelas B, C dan D? Jika tidak mengapa itu membuat contoh dari mereka? Jika hanya kelas D yang membutuhkan objek-waktu, mengapa Anda menyuntikkan kelas lain dengan TimeFactory? Apakah kelas D benar-benar membutuhkan TimeFactory, atau dapatkah hanya instance Time yang disuntikkan ke dalamnya?
simoraman
D perlu membuat objek Waktu, dengan informasi yang seharusnya dimiliki D. Saya harus memikirkan sisa komentar Anda. Mungkin ada masalah dalam "siapa yang menciptakan siapa" dalam kode saya
Dinaiz
"siapa yang menciptakan siapa" diselesaikan oleh wadah IoC, lihat jawaban saya untuk detail .. "siapa yang membuat siapa" adalah salah satu pertanyaan terbaik yang dapat Anda tanyakan saat menggunakan Dependency Injection.
GameDeveloper

Jawaban:

23

Di aplikasi saya, banyak kelas perlu membuat objek Waktu

Tampaknya Timekelas Anda adalah tipe data yang sangat mendasar yang seharusnya menjadi bagian dari "infrastruktur umum" aplikasi Anda. DI tidak berfungsi dengan baik untuk kelas seperti itu. Pikirkan apa artinya jika kelas seperti stringharus disuntikkan ke setiap bagian dari kode yang menggunakan string, dan Anda akan perlu menggunakan stringFactorysebagai satu-satunya kemungkinan membuat string baru - keterbacaan program Anda akan berkurang dengan urutan besarnya.

Jadi saran saya: jangan gunakan DI untuk tipe data umum seperti Time. Tulis tes unit untuk Timekelas itu sendiri, dan ketika selesai, gunakan itu di mana-mana dalam program Anda, seperti stringkelas, atau vectorkelas atau kelas lain dari lib standar. Gunakan DI untuk komponen yang harus benar-benar dipisahkan satu sama lain.

Doc Brown
sumber
Anda benar sepenuhnya. "Keterbacaan program Anda akan berkurang dengan urutan besarnya." : itulah yang terjadi ya. Asalkan saya masih ingin menggunakan pabrik, bukankah itu buruk untuk menjadikannya singleton?
Dinaiz
3
@Dinaiz: idenya adalah untuk menerima hubungan erat antara Timedan bagian lain dari program Anda. Jadi Anda bisa menerima juga menerima kopling ketat TimeFactory. Namun, yang akan saya hindari adalah memiliki satu TimeFactoryobjek global dengan status (misalnya, informasi lokal atau sesuatu seperti itu) - yang dapat menyebabkan efek samping buruk pada program Anda dan membuat pengujian umum dan penggunaan ulang sangat sulit. Entah membuatnya tanpa kewarganegaraan atau tidak menggunakannya sebagai tunggal.
Doc Brown
Sebenarnya itu harus memiliki keadaan, jadi ketika Anda mengatakan "Jangan menggunakannya sebagai singleton", maksud Anda "tetap menggunakan injeksi ketergantungan, yaitu meneruskan contoh ke setiap objek yang membutuhkannya", kan?
Dinaiz
1
@Dinaiz: jujur, itu tergantung - pada jenis negara, pada jenis dan jumlah bagian dari program Anda menggunakan Timedan TimeFactory, pada tingkat "evolvabilitas" yang Anda butuhkan untuk ekstensi di masa depan terkait dengan TimeFactory, dan seterusnya.
Doc Brown
OK saya akan mencoba untuk tetap menyuntikkan ketergantungan tetapi memiliki desain yang lebih pintar yang tidak memerlukan boilerplate begitu banyak kemudian! terima kasih banyak doc '
Dinaiz
4

Apa yang Anda maksud dengan "Saya kehilangan semua fleksibilitas dan keterbacaan yang saya kira harus menggunakan injeksi ketergantungan" - DI bukan tentang keterbacaan. Ini tentang memisahkan ketergantungan antara objek.

Sepertinya Anda memiliki Kelas A yang menciptakan Kelas B, Kelas B menciptakan Kelas C dan Kelas C menciptakan Kelas D.

Apa yang seharusnya Anda miliki adalah Kelas B disuntikkan ke Kelas A. Kelas C disuntikkan ke Kelas B. Kelas D disuntikkan ke Kelas C.

C0deAttack
sumber
DI bisa tentang keterbacaan. Jika Anda memiliki variabel global "ajaib" muncul di tengah metode, itu tidak membantu untuk memahami kode (dari sudut pandang pemeliharaan). Dari mana variabel ini? Siapa pemiliknya? Siapa yang menginisialisasi itu? Dan seterusnya ... Untuk poin kedua Anda, Anda mungkin benar tetapi saya tidak berpikir itu berlaku dalam kasus saya. Terima kasih telah meluangkan waktu untuk menjawab
Dinaiz
DI meningkatkan kewaspadaan karena "baru / hapus" akan secara ajaib dihapus dari kode Anda. Jika Anda tidak perlu memperingatkan tentang kode alokasi dinamis lebih mudah dibaca.
GameDeveloper
Juga lupa mengatakan bahwa ini membuat kode sedikit lebih kuat karena tidak dapat lagi membuat asumsi tentang "bagaimana" ketergantungan dibuat, itu "hanya membutuhkannya". Dan itu membuat kelas lebih mudah dipertahankan .. lihat jawaban saya
GameDeveloper
3

Saya tidak yakin mengapa Anda tidak ingin membuat pabrik waktu Anda lajang. Jika hanya ada satu contoh saja di seluruh aplikasi Anda, itu sebenarnya singleton.

Yang sedang berkata, sangat berbahaya untuk berbagi objek yang bisa berubah, kecuali jika itu dijaga dengan baik dengan menyinkronkan blok, dalam hal ini, tidak ada alasan untuk itu tidak menjadi singleton.

Jika Anda ingin melakukan injeksi dependensi, Anda mungkin ingin melihat pegas atau kerangka kerja injeksi dependensi lainnya, yang memungkinkan Anda untuk menetapkan parameter secara otomatis menggunakan anotasi

molyss
sumber
Spring untuk java dan saya menggunakan C ++. Tapi mengapa 2 orang menurunkan Anda? Ada poin di pos Anda yang saya tidak setuju dengan tetapi masih ...
Dinaiz
Saya akan menganggap downvote itu karena tidak menjawab pertanyaan itu sendiri, mungkin juga karena menyebutkan Spring. Namun, saran menggunakan Singleton adalah yang valid dalam pikiran saya, dan mungkin tidak menjawab pertanyaan - tetapi menyarankan alternatif yang valid dan memunculkan masalah desain yang menarik. Untuk alasan ini saya telah memberi +1.
Fergus In London
1

Ini bertujuan untuk menjadi jawaban pelengkap bagi Doc Brown, dan juga untuk menanggapi komentar Dinaiz yang belum terjawab yang masih terkait dengan Pertanyaan tersebut.

Apa yang mungkin Anda butuhkan adalah kerangka kerja untuk melakukan DI. Memiliki hierarki yang kompleks tidak harus berarti desain yang buruk, tetapi jika Anda harus menyuntikkan bottom-up TimeFactory (dari A ke D) alih-alih menyuntikkan langsung ke D, maka mungkin ada sesuatu yang salah dengan cara Anda melakukan Injeksi Ketergantungan.

Singleton? Tidak, terima kasih. Jika Anda hanya perlu satu istance untuk membagikannya di seluruh konteks aplikasi Anda (Menggunakan wadah IoC untuk DI seperti Infector ++ hanya perlu untuk mengikat TimeFactory sebagai istance tunggal), berikut ini contohnya (C ++ 11 omong-omong, tapi begitu C ++. Mungkin pindah to C ++ 11 sudah? Anda mendapatkan aplikasi Anti Bocor gratis):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Sekarang poin bagus dari wadah IoC adalah Anda tidak perlu melewatkan waktu pabrik hingga D. jika kelas "D" Anda membutuhkan waktu pabrik, cukup cantumkan pabrik waktu sebagai parameter konstruktor untuk kelas D.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

seperti yang Anda lihat Anda menyuntikkan TimeFactory hanya sekali. Bagaimana cara menggunakan "A"? Sangat sederhana, setiap kelas diinjeksi, dibangun di main atau di-istantiated dengan pabrik.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

setiap kali Anda membuat kelas A itu akan secara otomatis (lazy istantiation) disuntikkan dengan semua dependensi hingga D dan D akan disuntikkan dengan TimeFactory, jadi dengan memanggil hanya 1 metode, Anda sudah siap dengan hierarki lengkap Anda (dan bahkan hierarki kompleks diselesaikan dengan cara ini) menghapus BANYAK kode pelat ketel): Anda tidak perlu memanggil "baru / hapus" dan itu sangat penting karena Anda dapat memisahkan logika aplikasi dari kode lem.

D dapat membuat objek Waktu dengan informasi yang hanya dapat dimiliki D

Itu mudah, TimeFactory Anda memiliki metode "buat", lalu gunakan saja tanda tangan berbeda "buat (params)" dan Anda selesai. Parameter yang tidak ada dependensi sering diselesaikan dengan cara ini. Ini juga menghilangkan tugas menyuntikkan hal-hal seperti "string" atau "bilangan bulat" karena itu hanya menambah pelat ketel tambahan.

Siapa yang menciptakan siapa? Kontainer IoC menciptakan pabrik dan pabrik, pabrik menciptakan sisanya (pabrik dapat membuat objek yang berbeda dengan parameter sewenang-wenang, sehingga Anda tidak benar-benar memerlukan keadaan untuk pabrik). Anda masih dapat menggunakan pabrik sebagai pembungkus untuk Kontainer IoC: secara umum, menyuntikkan Kontainer IoC sangat buruk dan sama dengan menggunakan pencari lokasi layanan. Beberapa orang memecahkan masalah dengan membungkus Kontainer IoC dengan sebuah pabrik (ini tidak sepenuhnya diperlukan, tetapi memiliki keuntungan bahwa hierarki diselesaikan oleh Kontainer dan semua pabrik Anda menjadi lebih mudah dipelihara).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Juga jangan menyalahgunakan injeksi ketergantungan, tipe sederhana hanya bisa menjadi anggota kelas atau variabel cakupan lokal. Ini tampak jelas tetapi saya melihat orang menyuntikkan "std :: vector" hanya karena ada kerangka kerja DI yang memungkinkan. Selalu ingat hukum Demeter: "Suntikkan hanya apa yang benar-benar Anda butuhkan untuk disuntikkan"

GameDeveloper
sumber
Wow saya tidak melihat jawaban Anda sebelumnya, maaf! Tuan yang sangat informatif! Diperbaharui
Dinaiz
0

Apakah kelas A, B, dan C Anda juga perlu membuat instance Waktu, atau hanya kelas D? Jika hanya kelas D, maka A dan B seharusnya tidak tahu apa-apa tentang TimeFactory. Membuat instance dari TimeFactory di dalam kelas C, dan meneruskannya ke kelas D. Perhatikan bahwa dengan "membuat sebuah instance" Saya tidak harus berarti bahwa kelas C harus bertanggung jawab untuk membuat instance TimeFactory. Ia dapat menerima DClassFactory dari kelas B, dan DClassFactory tahu cara membuat instance Time.

Teknik yang juga sering saya gunakan ketika saya tidak memiliki kerangka kerja DI adalah menyediakan dua konstruktor, satu yang menerima pabrik, dan satu lagi yang menciptakan pabrik default. Yang kedua biasanya memiliki akses yang dilindungi / paket, dan digunakan terutama untuk pengujian unit.

rmaruszewski
sumber
-1

Saya menerapkan kerangka kerja injeksi ketergantungan C ++ lainnya, yang baru-baru ini diusulkan untuk meningkatkan - https://github.com/krzysztof-jusiak/di - perpustakaan bersifat makro kurang (gratis), hanya header, C ++ 03 / C ++ 11 / C ++ 14 perpustakaan menyediakan jenis aman, waktu kompilasi, injeksi ketergantungan konstruktor makro gratis.

krzychoo
sumber
3
Menjawab pertanyaan lama di P.SE adalah cara yang buruk untuk mengiklankan proyek Anda. Pertimbangkan bahwa ada puluhan ribu proyek di luar sana yang mungkin memiliki relevansi dengan beberapa pertanyaan. Jika semua penulis mereka memposting di sini, kualitas jawaban situs akan menurun.