Mempertanyakan salah satu argumen untuk kerangka kerja injeksi ketergantungan: Mengapa membuat grafik objek sulit?


13

Kerangka kerja injeksi ketergantungan seperti Google Guice memberikan motivasi berikut untuk penggunaannya ( sumber ):

Untuk membangun objek, pertama-tama Anda membangun dependensinya. Tetapi untuk membangun setiap ketergantungan, Anda membutuhkan dependensinya, dan sebagainya. Jadi ketika Anda membangun objek, Anda benar-benar perlu membuat grafik objek.

Membuat grafik objek dengan tangan membutuhkan banyak tenaga (...) dan membuat pengujian menjadi sulit.

Tapi saya tidak membeli argumen ini: Bahkan tanpa kerangka kerja ketergantungan injeksi, saya bisa menulis kelas yang mudah untuk instantiate dan nyaman untuk diuji. Misalnya, contoh dari halaman motivasi Guice dapat ditulis ulang dengan cara berikut:

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

Jadi mungkin ada argumen lain untuk kerangka kerja injeksi ketergantungan ( yang berada di luar cakupan untuk pertanyaan ini !), Tetapi pembuatan grafik objek yang mudah diuji bukan salah satunya, bukan?


1
Saya berpikir bahwa keuntungan lain dari injeksi ketergantungan yang sering diabaikan adalah memaksa Anda untuk mengetahui objek bergantung pada apa . Tidak ada yang muncul secara ajaib di objek, Anda menyuntikkan dengan atribut yang ditandai secara eksplisit atau melalui konstruktor secara langsung. Belum lagi bagaimana hal itu membuat kode Anda lebih dapat diuji.
Benjamin Gruenbaum

Perhatikan bahwa saya tidak mencari keuntungan lain dari injeksi ketergantungan - Saya hanya tertarik untuk memahami argumen bahwa "instantiasi objek sulit tanpa injeksi ketergantungan"
oberlies

Jawaban ini tampaknya memberikan contoh yang sangat bagus tentang mengapa Anda ingin semacam wadah untuk grafik objek Anda (singkatnya, Anda tidak berpikir cukup besar, hanya menggunakan 3 objek).
Izkata

@Izkata Tidak, ini bukan masalah ukuran. Dengan pendekatan yang dideskripsikan, kode dari pos itu akan menjadi new ShippingService().
Oberlies

@oberlies Still is; Anda kemudian harus menggali definisi kelas 9 untuk mencari tahu kelas mana yang digunakan untuk ShippingService itu, daripada satu lokasi
Izkata

Jawaban:


12

Ada perdebatan lama yang terus-menerus tentang cara terbaik untuk melakukan injeksi ketergantungan.

  • Potongan asli pegas yang dipakai objek polos, dan kemudian menyuntikkan dependensi melalui metode setter.

  • Tetapi kemudian sejumlah besar orang bersikeras bahwa menyuntikkan dependensi melalui parameter konstruktor adalah cara yang tepat untuk melakukannya.

  • Kemudian, akhir-akhir ini, ketika menggunakan refleksi menjadi lebih umum, menetapkan nilai-nilai anggota pribadi secara langsung, tanpa argumen setters, menjadi kemarahan.

Jadi konstruktor pertama Anda konsisten dengan pendekatan kedua untuk injeksi ketergantungan. Hal ini memungkinkan Anda untuk melakukan hal-hal bagus seperti menyuntikkan mock untuk pengujian.

