Menyeimbangkan injeksi ketergantungan dengan desain API publik

13

Saya telah merenungkan bagaimana menyeimbangkan desain yang dapat diuji menggunakan injeksi ketergantungan dengan menyediakan API publik sederhana yang tetap. Dilema saya adalah: orang ingin melakukan sesuatu seperti var server = new Server(){ ... }dan tidak perlu khawatir menciptakan banyak dependensi dan grafik dependensi yang Server(,,,,,,)mungkin dimiliki. Saat berkembang, saya tidak terlalu khawatir, karena saya menggunakan kerangka kerja IoC / DI untuk menangani semua itu (saya tidak menggunakan aspek manajemen siklus hidup dari wadah apa pun, yang akan memperumit masalah lebih lanjut).

Sekarang, dependensi tidak mungkin diimplementasikan kembali. Komponen dalam hal ini hampir murni untuk testabilitas (dan desain yang layak!) Daripada membuat jahitan untuk ekstensi, dll. Orang akan 99,999% dari waktu ingin menggunakan konfigurasi default. Begitu. Saya bisa membuat hardcode dependensi. Tidak ingin melakukan itu, kami kehilangan pengujian kami! Saya bisa menyediakan konstruktor default dengan dependensi hard-coded dan yang membutuhkan dependensi. Itu ... berantakan, dan mungkin membingungkan, tetapi layak. Saya bisa membuat dependensi menerima konstruktor internal dan membuat unit saya menguji teman perakitan (dengan asumsi C #), yang merapikan API publik tetapi meninggalkan perangkap tersembunyi yang jahat mengintai untuk pemeliharaan. Memiliki dua konstruktor yang secara implisit terhubung daripada secara eksplisit akan menjadi desain yang buruk secara umum dalam buku saya.

Saat ini tentang kejahatan paling sedikit yang bisa saya pikirkan. Pendapat? Kebijaksanaan?

kolektiv
sumber

Jawaban:

11

Injeksi ketergantungan adalah pola yang kuat ketika digunakan dengan baik, tetapi terlalu sering para praktisi menjadi tergantung pada beberapa kerangka eksternal. API yang dihasilkan cukup menyakitkan bagi kita yang tidak ingin secara longgar mengikat aplikasi kita bersama dengan lakban XML. Jangan lupakan Plain Old Objects (POO).

Pertama-tama pertimbangkan bagaimana Anda akan mengharapkan seseorang untuk menggunakan API Anda - tanpa kerangka yang terlibat.

  • Bagaimana Anda membuat Instantiate Server?
  • Bagaimana cara Anda memperpanjang Server?
  • Apa yang ingin Anda paparkan di API eksternal?
  • Apa yang ingin Anda sembunyikan di API eksternal?

Saya suka gagasan untuk menyediakan implementasi standar untuk komponen Anda, dan fakta bahwa Anda memiliki komponen-komponen itu. Pendekatan berikut adalah praktik OO yang sangat valid, dan layak:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Selama Anda mendokumentasikan apa implementasi default untuk komponen dalam dokumen API, dan menyediakan satu konstruktor yang melakukan semua pengaturan, Anda harus baik-baik saja. Konstruktor cascading tidak rapuh selama kode pengaturan terjadi dalam satu master konstruktor.

Pada dasarnya, semua konstruktor yang menghadap publik akan memiliki kombinasi komponen yang berbeda yang ingin Anda ungkapkan. Secara internal, mereka akan mengisi default dan tunduk pada konstruktor pribadi yang menyelesaikan pengaturan. Intinya, ini memberi Anda KERING dan cukup mudah untuk diuji.

Jika Anda perlu memberikan friendakses ke pengujian Anda untuk mengatur objek Server untuk mengisolasinya untuk pengujian, lakukan. Itu tidak akan menjadi bagian dari API publik, yang ingin Anda berhati-hati.

Hanya berbaik hati kepada konsumen API Anda dan tidak memerlukan kerangka kerja IoC / DI untuk menggunakan perpustakaan Anda. Untuk merasakan rasa sakit yang Anda timbulkan, pastikan unit test Anda juga tidak bergantung pada kerangka kerja IoC / DI. (Ini adalah masalah pribadi hewan peliharaan, tetapi segera setelah Anda memperkenalkan kerangka kerja itu bukan lagi pengujian unit - itu menjadi pengujian integrasi).

Berin Loritsch
sumber
Saya setuju, dan saya jelas bukan penggemar sup konfigurasi IoC - konfigurasi XML bukanlah sesuatu yang saya gunakan sama sekali terkait dengan IoC. Tentunya yang membutuhkan penggunaan IoC adalah apa yang saya hindari - itu adalah asumsi yang tidak pernah dapat dibuat oleh perancang API publik. Terima kasih atas komentarnya, itu yang saya pikirkan dan meyakinkan untuk melakukan pemeriksaan kewarasan sekarang dan lagi!
kolektiv
-1 Jawaban ini pada dasarnya mengatakan "jangan gunakan injeksi ketergantungan" dan mengandung asumsi yang tidak akurat. Dalam contoh Anda jika HttpListenerkonstruktor berubah maka Serverkelas juga harus berubah. Mereka digabungkan. Solusi yang lebih baik adalah apa yang @Winston Ewert katakan di bawah ini, yaitu menyediakan kelas default dengan dependensi yang ditentukan sebelumnya, di luar API perpustakaan. Maka programmer tidak dipaksa untuk mengatur semua dependensi karena Anda membuat keputusan untuknya, tetapi dia masih memiliki fleksibilitas untuk mengubahnya nanti. Ini adalah masalah besar ketika Anda memiliki dependensi pihak ketiga.
M. Dudley
FWIW contoh yang diberikan disebut "injeksi bajingan". stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley
1
@MichaelDudley, apakah Anda membaca pertanyaan OP. Semua "asumsi" didasarkan pada informasi yang diberikan OP. Untuk sementara waktu, "injeksi bajingan" seperti Anda menyebutnya, adalah format injeksi ketergantungan yang disukai - terutama untuk sistem yang komposisinya tidak berubah selama runtime. Setter dan getter juga merupakan bentuk injeksi ketergantungan lainnya. Saya hanya menggunakan contoh-contoh berdasarkan apa yang diberikan OP.
Berin Loritsch
4

Dalam Java dalam kasus seperti itu, biasanya memiliki konfigurasi "default" yang dibangun oleh pabrik, tetap independen dari server yang berperilaku "benar". Jadi, pengguna Anda menulis:

var server = DefaultServer.create();

sementara Serverkonstruktor masih menerima semua dependensinya dan dapat digunakan untuk kustomisasi mendalam.

fdreger
sumber
+1, SRP. Tanggung jawab kantor dokter gigi bukanlah membangun kantor dokter gigi. Tanggung jawab server bukan untuk membangun server.
R. Schmitz
2

Bagaimana dengan menyediakan subclass yang menyediakan "api publik"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

Pengguna dapat new StandardServer()dan sedang dalam perjalanan. Mereka juga dapat menggunakan kelas dasar Server jika mereka ingin lebih mengontrol bagaimana fungsi server. Ini kurang lebih pendekatan yang saya gunakan. (Saya tidak menggunakan kerangka kerja karena saya belum melihat intinya.)

Ini masih memperlihatkan api internal, tapi saya pikir Anda harus melakukannya. Anda telah membagi objek Anda menjadi berbagai komponen berguna yang harus bekerja secara independen. Tidak ada yang tahu kapan pihak ketiga mungkin ingin menggunakan salah satu komponen secara terpisah.

Winston Ewert
sumber
1

Saya dapat membuat dependensi menerima konstruktor internal dan membuat unit saya menguji teman perakitan (dengan asumsi C #), yang merapikan API publik tetapi meninggalkan perangkap tersembunyi yang jahat mengintai untuk pemeliharaan.

Itu tidak tampak seperti "perangkap tersembunyi yang jahat" bagi saya. Konstruktor publik seharusnya hanya memanggil yang internal dengan dependensi "default". Selama jelas bahwa konstruktor publik tidak boleh dimodifikasi, semuanya harus baik-baik saja.

Segera.
sumber
0

Saya sangat setuju dengan pendapat Anda. Kami tidak ingin mencemari batas penggunaan komponen hanya untuk tujuan pengujian unit. Ini adalah seluruh masalah solusi pengujian berbasis DI.

Saya akan menyarankan untuk menggunakan InjectableFactory / InjectableInstance pola. InjectableInstance adalah kelas utilitas sederhana yang dapat digunakan kembali yang merupakan value holder yang bisa diubah . Antarmuka komponen berisi referensi ke pemegang nilai ini yang diinisialisasi dengan implementasi standar. Antarmuka akan memberikan metode singleton get () yang mendelegasikan kepada pemegang nilai. Klien komponen akan memanggil metode get () alih-alih yang baru untuk mendapatkan instance implementasi untuk menghindari ketergantungan langsung. Selama waktu pengujian, kami dapat secara opsional mengganti implementasi standar dengan tiruan. Setelah ini saya akan menggunakan satu contoh TimeProviderkelas yang merupakan abstraksi Date () sehingga memungkinkan pengujian unit untuk menyuntikkan dan mengejek untuk mensimulasikan momen yang berbeda. Maaf saya akan menggunakan java di sini karena saya lebih akrab dengan java. C # harus serupa.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance cukup mudah diimplementasikan, saya punya referensi implementasi di java. Untuk detailnya, silakan lihat posting blog saya Ketergantungan Indirection dengan Injectable Factory

Jianwu Chen
sumber
Jadi ... Anda mengganti injeksi ketergantungan dengan singleton yang bisa berubah? Kedengarannya mengerikan, bagaimana Anda menjalankan tes independen? Apakah Anda membuka proses baru untuk setiap tes atau memaksa mereka ke urutan tertentu? Java bukan setelan kuat saya, apakah saya salah paham sesuatu?
novigt
Dalam proyek Java pakar, contoh yang dibagi bukan masalah karena mesin junit akan menjalankan setiap tes di loader kelas yang berbeda secara default, Namun saya mengalami masalah ini di beberapa IDE karena dapat menggunakan kembali loader kelas yang sama. Untuk mengatasi masalah ini, kelas menyediakan metode reset untuk dipanggil untuk mengatur ulang ke keadaan semula setelah setiap tes. Dalam praktiknya, ini adalah situasi yang langka dimana tes yang berbeda ingin menggunakan tiruan yang berbeda. Kami telah menerapkan pola ini untuk proyek berskala besar selama bertahun-tahun, Semuanya berjalan dengan lancar, kami menikmati kode yang mudah dibaca dan bersih tanpa mengorbankan portabilitas dan testabilitas.
Jianwu Chen