Inversi Ketergantungan memperluas API, menghasilkan tes yang tidak perlu

8

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?".

Chris Wohlert
sumber
Saya dapat mengedit pertanyaan saya untuk menambahkan tes jika Anda mau.
Chris Wohlert
1
"FooBuilder dan IFooBuilder harus bersifat publik". Tentunya hanya IFooBuilderperlu untuk umum? FooBuilderhanya perlu diekspos ke sistem DI melalui semacam "dapatkan implementasi default" metode yang mengembalikan IFooBuilderdan dapat tetap internal.
David Arno
Saya kira itu bisa menjadi solusi.
Chris Wohlert
2
Ini telah ditanyakan di sini pada Programmer setidaknya selusin kali, menggunakan istilah yang berbeda: "Saya ingin membuat perbedaan antara publicuntuk API perpustakaan dan publicuntuk 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 metode internaldan buat mereka terlihat oleh kode tes".
Doc Brown
Saya mungkin tidak akan mengganti aktor, tetapi memperkenalkan yang baru dan rantai mereka bersama-sama.
RubberDuck

Jawaban:

3

Saya akan pergi dengan:

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

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)).

Robert Jørgensgaard Engdahl
sumber
2
Komentar dari Bob ini sempurna, terima kasih banyak untuk berbagi ini.
Chris Wohlert
5

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:

Jika saya tidak peduli, tulis tes untuk FooBuilder meskipun hanya publik untuk menyenangkan DI

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.

Haruskah saya menyadari bahwa memperluas API adalah kelemahan dari Dependency Inversion, dan sangat jelas mengapa saya memilih untuk tidak mematuhinya di sini?

DI tidak diragukan lagi mengeluarkan biaya tambahan dengan kode tambahan.

Apakah saya terlalu menekankan memiliki API yang bersih

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:

  1. Pengubah akses terkadang dilonggarkan untuk menyederhanakan pengujian

  2. 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.

Robbie Dee
sumber
4
Memang itulah yang menyebabkan beberapa pengembang memilih atribut InternalsVisibleTo untuk metode internal daripada mengekspos mereka sebagai publik.
Robbie Dee
1
"Namun saat ini, tidak jarang untuk melihat metode yang dapat dibiarkan internal dipublikasikan untuk kemudahan pengujian (karena ini biasanya terjadi di majelis lain)." - benarkah?
Brian Agnew
3
@BrianAgnew sayang ya. Ini adalah anti-pola bersama dengan "kita harus memiliki 100% unit cakupan tes", termasuk getter dan setter :-( Mengapa coders begitu tidak terpikirkan saat ini ?!
gbjbaanb
2
@ChrisWohlert solusi sederhana. Jangan ganti aktor. Tambahkan yang baru. Rangkai mereka. Tinggalkan tes yang ada di tempat. Secara implisit menguji pembangun Anda dan kode Anda masih tertutup. Hanya tulis tes jika berharga untuk melakukannya. Ada titik di mana Anda mendapatkan hasil yang semakin berkurang dari investasi Anda.
RubberDuck
1
@ ChrisWohlert sama sekali tidak ada yang salah dengan memberikan konstruktor kenyamanan. Anda masih menyuntikkan dependensi sejati kelas . Menurut Anda bagaimana DI dilakukan sebelum semua kerangka wadah IoC "mewah" itu muncul?
RubberDuck
0

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:

interface IFooDao {
   public Foo GetFoo(int id);
}

dan tidak disebutkan tentang Anda FooBuilder

Dalam pertanyaan awal Anda, saya akan mengambil rute ketiga, menggunakan FooBuilder. Saya mungkin akan menguji Anda FooDaomenggunakan tiruan , sehingga berkonsentrasi pada FooDaofungsionalitas Anda (Anda selalu dapat menyuntikkan pembangun nyata jika lebih mudah) dan saya akan memiliki tes terpisah di sekitar Anda FooBuilder.

(Saya terkejut mengejek belum dirujuk dalam pertanyaan / jawaban ini - ini berjalan seiring dengan pengujian solusi berpola DI / IoC)

Kembali. komentar Anda:

Seperti yang dinyatakan, pembangun tidak menyediakan fungsionalitas baru, jadi saya tidak perlu mengujinya, namun, DIP membuatnya menjadi bagian dari API, yang memaksa saya untuk mengujinya.

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)