Tetapi konstruktor no-argumen memiliki masalah ini. Karena ini adalah instantiate kelas implementasi untuk PaypalCreditCardProcessordan DatabaseTransactionLog, itu menciptakan ketergantungan, waktu kompilasi yang sulit pada PayPal dan Database. Dibutuhkan tanggung jawab untuk membangun dan mengonfigurasi seluruh pohon dependensi dengan benar.

  • Bayangkan bahwa prosesor PayPay adalah subsistem yang sangat rumit, dan juga menarik banyak perpustakaan pendukung. Dengan membuat ketergantungan waktu kompilasi pada kelas implementasi itu, Anda membuat tautan yang tidak bisa dipecahkan ke seluruh pohon dependensi itu. Kompleksitas grafik objek Anda baru saja melonjak dengan urutan besarnya, mungkin dua.

  • Banyak dari item-item di pohon dependensi akan transparan, tetapi banyak dari mereka juga perlu instantiated. Kemungkinannya adalah, Anda tidak akan bisa hanya instantiate a PaypalCreditCardProcessor.

  • Selain instantiasi, masing-masing objek akan membutuhkan properti yang diterapkan dari konfigurasi.

Jika Anda hanya memiliki ketergantungan pada antarmuka, dan memungkinkan pabrik eksternal untuk membangun dan menyuntikkan dependensi, mereka yang Anda potong seluruh pohon ketergantungan PayPal, dan kompleksitas kode Anda berhenti di antarmuka.

Ada manfaat lain, seperti untuk menentukan kelas implementasi di dalam konfigurasi (yaitu pada saat runtime daripada waktu kompilasi), atau memiliki spesifikasi ketergantungan yang lebih dinamis yang bervariasi, misalnya, berdasarkan lingkungan (pengujian, integrasi, produksi).

Misalnya, katakanlah bahwa Prosesor PayPal memiliki 3 objek dependen, dan masing-masing dependensi tersebut memiliki dua objek lagi. Dan semua objek tersebut harus menarik properti dari konfigurasi. Kode apa adanya akan memikul tanggung jawab untuk membangun semua itu, mengatur properti dari konfigurasi, dll. - semua masalah yang akan ditangani oleh kerangka kerja DI.

Pada awalnya mungkin tidak terlihat jelas apa yang Anda lindungi dengan menggunakan kerangka kerja DI, tetapi hal itu bertambah dan menjadi sangat jelas dari waktu ke waktu. (lol Saya berbicara dari pengalaman setelah mencoba melakukannya dengan cara yang sulit)

...

Dalam prakteknya, bahkan untuk program yang sangat kecil, saya menemukan saya akhirnya menulis dengan gaya DI, dan memecah kelas menjadi pasangan implementasi / pabrik. Artinya, jika saya tidak menggunakan kerangka kerja DI seperti Spring, saya hanya mengumpulkan beberapa kelas pabrik sederhana.

Itu memberikan pemisahan keprihatinan sehingga kelas saya bisa melakukan hal itu, dan kelas pabrik mengambil tanggung jawab untuk membangun & mengonfigurasi hal-hal.

Bukan pendekatan yang diperlukan, tetapi FWIW

...

Secara umum, pola DI / antarmuka mengurangi kompleksitas kode Anda dengan melakukan dua hal:

  • abstrak dependensi hilir menjadi antarmuka

  • "Mengangkat" dependensi hulu dari kode Anda dan masuk ke semacam wadah

Selain itu, karena instantiasi dan konfigurasi objek adalah tugas yang cukup akrab, kerangka kerja DI dapat mencapai banyak skala ekonomis melalui notasi standar & menggunakan trik seperti refleksi. Menyebarkan kekhawatiran yang sama di sekitar kelas akhirnya menambah lebih banyak kekacauan daripada yang dipikirkan orang.


Kode akan memikul tanggung jawab untuk membangun semua itu - Sebuah kelas hanya bertanggung jawab untuk mengetahui apa implementasi kolaboratornya. Tidak perlu tahu apa yang dibutuhkan kolaborator ini pada gilirannya. Jadi ini tidak sebanyak itu.
Oberlies

1
Tetapi jika konstruktor PayPalProcessor memiliki 2 argumen, dari mana mereka datang?
Rob

Tidak. Sama seperti BillingService, PayPalProcessor memiliki konstruktor zero-arg yang menciptakan kolaborator yang dibutuhkannya sendiri.
oberlies

mengabstraksi dependensi hilir ke antarmuka - Kode contoh melakukan ini juga. Bidang prosesor adalah tipe CreditCardProcessor, dan bukan tipe implementasi.
Oberlies

