Haruskah Injeksi Ketergantungan datang dengan mengorbankan Enkapsulasi?


128

Jika saya mengerti dengan benar, mekanisme khas untuk Injeksi Ketergantungan adalah menyuntikkan baik melalui konstruktor kelas atau melalui properti umum (anggota) kelas.

Ini memperlihatkan ketergantungan yang disuntikkan dan melanggar prinsip OOP enkapsulasi.

Apakah saya benar dalam mengidentifikasi pengorbanan ini? Bagaimana Anda menangani masalah ini?

Silakan lihat juga jawaban saya untuk pertanyaan saya sendiri di bawah ini.


7
ini adalah pertanyaan yang sangat cerdas imho
dfa

5
Menjawab pertanyaan ini terlebih dahulu membutuhkan argumen tentang apa arti enkapsulasi . ;)
Jeff Sternal

2
Enkapsulasi dikelola oleh antarmuka. Mereka mengekspos karakteristik esensial dari objek dan menyembunyikan detail seperti dependensi. Hal ini memungkinkan kami untuk 'membuka' kelas hingga taraf tertentu untuk memberikan konfigurasi yang lebih fleksibel.
Lawrence Wagerfield

Jawaban:


62

Ada cara lain untuk melihat masalah ini yang mungkin menarik bagi Anda.

Saat kami menggunakan injeksi IoC / dependensi, kami tidak menggunakan konsep OOP. Memang kami menggunakan bahasa OO sebagai 'tuan rumah', tetapi ide di balik IoC berasal dari rekayasa perangkat lunak berorientasi komponen, bukan OO.

Perangkat lunak komponen adalah semua tentang mengelola dependensi - contoh yang umum digunakan adalah mekanisme .NET's Assembly. Setiap majelis menerbitkan daftar majelis yang dirujuknya, dan ini membuatnya lebih mudah untuk mengumpulkan (dan memvalidasi) bagian-bagian yang diperlukan untuk aplikasi yang berjalan.

Dengan menerapkan teknik serupa di program OO kami melalui IoC, kami bertujuan untuk membuat program lebih mudah untuk dikonfigurasi dan dipelihara. Menerbitkan dependensi (sebagai parameter konstruktor atau apa pun) adalah bagian penting dari ini. Enkapsulasi tidak benar-benar berlaku, karena di dunia berorientasi komponen / layanan, tidak ada 'tipe implementasi' untuk rincian dari mana bocor.

Sayangnya bahasa kami saat ini tidak memisahkan konsep berorientasi objek halus dari konsep berorientasi komponen kasar, jadi ini adalah perbedaan yang harus Anda pegang hanya dalam pikiran Anda :)


18
Enkapsulasi bukan hanya sepotong terminologi mewah. Ini adalah hal yang nyata dengan manfaat nyata, dan tidak masalah jika Anda menganggap program Anda sebagai "berorientasi komponen" atau "berorientasi objek". Enkapsulasi seharusnya melindungi keadaan objek / komponen / layanan Anda / apa pun dari perubahan dengan cara yang tidak terduga, dan IoC menghilangkan sebagian dari perlindungan ini, jadi pasti ada tradeoff.
Ron Inbar

1
Argumen yang diberikan melalui konstruktor masih berada dalam bidang cara yang diharapkan di mana objek dapat "diubah": mereka secara eksplisit diekspos, dan invarian di sekitar mereka ditegakkan. Menyembunyikan informasi adalah istilah yang lebih baik untuk jenis privasi yang Anda maksudkan, @RonInbar, dan itu tidak selalu bermanfaat (membuat pasta lebih sulit untuk dilepaskan ;-)).
Nicholas Blumhardt

2
Inti dari OOP adalah bahwa kusut pasta dipisahkan ke dalam kelas yang terpisah dan bahwa Anda hanya perlu mengacaukannya sama sekali jika itu perilaku kelas tertentu yang ingin Anda modifikasi (ini adalah bagaimana OOP mengurangi kompleksitas). Kelas (atau modul) mengenkapsulasi internal sambil mengekspos antarmuka publik yang nyaman (ini adalah bagaimana OOP memfasilitasi penggunaan kembali). Kelas yang mengekspos ketergantungannya melalui antarmuka menciptakan kompleksitas untuk klien dan hasilnya kurang dapat digunakan kembali. Itu juga secara inheren lebih rapuh.
Neutrino

1
Tidak peduli ke arah mana saya melihatnya, bagi saya tampaknya DI secara serius merusak beberapa manfaat paling berharga dari OOP, dan saya belum pernah menemukan situasi di mana saya benar-benar menemukannya menggunakannya dengan cara yang memecahkan masalah nyata. masalah yang ada.
Neutrino

1
Berikut cara lain untuk membingkai masalah: bayangkan jika .NET assemblies memilih "enkapsulasi" dan tidak menyatakan rakitan mana yang mereka andalkan. Ini akan menjadi situasi yang gila, membaca dokumen dan hanya berharap sesuatu bekerja setelah memuatnya. Mendeklarasikan dependensi pada level itu memungkinkan penanganan perkakas otomatis dengan komposisi aplikasi berskala besar. Anda harus menyipit untuk melihat analoginya, tetapi kekuatan yang sama berlaku di tingkat komponen. Ada trade-off, dan YMMV seperti biasa :-)
Nicholas Blumhardt

29

Ini pertanyaan yang bagus - tetapi pada beberapa titik, enkapsulasi dalam bentuk paling murni perlu dilanggar jika objek tersebut pernah memiliki ketergantungannya terpenuhi. Beberapa penyedia dependensi harus mengetahui bahwa objek tersebut memerlukan Foo, dan penyedia harus memiliki cara untuk menyediakan Fooobjek.

Klasik kasus terakhir ini ditangani seperti yang Anda katakan, melalui argumen konstruktor atau metode penyetel. Namun, ini tidak selalu benar - saya tahu bahwa versi terbaru dari kerangka kerja DI DI di Jawa, misalnya, memungkinkan Anda membuat anotasi bidang pribadi (misalnya dengan @Autowired) dan dependensi akan ditetapkan melalui refleksi tanpa Anda perlu mengekspos dependensi melalui salah satu metode / konstruktor kelas publik. Ini mungkin jenis solusi yang Anda cari.

