Konstruktor dengan banyak parameter vs pola pembangun

21

Sudah diketahui bahwa jika kelas Anda memiliki konstruktor dengan banyak parameter, katakan lebih dari 4, maka kemungkinan besar ini adalah bau kode . Anda perlu mempertimbangkan kembali jika kelas memuaskan SRP .

Tetapi bagaimana jika kita membangun dan objek yang tergantung pada 10 atau lebih parameter, dan akhirnya dengan mengatur semua parameter melalui pola Builder? Bayangkan, Anda membangun Personobjek jenis dengan info pribadinya, info kantor, info teman, info minat, info pendidikan, dan sebagainya. Ini sudah bagus, tetapi Anda entah bagaimana menetapkan lebih dari 4 parameter yang sama, bukan? Mengapa dua kasus ini tidak dianggap sama?

Narek
sumber

Jawaban:

25

Pola Builder tidak memecahkan "masalah" banyak argumen. Tetapi mengapa banyak argumen yang bermasalah?

  • Mereka mengindikasikan kelas Anda mungkin melakukan terlalu banyak . Namun, ada banyak jenis yang secara sah berisi banyak anggota yang tidak dapat dikelompokkan dengan bijaksana.
  • Menguji dan memahami suatu fungsi dengan banyak input menjadi lebih rumit secara eksponensial - secara harfiah!
  • Ketika bahasa tidak menawarkan parameter bernama, panggilan fungsi tidak mendokumentasikan diri . Membaca panggilan fungsi dengan banyak argumen cukup sulit karena Anda tidak tahu apa yang seharusnya dilakukan parameter ke-7. Anda bahkan tidak akan memperhatikan jika argumen ke-5 dan ke-6 bertukar secara tidak sengaja, terutama jika Anda menggunakan bahasa yang diketik secara dinamis atau semuanya terjadi sebagai string, atau ketika parameter terakhir adalah truekarena suatu alasan.

Memalsukan parameter bernama

The Builder Pola alamat hanya satu dari masalah ini, yaitu kekhawatiran pemeliharaan panggilan fungsi dengan banyak argumen * . Jadi panggilan fungsi suka

MyClass o = new MyClass(a, b, c, d, e, f, g);

mungkin menjadi

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

Pattern Pola Builder pada awalnya dimaksudkan sebagai pendekatan representasi-agnostik untuk merakit objek komposit, yang merupakan aspirasi yang jauh lebih besar daripada sekadar menyebutkan argumen untuk parameter. Secara khusus, pola pembangun tidak memerlukan antarmuka yang lancar.

Ini menawarkan sedikit keamanan ekstra karena akan meledak jika Anda memanggil metode pembangun yang tidak ada, tetapi jika tidak, Anda tidak akan mendapatkan apa pun yang tidak dimiliki komentar dalam panggilan konstruktor. Selain itu, membuat pembangun secara manual memerlukan kode, dan lebih banyak kode selalu mengandung lebih banyak bug.

Dalam bahasa yang mudah mendefinisikan tipe nilai baru, saya telah menemukan bahwa lebih baik menggunakan tipe microtyping / kecil untuk mensimulasikan argumen bernama. Dinamai demikian karena tipenya sangat kecil, tetapi akhirnya Anda mengetik lebih banyak ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Jelas, nama-nama jenis A, B, C, ... harus nama mendokumentasikan diri yang menggambarkan arti dari parameter, sering nama yang sama seperti yang Anda akan memberikan variabel parameter. Dibandingkan dengan idiom pembangun-untuk-nama-argumen, implementasi yang diperlukan jauh lebih sederhana, dan karenanya cenderung mengandung bug. Misalnya (dengan sintaks Java-ish):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

Kompiler membantu Anda menjamin bahwa semua argumen diberikan; dengan Builder Anda harus memeriksa secara manual argumen yang hilang, atau menyandikan mesin negara ke dalam sistem jenis bahasa host - keduanya kemungkinan mengandung bug.

Ada pendekatan umum lain untuk mensimulasikan argumen bernama: objek parameter abstrak tunggal yang menggunakan sintaks kelas inline untuk menginisialisasi semua bidang. Di Jawa:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