2
@oberlies: Contoh kode Anda berada di antara dua gaya, gaya argumen konstruktor dan gaya konstruktif argumen nol. Kekeliruannya adalah bahwa zero-args yang dapat dibangun tidak selalu mungkin. Keindahan DI atau Factory adalah bahwa mereka entah bagaimana memungkinkan objek dibangun dengan apa yang tampak seperti konstruktor zero-arg.
rwong

4
  1. Saat Anda berenang di ujung kolam yang dangkal, semuanya "mudah dan nyaman". Setelah Anda melewati selusin atau lebih objek, itu tidak lagi nyaman.
  2. Dalam contoh Anda, Anda telah mengikat proses penagihan Anda selamanya dan sehari ke PayPal. Misalkan Anda ingin menggunakan prosesor kartu kredit yang berbeda? Misalkan Anda ingin membuat prosesor kartu kredit khusus yang dibatasi pada jaringan? Atau Anda perlu menguji penanganan nomor kartu kredit? Anda telah membuat kode non-portabel: "tulis sekali, gunakan hanya sekali karena itu tergantung pada grafik objek spesifik yang dirancang."

Dengan mengikat grafik objek Anda di awal proses, yaitu, menghubungkannya ke dalam kode, Anda memerlukan kontrak dan implementasinya untuk hadir. Jika orang lain (mungkin bahkan Anda) ingin menggunakan kode itu untuk tujuan yang sedikit berbeda, mereka harus menghitung ulang seluruh grafik objek dan mengimplementasikannya kembali.

Kerangka DI memungkinkan Anda untuk mengambil banyak komponen dan menyatukannya saat runtime. Ini membuat sistem "modular", terdiri dari sejumlah modul yang berfungsi untuk antarmuka satu sama lain, bukan untuk implementasi satu sama lain.


Ketika saya mengikuti argumen Anda, alasan untuk DI seharusnya "membuat grafik objek dengan tangan itu mudah, tetapi mengarah ke kode yang sulit dipertahankan". Namun klaimnya adalah bahwa "membuat grafik objek dengan tangan itu sulit" dan saya hanya mencari argumen yang mendukung klaim itu. Jadi pos Anda tidak menjawab pertanyaan saya.
oberlies

1
Saya kira jika Anda mengurangi pertanyaan untuk "menciptakan sebuah objek grafik ...", maka itu sangat mudah. Maksud saya adalah bahwa DI mengatasi masalah yang tidak pernah Anda hadapi hanya dengan satu objek grafik; Anda berurusan dengan keluarga mereka.
BobDalgleish

Sebenarnya, hanya membuat satu objek grafik sudah bisa rumit jika kolaborator dibagikan .
oberlies

4

Pendekatan "instantiate my own ownaborators" mungkin bekerja untuk pohon dependensi , tetapi tentu saja tidak akan berfungsi dengan baik untuk grafik dependensi yang merupakan grafik acyclic diarahkan umum (DAG). Dalam DAG dependensi, banyak node dapat menunjuk ke node yang sama - yang berarti bahwa dua objek menggunakan objek yang sama sebagai kolaborator. Kasus ini sebenarnya tidak dapat dibangun dengan pendekatan yang dijelaskan dalam pertanyaan.

Jika beberapa kolaborator saya (atau kolaborator kolaborator) harus berbagi objek tertentu, saya perlu instantiate objek ini dan meneruskannya ke kolaborator saya. Jadi saya sebenarnya perlu tahu lebih banyak dari kolaborator langsung saya, dan ini jelas tidak berskala.


1
Tetapi ketergantungan pohon harus menjadi pohon, dalam arti teori grafik. Kalau tidak, Anda memiliki siklus, yang tidak memuaskan.
Xion

1
@ Xion Grafik ketergantungan harus grafik asiklik terarah. Tidak ada persyaratan bahwa hanya ada satu jalur antara dua node seperti di pohon.
Oberlies

