Pendekatan bertahap terhadap injeksi ketergantungan

12

Saya sedang berupaya membuat kelas saya dapat diuji unit, menggunakan injeksi ketergantungan. Tetapi beberapa dari kelas-kelas ini memiliki banyak klien, dan saya belum siap untuk refactor mereka semua untuk mulai melewati dependensi. Jadi saya mencoba melakukannya secara bertahap; menjaga dependensi default untuk saat ini, tetapi membiarkannya ditimpa untuk pengujian.

Satu pendekatan saya conisdering hanya memindahkan semua panggilan "baru" ke metode mereka sendiri, misalnya:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Kemudian dalam unit test saya, saya bisa mensubkelaskan kelas ini, dan mengesampingkan fungsi create, jadi mereka malah membuat objek palsu.

Apakah ini pendekatan yang baik? Apakah ada kerugiannya?

Lebih umum, apakah boleh memiliki dependensi hard-coded, selama Anda dapat menggantinya untuk pengujian? Saya tahu pendekatan yang disukai adalah secara eksplisit mengharuskan mereka dalam konstruktor, dan saya ingin sampai di sana pada akhirnya. Tapi saya bertanya-tanya apakah ini langkah pertama yang baik.

Satu kekurangan yang baru saja terpikir oleh saya: jika Anda memiliki subclass nyata yang perlu Anda uji, Anda tidak dapat menggunakan kembali subclass tes yang Anda tulis untuk kelas induk. Anda harus membuat subclass tes untuk setiap subclass nyata, dan itu harus mengesampingkan fungsi create yang sama.

JW01
sumber
Bisakah Anda menguraikan mengapa mereka tidak diuji unit sekarang?
Austin Salonen
Ada banyak "X baru" yang tersebar di seluruh kode, serta banyak penggunaan kelas statis dan lajang. Jadi ketika saya mencoba untuk menguji satu kelas, saya benar-benar menguji sejumlah besar dari mereka. Jika saya dapat mengisolasi dependensi dan menggantinya dengan benda palsu, saya akan memiliki kontrol lebih besar atas pengujian.
JW01

Jawaban:

13

Ini adalah pendekatan yang baik untuk memulai. Perhatikan bahwa yang paling penting adalah untuk menutupi kode Anda yang ada dengan tes unit; setelah Anda menjalani tes, Anda dapat melakukan refactor lebih bebas untuk meningkatkan desain Anda lebih lanjut.

Jadi poin awalnya bukan untuk membuat desain elegan dan berkilau - hanya untuk membuat unit kode diuji dengan perubahan paling tidak berisiko . Tanpa tes unit, Anda harus ekstra konservatif dan berhati-hati untuk menghindari melanggar kode Anda. Perubahan awal ini bahkan dapat membuat kode terlihat lebih canggung atau jelek dalam beberapa kasus - tetapi jika mereka memungkinkan Anda untuk menulis unit test pertama, pada akhirnya Anda akan dapat refactor ke arah desain ideal yang Anda pikirkan.

Pekerjaan mendasar untuk membaca tentang hal ini adalah Bekerja Efektif dengan Legacy Code . Ini menggambarkan trik yang Anda tunjukkan di atas dan banyak lagi, menggunakan beberapa bahasa.

Péter Török
sumber
Hanya komentar tentang ini "begitu Anda memiliki tes, Anda dapat refactor lebih bebas" - Jika Anda tidak memiliki tes, Anda tidak refactoring, Anda hanya mengubah kode.
ocodo
@ Slomojo Saya mengerti maksud Anda, tetapi Anda masih dapat melakukan banyak refactoring aman tanpa tes, terutama ractoring IDE otomatis seperti, misalnya (dalam gerhana untuk Jawa) mengekstraksi kode duplikat ke metode, mengubah nama semuanya, kelas bergerak dan mengekstraksi bidang, konstanta dll
Alb
Saya benar-benar hanya mengutip asal-usul istilah Refactoring, yang memodifikasi kode menjadi pola yang lebih baik yang dijamin dari kesalahan regresi oleh kerangka pengujian. Tentu saja, refactoring berbasis IDE telah membuatnya jauh lebih sepele untuk perangkat lunak 'refactor', tetapi saya cenderung lebih suka menghindari delusi diri, jika perangkat lunak saya tidak memiliki tes, dan saya "refactor" saya hanya mengubah kode. Tentu saja, itu mungkin bukan yang saya katakan kepada orang lain;)
ocodo
1
@ Alb, refactoring tanpa tes selalu lebih berisiko. Refactoring otomatis mengurangi risiko itu, tetapi tidak sampai nol. Selalu ada kasus tepi rumit yang alat mungkin tidak dapat menangani dengan benar. Dalam kasus yang lebih baik, hasilnya adalah beberapa pesan kesalahan dari alat ini, tetapi dalam kasus terburuk itu dapat secara diam-diam memperkenalkan bug halus. Jadi disarankan untuk tetap melakukan perubahan yang paling sederhana dan teraman bahkan dengan alat otomatis.
Péter Török
3