Namun, adalah mungkin untuk melupakan bidang, dan ini adalah solusi yang cukup spesifik untuk bahasa (saya telah melihat kegunaan dalam JavaScript, C #, dan C).

Untungnya, konstruktor masih dapat memvalidasi semua argumen, yang tidak terjadi ketika objek Anda dibuat dalam keadaan sebagian-dibangun, dan mengharuskan pengguna untuk memberikan argumen lebih lanjut melalui setter atau init()metode - yang memerlukan upaya pengkodean paling sedikit, tetapi buatlah lebih sulit untuk menulis program yang benar .

Jadi sementara ada banyak pendekatan untuk mengatasi "banyak parameter yang tidak disebutkan namanya membuat kode sulit untuk mempertahankan masalah", masalah lain tetap ada.

Mendekati masalah root

Misalnya masalah testability. Ketika saya menulis tes unit, saya perlu kemampuan untuk menyuntikkan data uji, dan untuk menyediakan implementasi tes untuk menghilangkan ketergantungan dan operasi yang memiliki efek samping eksternal. Saya tidak bisa melakukan itu ketika Anda membuat instance kelas dalam konstruktor Anda. Kecuali jika tanggung jawab kelas Anda adalah penciptaan objek lain, itu tidak boleh instantiate kelas non-sepele. Ini sejalan dengan masalah tanggung jawab tunggal. Semakin fokus tanggung jawab kelas, semakin mudah untuk menguji (dan sering lebih mudah digunakan).

Pendekatan termudah dan sering terbaik adalah untuk konstruktor untuk mengambil dependensi yang sepenuhnya dibangun sebagai parameter , meskipun ini mendorong tanggung jawab mengelola dependensi kepada penelepon - tidak ideal juga, kecuali dependensi adalah entitas independen dalam model domain Anda.

Kadang-kadang pabrik (abstrak) atau kerangka kerja injeksi ketergantungan penuh digunakan sebagai gantinya, meskipun ini mungkin berlebihan dalam sebagian besar kasus penggunaan. Secara khusus, ini hanya mengurangi jumlah argumen jika banyak dari argumen ini adalah objek kuasi-global atau nilai konfigurasi yang tidak berubah di antara instance objek. Misal jika parameter adan dglobal-ish, kita akan dapatkan

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Bergantung pada aplikasinya, ini mungkin merupakan game-changer di mana metode pabrik pada akhirnya hampir tidak memiliki argumen karena semua dapat disediakan oleh manajer dependensi, atau mungkin sejumlah besar kode yang mempersulit instantiasi tanpa manfaat yang jelas. Pabrik-pabrik semacam itu jauh lebih berguna untuk memetakan antarmuka ke tipe-tipe konkret daripada untuk mengatur parameter. Namun, pendekatan ini mencoba untuk mengatasi masalah root dari terlalu banyak parameter daripada hanya menyembunyikannya dengan antarmuka yang cukup lancar.

amon
sumber
Saya benar-benar akan menentang bagian yang mendokumentasikan diri. Jika Anda memiliki komentar layak IDE + yang layak, sebagian besar waktu konstruktor dan definisi parameter adalah satu keystroke jauh.
JavierIEH
Di Android Studio 3.0, nama parameter di konstruktor ditampilkan berdekatan dengan nilai yang diteruskan dalam panggilan konstruktor. mis: A baru (operan1: 34, operand2: 56); operand1 dan operand2 adalah nama-nama paramter di konstruktor. Mereka ditunjukkan oleh IDE untuk membuat kode lebih mudah dibaca. Jadi, tidak perlu pergi ke definisi untuk mencari tahu apa parameternya.
garnet
9

Pola pembangun tidak menyelesaikan apa pun untuk Anda dan tidak memperbaiki kegagalan desain.

Jika Anda memiliki kelas yang membutuhkan 10 parameter untuk dibangun, membuat pembangun untuk membangunnya tidak akan tiba-tiba membuat desain Anda lebih baik. Anda harus memilih untuk refactoring kelas yang bersangkutan.

Di sisi lain, jika Anda memiliki kelas, mungkin DTO sederhana, di mana atribut tertentu dari kelas adalah opsional, pola pembangun dapat memudahkan pembangunan objek tersebut.

Andy
sumber
1

Setiap bagian misalnya info pribadi, info pekerjaan (setiap "tugas" pekerjaan, setiap teman, dll., Harus dikonversi menjadi objek mereka sendiri.

Pertimbangkan bagaimana Anda akan menerapkan fungsi pembaruan. Pengguna ingin menambahkan riwayat kerja baru. Pengguna tidak ingin mengubah bagian lain dari informasi, atau riwayat pekerjaan sebelumnya - tetap sama saja.

Ketika ada banyak informasi, membangun informasi satu per satu tidak terhindarkan. Anda dapat mengelompokkan potongan-potongan tersebut secara logis - berdasarkan intuisi manusia (yang membuatnya lebih mudah digunakan oleh seorang programmer), atau berdasarkan pola penggunaan (informasi apa yang biasanya diperbarui pada saat yang bersamaan).

Pola pembangun hanyalah cara untuk menangkap daftar (diperintahkan atau tidak disusun) dari argumen yang diketik, yang kemudian dapat diteruskan ke konstruktor yang sebenarnya (atau konstruktor dapat membaca argumen yang ditangkap pembangun).

Pedoman 4 hanyalah aturan praktis. Ada situasi yang membutuhkan lebih dari 4 argumen dan tidak ada cara yang masuk akal atau logis untuk mengelompokkannya. Dalam kasus-kasus itu, mungkin masuk akal untuk membuat structyang dapat dihuni secara langsung atau dengan pemukim properti. Perhatikan bahwa structdan pembangun dalam kasus ini memiliki tujuan yang sangat mirip.

Namun, dalam contoh Anda, Anda telah menggambarkan apa yang akan menjadi cara logis untuk mengelompokkan mereka. Jika Anda menghadapi situasi di mana ini tidak benar, mungkin Anda dapat menggambarkannya dengan contoh berbeda.

rwong
sumber