Yang mengatakan, saya tidak berpikir injeksi konstruktor banyak masalah, baik. Saya selalu merasa bahwa objek harus sepenuhnya valid setelah konstruksi, sehingga apa pun yang mereka butuhkan untuk melakukan perannya (yaitu dalam keadaan valid) harus disediakan melalui konstruktor. Jika Anda memiliki objek yang memerlukan kolaborator untuk bekerja, tampaknya baik bagi saya bahwa konstruktor secara publik mengiklankan persyaratan ini dan memastikannya terpenuhi ketika instance kelas baru dibuat.

Idealnya ketika berhadapan dengan objek, Anda tetap berinteraksi dengan mereka melalui antarmuka, dan semakin banyak Anda melakukan ini (dan memiliki ketergantungan kabel melalui DI), semakin sedikit Anda benar-benar harus berurusan dengan konstruktor sendiri. Dalam situasi ideal, kode Anda tidak berurusan dengan atau bahkan membuat instance kelas yang konkret; jadi itu hanya diberikan IFoomelalui DI, tanpa khawatir tentang apa yang FooImplmengindikasikan konstruktor perlu melakukan tugasnya, dan bahkan tanpa menyadari FooImplkeberadaannya. Dari sudut pandang ini, enkapsulasi sempurna.

Tentu saja ini adalah pendapat, tetapi menurut saya DI tidak selalu melanggar enkapsulasi dan pada kenyataannya dapat membantu dengan memusatkan semua pengetahuan internal yang diperlukan ke satu tempat. Tidak hanya ini hal yang baik dalam dirinya sendiri, tetapi bahkan lebih baik tempat ini di luar basis kode Anda sendiri, jadi tidak ada kode yang Anda tulis perlu tahu tentang dependensi kelas.


2
Poin bagus. Saya menyarankan agar tidak menggunakan @Autowired di bidang pribadi; yang membuat kelas sulit untuk diuji; Bagaimana cara Anda menyuntikkan mock atau stub?
lumpynose

4
Saya tidak setuju. DI memang melanggar enkapsulasi, dan ini bisa dihindari. Misalnya, dengan menggunakan ServiceLocator, yang jelas tidak perlu tahu apa pun tentang kelas klien; hanya perlu tahu tentang implementasi ketergantungan Foo. Tetapi hal terbaik, dalam banyak kasus, adalah dengan hanya menggunakan operator "baru".
Rogério

5
@Rogerio - Bisa dibilang kerangka kerja DI bertindak persis seperti ServiceLocator yang Anda gambarkan; klien tidak tahu apa-apa tentang implementasi Foo, dan alat DI tidak tahu apa-apa tentang klien. Dan menggunakan "baru" jauh lebih buruk karena melanggar enkapsulasi, karena Anda perlu tahu tidak hanya kelas implementasi yang tepat, tetapi juga kelas dan contoh yang tepat dari semua dependensi yang dibutuhkan.
Andrzej Doyle

4
Menggunakan "baru" untuk membuat instance kelas pembantu, yang seringkali bahkan tidak umum, mempromosikan enkapsulasi. Alternatif DI adalah membuat kelas pembantu publik dan menambahkan konstruktor publik atau setter di kelas klien; kedua perubahan akan memecah enkapsulasi yang disediakan oleh kelas helper asli.
Rogério

1
"Ini pertanyaan yang bagus - tetapi pada titik tertentu, enkapsulasi dalam bentuk paling murni perlu dilanggar jika objeknya pernah memiliki ketergantungannya terpenuhi" Dasar pendirian balasan Anda sama sekali tidak benar. Ketika @ Rogério menyatakan bahwa memulai ketergantungan secara internal, dan metode lain di mana objek secara internal memuaskan ketergantungannya sendiri tidak melanggar enkapsulasi.
Neutrino

17

Ini memperlihatkan ketergantungan yang disuntikkan dan melanggar prinsip OOP enkapsulasi.

Yah, terus terang saja, semuanya melanggar enkapsulasi. :) Ini semacam prinsip tender yang harus diperlakukan dengan baik.

Jadi, apa yang melanggar enkapsulasi?

Warisan tidak .

"Karena warisan memaparkan subkelas ke detail implementasi orang tuanya, sering dikatakan bahwa 'warisan memecah enkapsulasi'". (Gang Empat Four 1995: 19)

Pemrograman berorientasi aspek tidak . Sebagai contoh, Anda mendaftar pada panggilan balikMethodCall () dan itu memberi Anda peluang besar untuk menyuntikkan kode ke evaluasi metode normal, menambahkan efek samping aneh dll.

Deklarasi teman di C ++ tidak .

Perluasan kelas di Ruby tidak . Tetapkan ulang metode string di suatu tempat setelah kelas string sepenuhnya ditentukan.

Nah, banyak hal tidak .

Enkapsulasi adalah prinsip yang baik dan penting. Tapi bukan satu-satunya.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
"Itu adalah prinsip saya, dan jika Anda tidak menyukainya ... yah, saya punya yang lain." (Groucho Marx)
Ron Inbar

2
Saya pikir ini adalah intinya. Itu tergantung injeksi vs enkapsulasi. Jadi hanya gunakan injeksi dependen yang memberikan manfaat signifikan. Ini DI mana-mana yang memberi DI nama buruk
Richard Tingle

Tidak yakin apa jawaban yang coba dikatakan ... Bahwa tidak apa-apa untuk melanggar enkapsulasi ketika melakukan DI, bahwa "selalu baik-baik saja" karena akan tetap dilanggar, atau hanya bahwa DI mungkin menjadi alasan untuk melanggar enkapsulasi? Pada catatan lain, hari ini tidak perlu lagi bergantung pada konstruktor atau properti publik untuk menyuntikkan dependensi; alih-alih, kami dapat menyuntikkan ke bidang beranotasi pribadi , yang lebih sederhana (lebih sedikit kode) dan mempertahankan enkapsulasi. Jadi, kita bisa memanfaatkan kedua prinsip ini secara bersamaan.
Rogério

