Pertanyaan ini telah mengganggu saya selama beberapa hari, dan rasanya seperti beberapa praktik yang saling bertentangan.
Contoh
Iterasi 1
public class FooDao : IFooDao
{
private IFooConnection fooConnection;
private IBarConnection barConnection;
public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
{
this.fooConnection = fooConnection;
this.barConnection = barConnection;
}
public Foo GetFoo(int id)
{
Foo foo = fooConection.get(id);
Bar bar = barConnection.get(foo);
foo.bar = bar;
return foo;
}
}
Sekarang, ketika menguji ini, saya akan memalsukan IFooConnection dan IBarConnection, dan menggunakan Dependency Injection (DI) ketika instantiating FooDao.
Saya dapat mengubah implementasinya, tanpa mengubah fungsionalitasnya.
Iterasi 2
public class FooDao : IFooDao
{
private IFooBuilder fooBuilder;
public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
{
this.fooBuilder = new FooBuilder(fooConnection, barConnection);
}
public Foo GetFoo(int id)
{
return fooBuilder.Build(id);
}
}
Sekarang, saya tidak akan menulis pembuat ini, tetapi bayangkan itu melakukan hal yang sama yang dilakukan FooDao sebelumnya. Ini hanya refactoring, jadi jelas, ini tidak mengubah fungsi, dan pengujian saya masih berjalan.
IFooBuilder adalah Internal, karena itu hanya ada untuk melakukan pekerjaan untuk perpustakaan, yaitu itu bukan bagian dari API.
Satu-satunya masalah adalah, saya tidak lagi mematuhi Ketergantungan Inversi. Jika saya menulis ulang ini untuk memperbaiki masalah itu, mungkin akan terlihat seperti ini.
Iterasi 3
public class FooDao : IFooDao
{
private IFooBuilder fooBuilder;
public FooDao(IFooBuilder fooBuilder)
{
this.fooBuilder = fooBuilder;
}
public Foo GetFoo(int id)
{
return fooBuilder.Build(id);
}
}
Ini harus dilakukan. Saya telah mengubah konstruktor, jadi tes saya perlu diubah untuk mendukung ini (atau sebaliknya), ini bukan masalah saya.
Agar ini bekerja dengan DI, FooBuilder dan IFooBuilder harus bersifat publik. Ini berarti saya harus menulis tes untuk FooBuilder, karena tiba-tiba menjadi bagian dari API perpustakaan saya. Masalah saya adalah, klien perpustakaan hanya boleh menggunakannya melalui desain API yang saya maksudkan, IFooDao, dan tes saya bertindak sebagai klien. Jika saya tidak mengikuti Ketergantungan Pembalikan, pengujian dan API saya lebih bersih.
Dengan kata lain, yang saya pedulikan sebagai klien, atau sebagai tes, adalah untuk mendapatkan Foo yang benar, bukan bagaimana itu dibangun.
Solusi
Haruskah saya tidak peduli, tulis tes untuk FooBuilder meskipun hanya publik untuk menyenangkan DI? - Mendukung iterasi 3
Haruskah saya menyadari bahwa memperluas API adalah kelemahan dari Dependency Inversion, dan sangat jelas mengapa saya memilih untuk tidak mematuhinya di sini? - Mendukung iterasi 2
Apakah saya terlalu menekankan memiliki API yang bersih? - Mendukung iterasi 3
EDIT: Saya ingin memperjelas, bahwa masalah saya bukan "Bagaimana cara menguji internal?", Melainkan sesuatu seperti "Bisakah saya tetap internal, dan masih mematuhi DIP, dan haruskah saya?".
sumber
IFooBuilder
perlu untuk umum?FooBuilder
hanya perlu diekspos ke sistem DI melalui semacam "dapatkan implementasi default" metode yang mengembalikanIFooBuilder
dan dapat tetap internal.public
untuk API perpustakaan danpublic
untuk pengujian". Atau dalam bentuk lain "Saya ingin menjaga metode pribadi agar tidak muncul di API, bagaimana cara menguji metode pribadi itu?". Jawabannya selalu "hidup dengan API publik yang lebih luas", "hidup dengan tes melalui API publik", atau "buat metodeinternal
dan buat mereka terlihat oleh kode tes".Jawaban:
Saya akan pergi dengan:
Namun, dalam Prinsip SOLID OO dan Desain Agile (pada 1:08:55) Paman Bob mengatakan bahwa aturannya tentang injeksi ketergantungan adalah jangan menyuntikkan segalanya, Anda menyuntikkan hanya di lokasi strategis. (Dia juga menyebutkan bahwa topik inversi ketergantungan dan injeksi ketergantungan adalah sama).
Artinya, inversi dependensi tidak dimaksudkan untuk diterapkan pada semua dependensi kelas dalam suatu program. Anda harus melakukannya di lokasi strategis (ketika membayar (atau mungkin membayar)).
sumber
Saya akan berbagi beberapa pengalaman / pengamatan berbagai basis kode yang pernah saya lihat. NB Saya tidak menganjurkan pendekatan apa pun secara khusus, hanya berbagi dengan Anda di mana saya melihat kode seperti itu:
Sebelum DI dan pengujian otomatis, kebijaksanaan yang diterima adalah bahwa sesuatu harus dibatasi pada ruang lingkup yang diperlukan. Namun saat ini, tidak jarang untuk melihat metode yang dapat dibiarkan internal dipublikasikan untuk kemudahan pengujian (karena ini biasanya terjadi di majelis lain). NB ada arahan untuk mengekspos metode internal tetapi ini kurang lebih sama dengan hal yang sama.
DI tidak diragukan lagi mengeluarkan biaya tambahan dengan kode tambahan.
Karena kerangka kerja DI memang memperkenalkan kode tambahan, saya telah memperhatikan pergeseran menuju kode yang sementara ditulis dalam gaya DI tidak menggunakan DI per se yaitu D dari SOLID.
Singkatnya:
Pengubah akses terkadang dilonggarkan untuk menyederhanakan pengujian
DI memang memperkenalkan kode tambahan
Dalam terang 1 & 2, beberapa pengembang berpendapat bahwa ini terlalu banyak pengorbanan dan hanya memilih untuk bergantung pada abstraksi pada awalnya dengan opsi untuk retro-fit DI nanti.
sumber
Saya khususnya tidak membeli gagasan ini bahwa Anda harus mengekspos metode pribadi (dengan cara apa pun) untuk melakukan pengujian yang sesuai. Saya juga menyarankan bahwa klien Anda menghadapi API tidak sama dengan metode publik objek Anda, mis. Inilah antarmuka publik Anda:
dan tidak disebutkan tentang Anda
FooBuilder
Dalam pertanyaan awal Anda, saya akan mengambil rute ketiga, menggunakan
FooBuilder
. Saya mungkin akan menguji AndaFooDao
menggunakan tiruan , sehingga berkonsentrasi padaFooDao
fungsionalitas Anda (Anda selalu dapat menyuntikkan pembangun nyata jika lebih mudah) dan saya akan memiliki tes terpisah di sekitar AndaFooBuilder
.(Saya terkejut mengejek belum dirujuk dalam pertanyaan / jawaban ini - ini berjalan seiring dengan pengujian solusi berpola DI / IoC)
Kembali. komentar Anda:
Saya tidak berpikir ini memperluas API yang Anda berikan kepada klien Anda. Anda dapat membatasi API yang digunakan klien Anda melalui antarmuka (jika diinginkan). Saya juga menyarankan agar tes Anda tidak hanya mengelilingi komponen Anda yang menghadap publik. Mereka harus merangkul semua kelas (implementasi) yang mendukung API di bawahnya. mis. inilah REST API untuk sistem yang sedang saya kerjakan:
(dalam pseudo-code)
Saya punya satu metode API, dan 1.000 soal tes - sebagian besar di antaranya ada di sekitar objek yang mendukung ini.
sumber
Mengapa Anda ingin mengikuti DI dalam hal ini jika bukan untuk tujuan pengujian? Jika tidak hanya API publik Anda, tetapi juga pengujian Anda benar-benar bersih tanpa
IFooBuilder
parameter (seperti yang Anda tulis), tidak masuk akal untuk memperkenalkan kelas semacam itu. DI bukan tujuan itu sendiri, itu akan membantu Anda untuk membuat kode Anda lebih dapat diuji. Jika Anda tidak ingin menulis tes otomatis untuk tingkat abstraksi tertentu, jangan menerapkan DI pada tingkat itu.Namun, mari kita asumsikan sejenak Anda ingin menerapkan DI untuk memungkinkan membuat tes unit untuk
FooDao
konstruktor secara terpisah tanpa proses pembangunan, tetapi tetap tidak inginFooDao(IFooBuilder fooBuilder)
konstruktor di API publik Anda, hanya konstruktorFooDao(IFooConnection fooConnection, IBarConnection barConnection)
. Kemudian berikan keduanya, yang pertama sebagai "internal", yang terakhir sebagai "publik":Sekarang Anda dapat menyimpan
FooBuilder
danIFooBuilder
internal, dan menerapkannyaInternalsVisibleTo
agar dapat diakses oleh unit test.sumber