void doSomeCalculation(json : CalculationRequestObject);

Saya punya satu metode API, dan 1.000 soal tes - sebagian besar di antaranya ada di sekitar objek yang mendukung ini.

Brian Agnew
sumber
Saya tidak secara khusus membeli gagasan ini bahwa Anda harus mengekspos metode pribadi (dengan cara apa pun) untuk melakukan pengujian yang sesuai - Saya tidak percaya ada yang menganjurkan ini ...
Robbie Dee
Tetapi Anda mengatakan di atas - 'Pengubah akses terkadang dilonggarkan untuk menyederhanakan pengujian'?
Brian Agnew
Internal dapat diekspos ke majelis uji dan implementasi antarmuka implisit menciptakan metode publik. Saya tidak pernah menganjurkan mengekspos metode pribadi ...
Robbie Dee
Dari PoV saya, 'Internal dapat diekspos ke majelis tes' jumlah hal yang sama. Saya tidak yakin bahwa pengujian internal (apakah ditandai sebagai pribadi atau tidak) adalah latihan yang bermanfaat, kecuali sebagai tes regresi sementara refactoring kode yang tidak terstruktur dengan baik
Brian Agnew
Hai Brian, saya, seperti Anda, akan memalsukan IFooBuilder ketika diuraikan ke FooDao, tetapi bukankah ini berarti saya akan memerlukan tes lain untuk penerapan IFooBuilder? Kami akhirnya mendekati masalah sebenarnya, dan bukan apakah saya harus mengungkapkan internal atau tidak.
Chris Wohlert
0

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 IFooBuilderparameter (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 FooDaokonstruktor secara terpisah tanpa proses pembangunan, tetapi tetap tidak ingin FooDao(IFooBuilder fooBuilder)konstruktor di API publik Anda, hanya konstruktor FooDao(IFooConnection fooConnection, IBarConnection barConnection). Kemudian berikan keduanya, yang pertama sebagai "internal", yang terakhir sebagai "publik":

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Sekarang Anda dapat menyimpan FooBuilderdan IFooBuilderinternal, dan menerapkannya InternalsVisibleToagar dapat diakses oleh unit test.

Doc Brown
sumber
Bagian pertama dari pertanyaan Anda adalah persis apa yang saya cari. Jawaban untuk pertanyaan: "Haruskah saya menggunakan DIP di sini". Saya tidak ingin konstruktor kedua itu. Jika saya tidak mematuhi DIP, saya tidak perlu membuat IFooBuilder menjadi publik, dan saya bisa berhenti memalsukan / mengujinya. Singkatnya, Anda mengatakan kepada saya untuk tetap berpegang pada iterasi 2.
Chris Wohlert
Jujur, jika Anda menghapus semuanya dari "Namun", saya akan menerima jawabannya. Saya pikir bagian pertama menjelaskan dengan sangat baik kapan dan mengapa saya harus / tidak harus menggunakan DI.
Chris Wohlert
Saya hanya menambahkan peringatan bahwa DI bukan hanya tentang pengujian - itu hanya menyederhanakan bertukar satu konkret dengan yang lain. Mengenai apakah Anda memerlukan ini di sini adalah sesuatu yang hanya dapat Anda jawab. Seperti 17 dari 26 menempatkannya secara luar biasa - di suatu tempat antara YAGNI & PYIAC.
Robbie Dee
@ChrisWohlert: Saya tidak akan menghapus bagian ke-2 hanya karena Anda tidak suka menerapkannya untuk situasi Anda. Anda tidak perlu tes pada level itu - baiklah, terapkan bagian 1, jangan gunakan DI sini. Anda ingin tes dan menghindari memiliki bagian-bagian tertentu di API publik - baik, Anda perlu dua titik masuk dalam kode Anda dengan pengubah akses yang berbeda, jadi terapkan bagian 2. Everone dapat memilih solusi yang paling cocok untuknya.
Doc Brown
Tentu semua orang dapat memilih solusi yang mereka inginkan, tetapi Anda salah memahami pertanyaan itu jika Anda pikir saya ingin menguji pembangun. Itulah yang saya coba singkirkan.
Chris Wohlert