Guice-people merekomendasikan penggunaan

@Inject
public void setX(.... X x) {
   this.x = x;
}

untuk semua properti alih-alih hanya menambahkan @Inject ke definisi mereka. Ini akan memungkinkan Anda untuk memperlakukan mereka sebagai kacang normal, yang dapat Anda lakukan newsaat menguji (dan mengatur properti secara manual) dan @Inject dalam produksi.


sumber
3

Ketika saya dihadapkan dengan masalah jenis ini, saya akan mengidentifikasi setiap ketergantungan kelas-untuk-tes saya dan membuat konstruktor yang dilindungi yang akan memungkinkan saya untuk melewatinya. Ini secara efektif sama dengan membuat setter untuk masing-masing, tetapi saya lebih suka mendeklarasikan dependensi pada konstruktor saya sehingga saya tidak lupa untuk instantiate di masa depan.

Jadi kelas unit baru yang dapat diuji akan memiliki 2 konstruktor:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}
Seth M.
sumber
2

Anda mungkin ingin mempertimbangkan idiom "pengambil menciptakan" sampai Anda dapat menggunakan ketergantungan yang disuntikkan.

Sebagai contoh,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Ini akan memungkinkan Anda untuk melakukan refactor secara internal sehingga Anda dapat mereferensikan myObject Anda melalui pengambil. Anda tidak perlu menelepon new. Di masa mendatang, ketika semua klien dikonfigurasi untuk memungkinkan injeksi ketergantungan, maka yang harus Anda lakukan adalah menghapus kode pembuatan di satu tempat - pengambil. Setter akan memungkinkan Anda untuk menyuntikkan benda tiruan sesuai kebutuhan.

Kode di atas hanya sebuah contoh dan itu tidak secara langsung mengekspos keadaan internal. Anda harus hati-hati mempertimbangkan jika pendekatan ini sesuai untuk basis kode Anda.

Saya juga menyarankan agar Anda membaca " Bekerja Efektif dengan Kode Warisan " yang berisi banyak tips berguna untuk membuat keberhasilan refactoring besar.

Gary Rowe
sumber
Terima kasih. Saya sedang mempertimbangkan pendekatan ini untuk beberapa kasus. Tapi apa yang Anda lakukan ketika konstruktor MyObject membutuhkan parameter yang hanya tersedia dari dalam kelas Anda? Atau ketika Anda perlu membuat lebih dari satu MyObject selama masa hidup kelas Anda? Apakah Anda harus menyuntikkan MyObjectFactory?
JW01
@ JW01 Anda dapat mengonfigurasi kode pembuat pengambil untuk membaca status internal dari kelas Anda dan menyesuaikannya. Menciptakan lebih dari satu contoh selama masa hidup akan sulit untuk diatur. Jika Anda mengalami situasi itu, saya sarankan desain ulang, daripada bekerja di sekitar. Jangan membuat getter Anda terlalu rumit atau mereka akan menjadi batu kilangan di leher Anda. Tujuan Anda adalah untuk memudahkan proses refactoring. Jika tampaknya terlalu sulit, pindah ke area lain untuk memperbaikinya di sana.
Gary Rowe
itu singleton. Hanya saja, jangan gunakan lajang O_O
CoffeDeveloper
@DarioOO Sebenarnya itu adalah singleton yang sangat buruk. Implementasi yang lebih baik akan menggunakan enum sesuai Josh Bloch. Singletons adalah pola desain yang valid dan memiliki kegunaannya (kreasi satu kali yang mahal, dll.).
Gary Rowe