@Xion: Belum tentu. Pertimbangkan UnitOfWorkyang memiliki DbContextrepositori tunggal dan ganda. Semua repositori ini harus menggunakan DbContextobjek yang sama . Dengan usulan "instantiasi diri" OP, itu menjadi tidak mungkin dilakukan.
Flater

1

Saya belum pernah menggunakan Google Guice, tetapi saya telah mengambil banyak waktu untuk memigrasikan aplikasi lama N-tier lama di .Net ke arsitektur IoC seperti Onion Layer yang bergantung pada Ketergantungan Injeksi untuk memisahkan hal-hal.

Mengapa Injeksi Ketergantungan?

Tujuan dari Ketergantungan Injeksi sebenarnya bukan untuk testability, itu sebenarnya untuk mengambil aplikasi yang erat dan melonggarkan kopling sebanyak mungkin. (Yang diinginkan oleh produk membuat kode Anda jauh lebih mudah diadaptasi untuk pengujian unit yang tepat)

Mengapa saya harus khawatir tentang kopling sama sekali?

Kopling atau ketergantungan yang ketat bisa menjadi hal yang sangat berbahaya. (Terutama dalam bahasa yang dikompilasi) Dalam kasus ini Anda dapat memiliki perpustakaan, dll, dll yang sangat jarang digunakan yang memiliki masalah yang secara efektif membuat seluruh aplikasi offline. (Seluruh aplikasi Anda mati karena bagian yang tidak penting memiliki masalah ... ini buruk ... BENAR-BENAR buruk) Sekarang ketika Anda memisahkan hal-hal yang sebenarnya Anda dapat mengatur aplikasi Anda sehingga dapat berjalan bahkan jika DLL atau Perpustakaan itu hilang sama sekali! Yakin bahwa satu bagian yang membutuhkan pustaka atau DLL itu tidak akan berfungsi, tetapi sisa dari chugs aplikasi bisa bahagia.

Mengapa saya perlu Injeksi Ketergantungan untuk pengujian yang tepat

Benar-benar Anda hanya ingin kode yang digabungkan secara longgar, Injeksi Ketergantungan hanya memungkinkan itu. Anda dapat secara bebas memasangkan beberapa hal tanpa IoC, tetapi biasanya lebih banyak pekerjaan dan kurang mudah beradaptasi (saya yakin seseorang di luar sana memiliki pengecualian)

Dalam kasus yang Anda berikan, saya pikir akan jauh lebih mudah untuk hanya mengatur injeksi dependensi sehingga saya dapat mengejek kode saya tidak tertarik untuk menghitung sebagai bagian dari tes ini. Cukup beri tahu Anda metode "Hei, saya tahu saya sudah bilang untuk menelepon repositori, tetapi sebaliknya inilah datanya" seharusnya "mengembalikan kedipan " sekarang karena data itu tidak pernah berubah, Anda tahu Anda hanya menguji bagian yang menggunakan data itu, bukan pengambilan aktual data.

Pada dasarnya ketika menguji Anda ingin Integrasi (fungsionalitas) Pengujian yang menguji sepotong fungsionalitas dari awal hingga selesai, dan pengujian unit lengkap yang menguji setiap bagian kode (biasanya pada tingkat metode atau fungsi) secara independen.

Idenya adalah Anda ingin memastikan seluruh fungsionalitas berfungsi, jika tidak Anda ingin tahu persis bagian kode yang tidak berfungsi.

Ini BISA dilakukan tanpa Injeksi Ketergantungan, tetapi biasanya seiring proyek Anda tumbuh, menjadi semakin sulit untuk melakukannya tanpa Injeksi Ketergantungan. (SELALU berasumsi bahwa proyek Anda akan tumbuh! Lebih baik memiliki praktik keterampilan bermanfaat yang tidak perlu daripada menemukan proyek yang meningkat cepat dan membutuhkan refactoring dan rekayasa ulang yang serius setelah semuanya sudah berjalan.)


1
Menulis tes menggunakan sebagian besar tetapi tidak semua grafik ketergantungan sebenarnya adalah argumen yang baik untuk DI.
oberlies