Warisan pada prinsipnya tidak melanggar enkapsulasi, meskipun dapat dilakukan jika kelas induknya ditulis dengan buruk. Poin-poin lain yang Anda ajukan adalah satu paradigma pemrograman yang cukup rumit dan beberapa fitur bahasa yang tidak ada hubungannya dengan arsitektur atau desain.
Neutrino

13

Ya, DI melanggar enkapsulasi (juga dikenal sebagai "menyembunyikan informasi").

Tetapi masalah sebenarnya muncul ketika pengembang menggunakannya sebagai alasan untuk melanggar prinsip KISS (Keep It Short and Simple) dan YAGNI (You Ain't Gonna Need It).

Secara pribadi, saya lebih suka solusi yang sederhana dan efektif. Saya kebanyakan menggunakan operator "baru" untuk instantiate dependensi stateful kapanpun dan dimanapun mereka diperlukan. Sederhana, dikemas dengan baik, mudah dimengerti, dan mudah diuji. Jadi mengapa tidak?


Berpikir ke depan tidak buruk, tapi saya setuju Keep It Simple, Bodoh, terutama jika Anda Tidak Akan Membutuhkannya! Saya telah melihat pengembang membuang-buang siklus karena mereka terlalu merancang sesuatu untuk menjadi bukti di masa depan, dan berdasarkan dugaan, bahkan persyaratan bisnis yang diketahui / dicurigai.
Jacob McKay

5

Wadah / sistem injeksi ketergantungan yang baik akan memungkinkan injeksi konstruktor. Objek dependen akan dienkapsulasi, dan tidak perlu diekspos secara publik sama sekali. Lebih jauh, dengan menggunakan sistem DP, tidak ada kode Anda yang "tahu" detail tentang bagaimana objek dibangun, bahkan mungkin termasuk objek yang sedang dibangun. Ada lebih banyak enkapsulasi dalam hal ini karena hampir semua kode Anda tidak hanya dilindungi dari pengetahuan tentang objek yang dienkapsulasi, tetapi bahkan tidak berpartisipasi dalam konstruksi objek.

Sekarang, saya berasumsi Anda membandingkan terhadap kasus di mana objek yang dibuat membuat objek yang dienkapsulasi sendiri, kemungkinan besar dalam konstruktornya. Pemahaman saya tentang DP adalah bahwa kita ingin mengambil tanggung jawab ini dari objek dan memberikannya kepada orang lain. Untuk itu, "orang lain", yang merupakan wadah DP dalam kasus ini, memang memiliki pengetahuan intim yang "melanggar" enkapsulasi; manfaatnya adalah ia menarik pengetahuan itu keluar dari objek itu sendiri. Seseorang harus memilikinya. Sisa aplikasi Anda tidak.

Saya akan memikirkannya seperti ini: Wadah injeksi ketergantungan / sistem melanggar enkapsulasi, tetapi kode Anda tidak. Bahkan, kode Anda lebih "dienkapsulasi" dari sebelumnya.


3
Jika Anda memiliki situasi di mana objek klien BISA instantiate dependensi secara langsung, lalu mengapa tidak melakukannya? Ini jelas merupakan hal paling sederhana untuk dilakukan, dan tidak serta merta mengurangi testabilitas. Selain kesederhanaan dan enkapsulasi yang lebih baik, ini juga membuatnya lebih mudah untuk memiliki objek stateful daripada lajang stateless.
Rogério

1
Menambahkan apa yang dikatakan oleh Rogério juga berpotensi lebih efisien secara signifikan. Tidak setiap kelas yang pernah dibuat dalam sejarah dunia perlu memiliki setiap satu dari dependensinya yang dipakai untuk keseluruhan umur objek yang dimiliki. Objek menggunakan DI kehilangan kontrol paling mendasar dari ketergantungannya sendiri, yaitu masa hidup mereka.
Neutrino

5

Seperti yang ditunjukkan Jeff Sternal dalam komentar untuk pertanyaan itu, jawabannya sepenuhnya tergantung pada bagaimana Anda mendefinisikan enkapsulasi .

Tampaknya ada dua kubu utama dari apa arti enkapsulasi:

  1. Segala sesuatu yang berhubungan dengan objek adalah metode pada objek. Jadi, sebuah Fileobjek mungkin memiliki metode untuk Save, Print, Display, ModifyText, dll
  2. Objek adalah dunia kecilnya sendiri, dan tidak bergantung pada perilaku luar.

Kedua definisi ini saling bertentangan secara langsung. Jika suatu Fileobjek dapat mencetak dirinya sendiri, itu akan sangat bergantung pada perilaku printer. Di sisi lain, jika ia hanya tahu tentang sesuatu yang dapat dicetak untuk itu ( IFilePrinterantarmuka atau semacam itu), maka Fileobjek tidak harus tahu apa-apa tentang pencetakan, dan dengan bekerja dengannya akan mengurangi ketergantungan pada objek.

Jadi, injeksi dependensi akan merusak enkapsulasi jika Anda menggunakan definisi pertama. Tapi, terus terang saya tidak tahu apakah saya suka definisi pertama - itu jelas tidak skala (jika ya, MS Word akan menjadi satu kelas besar).

Di sisi lain, injeksi ketergantungan hampir wajib jika Anda menggunakan definisi enkapsulasi kedua.


Saya pasti setuju dengan Anda tentang definisi pertama. Itu juga melanggar SoC yang bisa dibilang salah satu dosa utama pemrograman dan mungkin salah satu alasan mengapa ia tidak berskala.
Marcus Stade

4

Itu tidak melanggar enkapsulasi. Anda menyediakan kolaborator, tetapi kelas memutuskan bagaimana menggunakannya. Selama Anda mengikuti Tell, jangan bertanya hal-hal baik-baik saja. Saya menemukan injeksi konstructer lebih disukai, tetapi setter bisa baik-baik saja asalkan mereka pintar. Itu mereka mengandung logika untuk mempertahankan invarian yang diwakili kelas.


1
Karena itu ... bukan? Jika Anda memiliki logger, dan Anda memiliki kelas yang membutuhkan logger, meneruskan logger ke kelas itu tidak melanggar enkapsulasi. Dan itu semua injeksi ketergantungan.
jrockway

3
Saya pikir Anda salah paham enkapsulasi. Misalnya, ambil kelas tanggal yang naif. Secara internal mungkin memiliki variabel instance hari, bulan dan tahun. Jika ini diekspos sebagai setter sederhana tanpa logika, ini akan memecahkan enkapsulasi, karena saya bisa melakukan sesuatu seperti mengatur bulan ke 2 dan hari ke 31. Di sisi lain, jika setter cerdas dan memeriksa invarian, maka semuanya baik-baik saja . Juga perhatikan bahwa dalam versi yang terakhir, saya bisa mengubah penyimpanan menjadi berhari-hari sejak 1/1/1970, dan tidak ada yang menggunakan antarmuka yang perlu diperhatikan ini asalkan saya menulis ulang metode hari / bulan / tahun dengan tepat.
Jason Watkins

2
DI pasti melanggar enkapsulasi / penyembunyian informasi. Jika Anda mengubah ketergantungan internal pribadi menjadi sesuatu yang terpapar di antarmuka publik kelas, maka menurut definisi Anda telah merusak enkapsulasi ketergantungan itu.
Rogério

2
Saya punya contoh konkret di mana saya pikir enkapsulasi dikompromikan oleh DI. Saya memiliki FooProvider yang mendapat "data foo" dari DB dan FooManager yang menyimpannya dan menghitung hal-hal di atas penyedia. Saya memiliki konsumen kode saya keliru pergi ke FooProvider untuk data, di mana saya lebih suka memilikinya dienkapsulasi sehingga mereka hanya mengetahui FooManager. Ini pada dasarnya adalah pemicu untuk pertanyaan awal saya.
urig

1
@Ogerio: Saya berpendapat bahwa konstruktor bukan bagian dari antarmuka publik, karena hanya digunakan di root komposisi. Dengan demikian, ketergantungan hanya "dilihat" oleh komposisi root. Tanggung jawab tunggal root komposisi adalah untuk menyatukan semua dependensi ini. Jadi menggunakan injeksi konstruktor tidak merusak enkapsulasi apa pun.
Jay Sullivan

4

Ini mirip dengan jawaban terangkat tetapi saya ingin berpikir keras - mungkin orang lain melihat hal-hal seperti ini juga.

  • OO Klasik menggunakan konstruktor untuk mendefinisikan kontrak "inisialisasi" publik untuk konsumen kelas (menyembunyikan SEMUA detail implementasi; alias enkapsulasi). Kontrak ini dapat memastikan bahwa setelah instantiasi Anda memiliki objek yang siap digunakan (yaitu tidak ada langkah inisialisasi tambahan untuk diingat (er, dilupakan) oleh pengguna).

  • (konstruktor) DI tidak dapat dipungkiri memecah encapsulation dengan pendarahan implementasi detail melalui antarmuka konstruktor publik ini. Selama kami masih menganggap konstruktor publik yang bertanggung jawab untuk menentukan kontrak inisialisasi untuk pengguna, kami telah menciptakan pelanggaran enkapsulasi yang mengerikan.

Contoh Teoritis:

Class Foo memiliki 4 metode dan membutuhkan integer untuk inisialisasi, sehingga konstruktornya terlihat seperti Foo (ukuran int) dan segera jelas bagi pengguna kelas Foo bahwa mereka harus memberikan ukuran pada instantiasi agar Foo dapat berfungsi.

Katakanlah implementasi khusus Foo ini mungkin juga memerlukan IWidget untuk melakukan tugasnya. Injeksi konstruktor dari dependensi ini meminta kami membuat konstruktor seperti Foo (ukuran int, widget IWidget)

Apa yang mengganggu saya tentang ini adalah sekarang kita memiliki konstruktor yang memadukan data inisialisasi dengan dependensi - satu input menarik bagi pengguna kelas ( ukuran ), yang lain adalah ketergantungan internal yang hanya berfungsi untuk membingungkan pengguna dan merupakan implementasi detail ( widget ).

Parameter ukuran BUKAN ketergantungan - itu sederhana nilai inisialisasi per-instance. IoC bagus untuk dependensi eksternal (seperti widget) tetapi tidak untuk inisialisasi keadaan internal.

Lebih buruk lagi, bagaimana jika Widget hanya diperlukan untuk 2 dari 4 metode pada kelas ini; Saya mungkin mengeluarkan overhead instantiation untuk Widget meskipun mungkin tidak digunakan!

Bagaimana cara kompromi / rekonsiliasi ini?

Salah satu pendekatan adalah beralih secara eksklusif ke antarmuka untuk menentukan kontrak operasi; dan menghapus penggunaan konstruktor oleh pengguna. Agar konsisten, semua objek harus diakses melalui antarmuka saja, dan dipakai hanya melalui beberapa bentuk resolver (seperti wadah IOC / DI). Hanya wadah yang bisa instantiate hal.

Itu menangani ketergantungan Widget, tetapi bagaimana kita menginisialisasi "ukuran" tanpa menggunakan metode inisialisasi terpisah pada antarmuka Foo? Menggunakan solusi ini, kami kehilangan kemampuan untuk memastikan bahwa instance Foo diinisialisasi penuh pada saat Anda mendapatkan instance. Nyebelin, karena saya sangat suka ide dan kesederhanaan injeksi konstruktor.

Bagaimana cara mencapai inisialisasi dijamin di dunia DI ini, ketika inisialisasi LEBIH dari HANYA ketergantungan eksternal?


Pembaruan: Saya baru saja melihat Unity 2.0 mendukung pemberian nilai untuk parameter konstruktor (seperti inisialisasi negara), sementara masih menggunakan mekanisme normal untuk IoC dari dependensi selama resol (). Mungkin wadah lain juga mendukung ini? Itu memecahkan kesulitan teknis pencampuran status init dan DI dalam satu konstruktor, tetapi masih melanggar enkapsulasi!
shawnT

Aku dengar ya Saya mengajukan pertanyaan ini karena saya juga merasa bahwa dua hal baik (DI & enkapsulasi) datang satu dengan mengorbankan yang lain. BTW, dalam contoh Anda di mana hanya 2 dari 4 metode memerlukan IWidget, yang akan menunjukkan bahwa 2 lainnya termasuk dalam komponen IMHO yang berbeda.
urig

3

Enkapsulasi murni adalah cita-cita yang tidak pernah dapat dicapai. Jika semua dependensi disembunyikan maka Anda tidak akan memerlukan DI sama sekali. Pikirkan seperti ini, jika Anda benar-benar memiliki nilai pribadi yang dapat diinternalisasi dalam objek, katakan misalnya nilai integer dari kecepatan objek mobil, maka Anda tidak memiliki ketergantungan eksternal dan tidak perlu membalikkan atau menyuntikkan ketergantungan itu. Nilai-nilai keadaan internal semacam ini yang dioperasikan murni oleh fungsi-fungsi pribadi adalah apa yang selalu ingin Anda enkapsulasi.

Tetapi jika Anda sedang membangun mobil yang menginginkan jenis mesin tertentu maka Anda memiliki ketergantungan eksternal. Anda dapat membuat instance engine tersebut - misalnya GMOverHeadCamEngine () baru - secara internal di dalam konstruktor objek mobil, menjaga enkapsulasi tetapi membuat kopling yang jauh lebih berbahaya ke kelas beton GMOverHeadCamEngine, atau Anda dapat menyuntikkannya, memungkinkan objek Mobil Anda beroperasi secara agnostik (dan jauh lebih kuat) misalnya antarmuka IEngine tanpa ketergantungan konkret. Apakah Anda menggunakan wadah IOC atau DI sederhana untuk mencapai ini bukan itu intinya - intinya adalah bahwa Anda punya Mobil yang dapat menggunakan berbagai jenis mesin tanpa digabungkan ke salah satu dari mereka, sehingga membuat basis kode Anda lebih fleksibel dan kurang rentan terhadap efek samping.

DI bukan merupakan pelanggaran enkapsulasi, ini adalah cara meminimalkan penggandengan ketika enkapsulasi harus dipecah sebagai hal yang biasa dalam setiap proyek OOP. Menyuntikkan ketergantungan ke antarmuka eksternal meminimalkan efek samping kopling dan memungkinkan kelas Anda untuk tetap agnostik tentang implementasi.


3

Itu tergantung pada apakah ketergantungan itu benar-benar detail implementasi atau sesuatu yang ingin / perlu diketahui klien dengan cara tertentu. Satu hal yang relevan adalah tingkat abstraksi yang ditargetkan oleh kelas. Berikut ini beberapa contohnya:

Jika Anda memiliki metode yang menggunakan caching di bawah tenda untuk mempercepat panggilan, maka objek cache harus berupa Singleton atau sesuatu dan tidak boleh disuntikkan. Fakta bahwa cache sedang digunakan sama sekali adalah detail implementasi yang tidak perlu dipedulikan oleh klien kelas Anda.

Jika kelas Anda perlu menampilkan aliran data, mungkin masuk akal untuk menyuntikkan aliran output sehingga kelas dapat dengan mudah menampilkan hasil ke array, file, atau di mana pun orang lain mungkin ingin mengirim data.

Untuk area abu-abu, katakanlah Anda memiliki kelas yang melakukan beberapa simulasi monte carlo. Perlu sumber keacakan. Di satu sisi, fakta bahwa ini membutuhkan ini adalah detail implementasi di mana klien benar-benar tidak peduli dari mana asal keacakan. Di sisi lain, karena generator bilangan acak dunia nyata menghasilkan pertukaran antara tingkat keacakan, kecepatan, dll. Yang mungkin ingin dikendalikan oleh klien, dan klien mungkin ingin mengendalikan penyemaian untuk mendapatkan perilaku berulang, injeksi mungkin masuk akal. Dalam hal ini, saya akan menyarankan menawarkan cara membuat kelas tanpa menentukan generator nomor acak, dan menggunakan Singleton-thread lokal sebagai default. Jika / ketika kebutuhan untuk kontrol yang lebih baik muncul, sediakan konstruktor lain yang memungkinkan sumber keacakan untuk diinjeksi.


2

Saya percaya pada kesederhanaan. Menerapkan IOC / Dependecy Injection di kelas Domain tidak membuat perbaikan apa pun kecuali membuat kode lebih sulit untuk dipelihara dengan memiliki file xml eksternal yang menggambarkan relasinya. Banyak teknologi seperti EJB 1.0 / 2.0 & struts 1.1 membalikkan kembali dengan mengurangi hal-hal yang dimasukkan ke dalam XML dan mencoba memasukkannya ke dalam kode sebagai pengumuman dll. Jadi menerapkan IOC untuk semua kelas yang Anda kembangkan akan membuat kode tersebut tidak masuk akal.

IOC memilikinya manfaat ketika objek dependen tidak siap untuk dibuat pada waktu kompilasi. Hal ini dapat terjadi di sebagian besar komponen arsitektur tingkat abstrak infrasture, mencoba membangun kerangka dasar bersama yang mungkin perlu untuk skenario yang berbeda. Di tempat-tempat itu, penggunaan IOC lebih masuk akal. Namun ini tidak membuat kode lebih sederhana / dapat dipelihara.

Seperti semua teknologi lainnya, ini juga memiliki PRO & KONS. Kekhawatiran saya adalah, kami menerapkan teknologi terbaru di semua tempat terlepas dari penggunaan konteks terbaik mereka.


2

Enkapsulasi hanya rusak jika kelas memiliki kedua tanggung jawab untuk membuat objek (yang membutuhkan pengetahuan tentang detail implementasi) dan kemudian menggunakan kelas (yang tidak membutuhkan pengetahuan tentang detail ini). Saya akan menjelaskan alasannya, tetapi pertama-tama anaologi mobil cepat:

Ketika saya mengendarai Kombi 1971 saya yang lama, saya bisa menekan pedal gas dan itu (sedikit) lebih cepat. Saya tidak perlu tahu mengapa, tetapi orang-orang yang membangun Kombi di pabrik tahu persis mengapa.

Tetapi kembali ke pengkodean. Enkapsulasi adalah "menyembunyikan detail implementasi dari sesuatu yang menggunakan implementasi itu." Enkapsulasi adalah hal yang baik karena detail implementasi dapat berubah tanpa diketahui pengguna kelas.

Saat menggunakan injeksi dependensi, injeksi konstruktor digunakan untuk membangun objek tipe layanan (sebagai lawan dari entitas / nilai objek yang menyatakan model). Setiap variabel anggota dalam objek jenis layanan mewakili detail implementasi yang tidak boleh bocor. mis. nomor port soket, kredensial basis data, kelas lain untuk dipanggil untuk melakukan enkripsi, cache, dll.

The konstruktor relevan ketika kelas sedang awalnya dibuat. Ini terjadi selama fase konstruksi saat kontainer DI Anda (atau pabrik) menyatukan semua objek servis. Wadah DI hanya tahu tentang detail implementasi. Ia tahu semua tentang detail implementasi seperti orang-orang di pabrik Kombi tahu tentang busi.

Pada saat run-time, objek layanan yang dibuat disebut apon untuk melakukan beberapa pekerjaan nyata. Pada saat ini, penelepon objek tidak mengetahui detail implementasi.

Itu saya yang mengendarai Kombi saya ke pantai.

Sekarang, kembali ke enkapsulasi. Jika detail implementasi berubah, maka kelas yang menggunakan implementasi itu pada saat run-time tidak perlu diubah. Enkapsulasi tidak rusak.

Saya juga bisa mengendarai mobil baru ke pantai. Enkapsulasi tidak rusak.

Jika detail implementasi berubah, wadah DI (atau pabrik) perlu diubah. Anda tidak pernah mencoba menyembunyikan detail implementasi dari pabrik sejak awal.


Bagaimana Anda menguji unit pabrik Anda? Dan itu berarti klien perlu tahu tentang pabrik untuk mendapatkan mobil yang berfungsi, yang berarti Anda membutuhkan pabrik untuk setiap objek lain di sistem Anda.
Rodrigo Ruiz

2

Setelah berjuang dengan masalah ini sedikit lebih jauh, saya sekarang berpendapat bahwa Injeksi Ketergantungan tidak (pada saat ini) melanggar enkapsulasi sampai tingkat tertentu. Tapi jangan salah paham - saya pikir menggunakan injeksi ketergantungan layak untuk tradeoff dalam kebanyakan kasus.

Kasus mengapa DI melanggar enkapsulasi menjadi jelas ketika komponen yang Anda kerjakan akan dikirim ke pihak "eksternal" (pikirkan menulis perpustakaan untuk pelanggan).

Ketika komponen saya memerlukan sub-komponen yang akan disuntikkan melalui konstruktor (atau properti publik) tidak ada jaminan untuk

"mencegah pengguna mengatur data internal komponen menjadi keadaan tidak valid atau tidak konsisten".

Pada saat yang sama tidak dapat dikatakan demikian

"Pengguna komponen (perangkat lunak lain) hanya perlu tahu apa yang komponen itu lakukan, dan tidak dapat membuat diri mereka bergantung pada detail bagaimana melakukannya" .

Kedua kutipan tersebut berasal dari wikipedia .

Untuk memberikan contoh spesifik: Saya perlu memberikan DLL sisi klien yang menyederhanakan dan menyembunyikan komunikasi ke layanan WCF (pada dasarnya fasad jarak jauh). Karena ini tergantung pada 3 kelas proxy WCF yang berbeda, jika saya mengambil pendekatan DI, saya dipaksa untuk mengeksposnya melalui konstruktor. Dengan itu saya mengekspos internal lapisan komunikasi saya yang saya coba sembunyikan.

Secara umum saya semua untuk DI. Dalam contoh khusus (ekstrem) ini, menurut saya berbahaya.


2

DI melanggar Enkapsulasi untuk objek NON-Dibagikan - titik. Objek yang dibagikan memiliki rentang hidup di luar objek yang sedang dibuat, dan karenanya harus AGREGASI ke dalam objek yang sedang dibuat. Objek yang bersifat pribadi untuk objek yang sedang dibuat harus dikomposisikan ke dalam objek yang dibuat - ketika objek yang dibuat dihancurkan, dibutuhkan objek yang disusun dengannya. Mari kita ambil tubuh manusia sebagai contoh. Apa yang dikomposisikan dan apa yang dikumpulkan. Jika kita menggunakan DI, konstruktor tubuh manusia akan memiliki 100 objek. Banyak organ, misalnya, yang berpotensi diganti. Tapi, mereka masih tersusun dalam tubuh. Sel darah dibuat dalam tubuh (dan dihancurkan) setiap hari, tanpa perlu pengaruh eksternal (selain protein). Dengan demikian, sel-sel darah dibuat secara internal oleh tubuh - BloodCell baru ().

Pendukung DI berpendapat bahwa suatu objek TIDAK PERNAH harus menggunakan operator baru. Pendekatan "murni" itu tidak hanya melanggar enkapsulasi tetapi juga Prinsip Substitusi Liskov bagi siapa pun yang menciptakan objek.


1

Saya berjuang dengan gagasan ini juga. Pada awalnya, 'persyaratan' untuk menggunakan wadah DI (seperti Spring) untuk instantiate objek terasa seperti melompat melalui lingkaran. Tetapi pada kenyataannya, itu benar-benar bukan sebuah lingkaran - itu hanyalah cara lain yang 'dipublikasikan' untuk membuat objek yang saya butuhkan. Tentu, enkapsulasi 'rusak' karena seseorang 'di luar kelas' tahu apa yang dibutuhkannya, tetapi sebenarnya bukan seluruh sistem yang tahu itu - itu adalah wadah DI. Tidak ada yang ajaib terjadi secara berbeda karena DI 'tahu' satu objek membutuhkan yang lain.

Bahkan itu menjadi lebih baik - dengan berfokus pada Pabrik dan Gudang saya bahkan tidak perlu tahu DI terlibat sama sekali! Itu bagi saya menempatkan tutupnya kembali pada enkapsulasi. Wah!


1
Selama DI bertanggung jawab atas seluruh rantai Instansiasi, maka saya setuju bahwa semacam enkapsulasi terjadi. Semacam itu, karena dependensi masih bersifat publik dan dapat disalahgunakan. Tetapi ketika suatu tempat "naik" dalam rantai seseorang perlu instantiate objek tanpa mereka menggunakan DI (mungkin mereka adalah "pihak ketiga") maka itu menjadi berantakan. Mereka terpapar pada ketergantungan Anda dan mungkin tergoda untuk menyalahgunakannya. Atau mereka mungkin tidak ingin tahu tentang mereka sama sekali.
urig

1

PS. Dengan memberikan Injeksi Ketergantungan Anda tidak harus memutus Enkapsulasi . Contoh:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Kode klien masih belum tahu detail implementasi.


Bagaimana dalam contoh Anda sesuatu disuntikkan? (Kecuali untuk penamaan fungsi setter Anda)
foo

1

Mungkin ini adalah cara berpikir naif tentang itu, tetapi apa perbedaan antara konstruktor yang mengambil parameter integer dan konstruktor yang menggunakan layanan sebagai parameter? Apakah ini berarti bahwa mendefinisikan bilangan bulat di luar objek baru dan memasukkannya ke dalam objek memecah enkapsulasi? Jika layanan ini hanya digunakan dalam objek baru, saya tidak melihat bagaimana itu akan memecah enkapsulasi.

Juga, dengan menggunakan semacam fitur autowiring (Autofac untuk C #, misalnya), itu membuat kode sangat bersih. Dengan membangun metode ekstensi untuk pembangun Autofac, saya dapat memotong BANYAK kode konfigurasi DI yang harus saya pertahankan dari waktu ke waktu seiring bertambahnya daftar dependensi.


Ini bukan tentang nilai vs layanan. Ini adalah tentang inversi kontrol - apakah konstruktor kelas mengatur kelas, atau apakah layanan itu mengambil alih pengaturan kelas. Untuk itu perlu mengetahui detail implementasi kelas itu, sehingga Anda punya tempat lain untuk memelihara dan menyinkronkan.
foo

1

Saya pikir itu jelas bahwa setidaknya DI secara signifikan melemahkan enkapsulasi. Selain itu, berikut adalah beberapa kelemahan DI lainnya untuk dipertimbangkan.

  1. Itu membuat kode lebih sulit untuk digunakan kembali. Modul yang dapat digunakan klien tanpa harus secara eksplisit menyediakan dependensi, jelas lebih mudah digunakan daripada modul yang entah bagaimana klien harus menemukan apa dependensi komponen itu dan kemudian membuatnya tersedia. Misalnya komponen yang awalnya dibuat untuk digunakan dalam aplikasi ASP mungkin berharap memiliki dependensinya yang disediakan oleh wadah DI yang menyediakan contoh objek dengan masa hidup yang terkait dengan permintaan http klien. Ini mungkin tidak mudah direproduksi di klien lain yang tidak datang dengan wadah DI built in yang sama dengan aplikasi ASP asli.

  2. Itu bisa membuat kode lebih rapuh. Ketergantungan yang diberikan oleh spesifikasi antarmuka dapat diimplementasikan dengan cara yang tidak terduga yang menimbulkan seluruh kelas bug runtime yang tidak mungkin dengan ketergantungan beton yang diselesaikan secara statis.

  3. Itu bisa membuat kode kurang fleksibel dalam arti bahwa Anda mungkin berakhir dengan lebih sedikit pilihan tentang bagaimana Anda ingin itu berfungsi. Tidak setiap kelas perlu memiliki semua dependensinya untuk seumur hidup dari instance yang dimiliki, namun dengan banyak implementasi DI Anda tidak memiliki pilihan lain.

Dengan mengingat hal itu saya pikir pertanyaan yang paling penting kemudian menjadi, " apakah ketergantungan tertentu perlu ditentukan secara eksternal sama sekali? ". Dalam praktiknya, saya jarang merasa perlu untuk membuat ketergantungan yang diberikan secara eksternal hanya untuk mendukung pengujian.

Di mana ketergantungan benar-benar perlu dipasok secara eksternal, yang biasanya menunjukkan bahwa hubungan antara objek adalah kolaborasi daripada ketergantungan internal, dalam hal ini tujuan yang sesuai kemudian enkapsulasi masing - masing kelas, daripada enkapsulasi satu kelas di dalam yang lain .

Dalam pengalaman saya, masalah utama mengenai penggunaan DI adalah apakah Anda memulai dengan kerangka kerja aplikasi dengan built in DI, atau Anda menambahkan dukungan DI ke basis kode Anda, untuk beberapa alasan orang berasumsi bahwa karena Anda memiliki dukungan DI yang harus benar cara untuk instantiate segalanya . Mereka bahkan tidak pernah repot-repot mengajukan pertanyaan "apakah ketergantungan ini perlu ditentukan secara eksternal?". Dan lebih buruk lagi, mereka juga mulai mencoba memaksa orang lain untuk menggunakan dukungan DI untuk semuanya juga.

Hasil dari ini adalah bahwa basis kode Anda mulai berubah menjadi keadaan di mana membuat instance apa pun dari basis kode Anda memerlukan rim konfigurasi wadah DI yang tumpul, dan men-debug sesuatu adalah dua kali lebih sulit karena Anda memiliki beban kerja ekstra untuk mencoba mengidentifikasi bagaimana dan di mana ada sesuatu yang dipakai.

Jadi jawaban saya untuk pertanyaannya adalah ini. Gunakan DI tempat Anda dapat mengidentifikasi masalah aktual yang dipecahkannya untuk Anda, yang tidak bisa Anda selesaikan dengan cara lain apa pun.


0

Saya setuju bahwa dengan ekstrem, DI dapat melanggar enkapsulasi. Biasanya DI memperlihatkan dependensi yang tidak pernah benar-benar dienkapsulasi. Berikut adalah contoh sederhana yang dipinjam dari Lajang Miško Hevery adalah Pembohong Patologis :

Anda mulai dengan tes CreditCard dan menulis tes unit sederhana.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

Bulan depan Anda mendapatkan tagihan sebesar $ 100. Mengapa Anda ditagih? Tes unit mempengaruhi basis data produksi. Secara internal, panggilan CreditCard Database.getInstance(). Refactoring CreditCard sehingga memerlukan DatabaseInterfacekonstruktor memperlihatkan fakta bahwa ada ketergantungan. Tetapi saya berpendapat bahwa ketergantungan tidak pernah dienkapsulasi sejak kelas CreditCard menyebabkan efek samping yang terlihat secara eksternal. Jika Anda ingin menguji CreditCard tanpa refactoring, Anda tentu dapat mengamati ketergantungannya.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

Saya tidak berpikir perlu khawatir apakah mengekspos Database sebagai ketergantungan mengurangi enkapsulasi, karena itu desain yang bagus. Tidak semua keputusan DI akan lurus ke depan. Namun, tidak ada jawaban lain yang menunjukkan contoh balasan.


1
Tes unit biasanya ditulis oleh penulis kelas, jadi tidak apa-apa untuk menguraikan dependensi dalam kasus uji, dari sudut pandang teknis. Ketika nantinya kelas kartu kredit berubah untuk menggunakan API Web seperti PayPal, pengguna harus mengubah segalanya jika sudah MATI. Pengujian unit biasanya dilakukan dengan pengetahuan mendalam tentang subjek yang diuji (bukankah itu intinya?) Jadi saya pikir tes lebih merupakan pengecualian daripada contoh biasa.
kizzx2

1
Maksud dari DI adalah untuk menghindari perubahan yang Anda gambarkan. Jika Anda beralih dari API Web ke PayPal, Anda tidak akan mengubah sebagian besar tes karena mereka akan menggunakan Layanan MockPayment dan CreditCard akan dibangun dengan Layanan Pembayaran. Anda hanya akan memiliki beberapa tes yang melihat interaksi nyata antara CreditCard dan Layanan Pembayaran nyata, sehingga perubahan di masa depan sangat terisolasi. Manfaatnya bahkan lebih besar untuk grafik ketergantungan yang lebih dalam (seperti kelas yang bergantung pada CreditCard).
Craig P. Motlin

@Craig p. Motlin Bagaimana objek dapat CreditCardberubah dari WebAPI ke PayPal, tanpa harus eksternal apa pun dari kelas yang harus diubah?
Ian Boyd

@Ian saya sebutkan CreditCard harus di refactored untuk mengambil DatabaseInterface dalam konstruktornya yang melindunginya dari perubahan dalam implementasi DatabaseInterfaces. Mungkin itu perlu lebih generik, dan mengambil StorageInterface dimana WebAPI bisa menjadi implementasi lain. PayPal berada di level yang salah, karena ini merupakan alternatif untuk CreditCard. PayPal dan CreditCard dapat mengimplementasikan PaymentInterface untuk melindungi bagian lain dari aplikasi dari luar contoh ini.
Craig P. Motlin

@ kizzx2 Dengan kata lain, tidak masuk akal mengatakan kartu kredit harus menggunakan PayPal. Mereka adalah alternatif dalam kehidupan nyata.
Craig P. Motlin

0

Saya pikir ini masalah ruang lingkup. Ketika Anda mendefinisikan enkapsulasi (tidak memberi tahu caranya), Anda harus menentukan apa fungsi yang dienkapsulasi.

  1. Kelas apa adanya: apa yang Anda enkapsulasi adalah satu-satunya responsabilitas kelas. Apa yang ia tahu bagaimana melakukannya. Misalnya, menyortir. Jika Anda menyuntikkan beberapa pembanding untuk memesan, katakanlah, klien, itu bukan bagian dari hal yang diringkas: quicksort.

  2. Fungsi yang dikonfigurasikan : jika Anda ingin menyediakan fungsionalitas yang siap digunakan maka Anda tidak menyediakan kelas QuickSort, tetapi instance kelas QuickSort yang dikonfigurasikan dengan Pembanding. Dalam hal ini, kode yang bertanggung jawab untuk membuat dan mengonfigurasi itu harus disembunyikan dari kode pengguna. Dan itu enkapsulasi.

Saat Anda memprogram kelas, menerapkan tanggung jawab tunggal ke dalam kelas, Anda menggunakan opsi 1.

Ketika Anda memprogram aplikasi, itu adalah, membuat sesuatu yang melakukan beberapa pekerjaan konkret yang berguna maka Anda berulang kali menggunakan opsi 2.

Ini adalah implementasi dari instance yang dikonfigurasi:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Ini adalah cara beberapa kode klien lain menggunakannya:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Itu dirangkum karena jika Anda mengubah implementasi (Anda mengubah clientSorterdefinisi kacang) itu tidak merusak penggunaan klien. Mungkin, saat Anda menggunakan file xml dengan semua yang ditulis bersama, Anda melihat semua detail. Tapi percayalah, kode klien ( ClientService) tidak tahu apa-apa tentang penyortirnya.


0

Mungkin perlu disebutkan bahwa Encapsulationagak tergantung perspektif.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Dari perspektif seseorang yang bekerja di Akelas, dalam contoh kedua Atahu banyak tentang sifatthis.b

Sedangkan tanpa DI

new A()

vs.

new A(new B())

Orang yang melihat kode ini tahu lebih banyak tentang sifat dari Acontoh kedua.

Dengan DI, setidaknya semua pengetahuan yang bocor itu ada di satu tempat.

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.