1
Ini juga patut dicatat dengan DI Anda secara program dapat menukar seluruh potongan kode Anda dengan mudah. Saya telah melihat kasus-kasus di mana suatu sistem akan membersihkan dan menginjeksi ulang antarmuka dengan kelas yang sama sekali berbeda untuk bereaksi terhadap gangguan dan masalah kinerja oleh layanan jarak jauh. Mungkin ada cara yang lebih baik untuk menanganinya, tetapi ternyata bekerja dengan sangat baik.
RualStorge

0

Seperti yang saya sebutkan dalam jawaban lain , masalah di sini adalah bahwa Anda ingin kelas Abergantung pada beberapa kelasB tanpa hard-coding kelas manaB yang digunakan ke dalam kode sumber A. Ini tidak mungkin di Jawa dan C # karena satu-satunya cara untuk mengimpor kelas adalah merujuknya dengan nama yang unik secara global.

Dengan menggunakan antarmuka, Anda dapat mengatasi dependensi kelas yang dikodekan dengan keras, tetapi Anda masih harus menggunakan instance antarmuka, dan Anda tidak dapat memanggil konstruktor atau Anda langsung kembali ke kotak 1. Jadi sekarang kode yang sebaliknya dapat menciptakan ketergantungannya mendorong tanggung jawab itu kepada orang lain. Dan ketergantungannya melakukan hal yang sama. Jadi sekarang setiap kali Anda membutuhkan instance kelas Anda akhirnya membangun seluruh pohon dependensi secara manual, sedangkan dalam kasus di mana kelas A bergantung pada B secara langsung, Anda bisa memanggil new A()dan memanggil konstruktor itu new B(), dan seterusnya.

Kerangka kerja injeksi ketergantungan mencoba untuk mengatasinya dengan membiarkan Anda menentukan pemetaan antara kelas dan membangun pohon ketergantungan untuk Anda. Tangkapannya adalah ketika Anda mengacaukan pemetaan, Anda akan mengetahui saat runtime, bukan pada waktu kompilasi seperti yang Anda lakukan dalam bahasa yang mendukung modul pemetaan sebagai konsep kelas satu.


0

Saya pikir ini adalah kesalahpahaman besar di sini.

Guice adalah kerangka kerja injeksi ketergantungan . Itu membuat DI otomatis . Poin yang mereka buat dalam kutipan yang Anda kutip adalah tentang Guice dapat menghilangkan kebutuhan untuk menciptakan "konstruktor yang dapat diuji" secara manual yang Anda sajikan dalam contoh Anda. Sama sekali tidak ada hubungannya dengan injeksi ketergantungan itu sendiri.

Konstruktor ini:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

sudah menggunakan injeksi ketergantungan. Anda pada dasarnya hanya mengatakan bahwa menggunakan DI itu mudah.

Masalah yang dipecahkan oleh Guice adalah bahwa untuk menggunakan konstruktor itu, Anda sekarang harus memiliki kode konstruktor grafik objek di suatu tempat , secara manual melewati objek yang sudah dipakai sebagai argumen konstruktor itu. Guice memungkinkan Anda untuk memiliki satu tempat di mana Anda dapat mengonfigurasi kelas implementasi nyata apa yang sesuai dengan mereka CreditCardProcessordan TransactionLogantarmuka. Setelah konfigurasi itu, setiap kali Anda membuat BillingServicemenggunakan Guice, kelas-kelas itu akan diteruskan ke konstruktor secara otomatis.

Inilah yang dilakukan oleh kerangka kerja injeksi ketergantungan . Tetapi konstruktor itu sendiri yang Anda sajikan sudah merupakan implementasi dari prinsip injeksi ketergantungan . Kontainer IoC dan kerangka kerja DI adalah sarana untuk mengotomatiskan prinsip-prinsip yang sesuai tetapi tidak ada yang menghentikan Anda untuk melakukan semuanya dengan tangan, itulah intinya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.