Membuat objek yang besar dan tidak dapat diubah tanpa menggunakan konstruktor yang memiliki daftar parameter yang panjang

96

Saya memiliki beberapa objek besar (lebih dari 3 bidang) yang dapat dan harus tetap. Setiap kali saya mengalami kasus itu saya cenderung membuat kekejian konstruktor dengan daftar parameter yang panjang.

Rasanya tidak benar, sulit digunakan, dan sulit dibaca.

Lebih buruk lagi jika bidang adalah semacam jenis koleksi seperti daftar. Sederhana addSibling(S s)akan sangat memudahkan pembuatan objek tetapi membuat objek bisa berubah.

Apa yang kalian gunakan dalam kasus seperti itu?

Saya menggunakan Scala dan Java, tapi menurut saya masalahnya adalah bahasa agnostik selama bahasanya berorientasi objek.

Solusi yang dapat saya pikirkan:

  1. "Kekejian konstruktor dengan daftar parameter yang panjang"
  2. Pola Pembangun
Malax
sumber
5
Saya pikir pola pembangun adalah solusi terbaik dan paling standar untuk ini.
Zachary Wright
@Zachary: Apa yang Anda anjurkan hanya berfungsi untuk bentuk khusus dari pola pembangun, seperti yang dijelaskan di sini oleh Joshua Bloch: drdobbs.com/java/208403883?pgno=2 Saya lebih suka menggunakan istilah "antarmuka fasih" yang terkait untuk menelepon ini "bentuk khusus dari pola pembangun" (lihat jawaban saya).
SyntaxT3rr0r

Jawaban:

76

Nah, Anda ingin objek yang lebih mudah dibaca dan tidak dapat diubah setelah dibuat?

Saya pikir antarmuka yang lancar DILAKUKAN DENGAN BENAR akan membantu Anda.

Ini akan terlihat seperti ini (contoh yang dibuat murni):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

Saya menulis "SELESAI DENGAN BENAR" dengan huruf tebal karena sebagian besar programmer Java salah menggunakan antarmuka yang fasih dan mencemari objek mereka dengan metode yang diperlukan untuk membangun objek, yang tentu saja sepenuhnya salah.

Triknya adalah hanya metode build () yang benar-benar membuat Foo (karena itu Foo Anda tidak dapat diubah).

FooFactory.create () , di manaXXX (..) dan withXXX (..) semuanya menciptakan "sesuatu yang lain".

Bahwa sesuatu yang lain mungkin FooFactory, inilah salah satu cara untuk melakukannya ....

Anda FooFactory akan terlihat seperti ini:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}
SintaksT3rr0r
sumber
11
@ all: jangan ada keluhan tentang postfix "Impl" di "FooImpl": kelas ini tersembunyi di dalam pabrik dan tidak ada yang akan melihatnya selain orang yang menulis antarmuka yang lancar. Semua pengguna peduli adalah bahwa dia mendapat "Foo". Saya bisa saja memanggil "FooImpl" "FooPointlessNitpick" juga;)
SyntaxT3rr0r
5
Merasa pre-emptive? ;) Anda pernah dikritik di masa lalu tentang hal ini. :)
Greg D
3
Saya percaya kesalahan umum dia maksudkan adalah bahwa orang-orang menambahkan "withXXX" (dll) metode untuk itu Fooobjek, daripada harus terpisah FooFactory.
Dean Harding
3
Anda masih memiliki FooImplkonstruktor dengan 8 parameter. Apa perbaikannya?
Mot
4
Saya tidak akan menyebut kode ini immutable, dan saya akan takut orang menggunakan kembali objek pabrik karena mereka mengira begitu. Maksud saya: di FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();mana tallssekarang hanya berisi wanita. Ini seharusnya tidak terjadi dengan API yang tidak dapat diubah.
Thomas Ahle
60

Di Scala 2.8, Anda dapat menggunakan parameter bernama dan default serta copymetode pada kelas kasus. Berikut beberapa contoh kode:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))
Martin Odersky
sumber
31
1 untuk menemukan bahasa Scala. Ya, ini adalah penyalahgunaan sistem reputasi tapi ... aww ... Saya sangat menyukai Scala sehingga saya harus melakukannya. :)
Malax
1
Oh, man ... Saya baru saja menjawab sesuatu yang hampir identik! Yah, aku di perusahaan yang baik. :-) Saya ingin tahu bahwa saya tidak melihat jawaban Anda sebelumnya ... <shrug>
Daniel C. Sobral
20

Pertimbangkan ini di Scala 2.8:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Ini memang punya masalah, tentu saja. Misalnya, cobalah membuat espousedan Option[Person], lalu menikahi dua orang satu sama lain. Saya tidak dapat memikirkan cara untuk menyelesaikannya tanpa menggunakan salah satu private vardan / atau privatekonstruktor plus pabrik.

Daniel C. Sobral
sumber
11

Berikut ini beberapa opsi lainnya:

Pilihan 1

Jadikan implementasinya bisa berubah, tetapi pisahkan antarmuka yang diekspos menjadi bisa berubah dan tidak bisa diubah. Ini diambil dari desain perpustakaan Swing.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

pilihan 2

Jika aplikasi Anda berisi kumpulan objek permanen yang besar tapi telah ditentukan sebelumnya (misalnya, objek konfigurasi), Anda dapat mempertimbangkan untuk menggunakan framework Spring .

Tabel Bobby Kecil
sumber
3
Opsi 1 itu pintar (tapi tidak terlalu pintar), jadi saya menyukainya.
Timotius
4
Saya melakukan ini sebelumnya, tetapi menurut saya ini jauh dari solusi yang baik karena objeknya masih bisa berubah, hanya metode mutasi yang "disembunyikan". Mungkin saya terlalu pilih-pilih tentang hal ini ...
Malax
varian pada opsi 1 memiliki kelas terpisah untuk varian yang Tidak Dapat Diubah dan Diubah. Ini memberikan keamanan yang lebih baik daripada pendekatan antarmuka. Bisa dibilang Anda berbohong kepada konsumen API setiap kali Anda memberi mereka objek yang bisa berubah bernama Immutable yang hanya perlu dilemparkan ke antarmuka yang bisa berubah itu. Pendekatan kelas terpisah membutuhkan metode untuk mengubah bolak-balik. JodaTime API menggunakan pola ini. Lihat DateTime dan MutableDateTime.
toolbear
6

Ini membantu untuk mengingat ada berbagai jenis kekekalan . Untuk kasus Anda, menurut saya keabadian "es loli" akan bekerja dengan sangat baik:

Keabadian es loli: adalah apa yang secara aneh saya sebut sedikit melemahnya ketetapan sekali tulis. Orang bisa membayangkan sebuah objek atau bidang yang tetap bisa berubah untuk beberapa saat selama inisialisasi, dan kemudian menjadi "beku" selamanya. Jenis keabadian ini sangat berguna untuk objek yang tidak dapat diubah yang secara melingkar mereferensikan satu sama lain, atau objek yang tidak dapat diubah yang telah diserialkan ke disk dan setelah deserialisasi perlu "berubah-ubah" sampai seluruh proses deserialisasi selesai, pada titik mana semua benda mungkin beku.

Jadi, Anda menginisialisasi objek Anda, lalu menyetel semacam tanda "beku" yang menunjukkan bahwa objek tersebut tidak lagi dapat ditulis. Lebih disukai, Anda menyembunyikan mutasi di balik suatu fungsi sehingga fungsi tersebut masih murni untuk klien yang menggunakan API Anda.

Juliet
sumber
1
Suara negatif? Ada yang ingin memberikan komentar tentang mengapa ini bukan solusi yang baik?
Juliet
+1. Mungkin seseorang tidak menyukai ini karena Anda mengisyaratkan penggunaan clone()untuk mendapatkan contoh baru.
finnw
Atau mungkin karena dapat membahayakan keamanan thread di Java: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw
Kerugiannya adalah hal ini membutuhkan pengembang masa depan untuk berhati-hati dalam menangani flag "freeze". Jika metode mutator ditambahkan kemudian yang lupa untuk menegaskan bahwa metode tersebut tidak dibekukan, mungkin ada masalah. Demikian pula, jika konstruktor baru ditulis yang seharusnya, tetapi tidak, memanggil freeze()metode tersebut, semuanya bisa menjadi jelek.
stalepretzel
5

Anda juga dapat membuat objek yang tidak dapat diubah mengekspos metode yang terlihat seperti mutator (seperti addSibling) tetapi membiarkannya mengembalikan instance baru. Itulah yang dilakukan oleh koleksi Scala yang tidak dapat diubah.

Sisi negatifnya adalah Anda dapat membuat lebih banyak instance daripada yang diperlukan. Ini juga hanya berlaku jika ada konfigurasi valid perantara (seperti beberapa node tanpa saudara kandung yang ok dalam banyak kasus) kecuali Anda tidak ingin berurusan dengan objek yang dibangun sebagian.

Misalnya tepi grafik yang tidak memiliki tujuan belum merupakan tepi grafik yang valid.

ziggystar
sumber
Sisi negatifnya - membuat lebih banyak contoh daripada yang diperlukan - sebenarnya tidak terlalu menjadi masalah. Alokasi objek sangat murah, seperti pengumpulan sampah objek berumur pendek. Ketika analisis pelolosan diaktifkan secara default, jenis "objek perantara" ini cenderung dialokasikan tumpukan dan secara harfiah tidak ada biaya untuk membuatnya.
gustafc
2
@ Gustafc: Ya. Cliff Click pernah menceritakan kisah tentang bagaimana mereka membandingkan simulasi Koloni Semut Clojure Rich Hickey pada salah satu kotak besar mereka (864 core, 768 GB RAM): 700 thread paralel berjalan pada 700 core, masing-masing di 100%, menghasilkan lebih dari 20 GB sampah fana per detik . GC bahkan tidak berkeringat.
Jörg W Mittag
5

Pertimbangkan empat kemungkinan:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Bagi saya, masing-masing 2, 3, dan 4 disesuaikan dengan situasi yang berbeda. Yang pertama sulit untuk dicintai, karena alasan yang dikutip oleh OP, dan umumnya merupakan gejala desain yang telah merayap dan perlu beberapa refactoring.

Apa yang saya cantumkan sebagai (2) adalah baik bila tidak ada keadaan di balik 'pabrik', sedangkan (3) adalah desain pilihan bila ada keadaan. Saya menemukan diri saya menggunakan (2) daripada (3) ketika saya tidak ingin khawatir tentang utas dan sinkronisasi, dan saya tidak perlu khawatir tentang amortisasi beberapa penyiapan yang mahal selama produksi banyak objek. (3), di sisi lain, dipanggil ketika pekerjaan nyata masuk ke dalam pembangunan pabrik (menyiapkan dari SPI, membaca file konfigurasi, dll).

Terakhir, jawaban orang lain menyebutkan opsi (4), di mana Anda memiliki banyak objek kecil yang tidak dapat diubah dan pola yang lebih disukai adalah mendapatkan berita dari yang lama.

Perhatikan bahwa saya bukan anggota 'klub penggemar pola' - tentu saja, beberapa hal patut ditiru, tetapi bagi saya tampaknya mereka menjalani kehidupan yang tidak membantu begitu orang memberi mereka nama dan topi lucu.

bmargulies
sumber
6
Ini adalah Pola Builder (opsi 2)
Simon Nickerson
Bukankah itu adalah objek pabrik ('pembangun') yang memancarkan objeknya yang tidak dapat diubah?
bmargulies
perbedaan itu tampaknya cukup semantik. Bagaimana cara membuat kacang? Apa bedanya dengan pembangun?
Carl
Anda tidak ingin paket konvensi Java Bean untuk pembangun (atau yang lainnya).
Tom Hawtin - tackline
4

Opsi potensial lainnya adalah melakukan refaktorisasi agar memiliki lebih sedikit bidang yang dapat dikonfigurasi. Jika sekelompok bidang hanya bekerja (sebagian besar) satu sama lain, kumpulkan mereka ke dalam objek kecil mereka yang tidak dapat diubah. Konstruktor / pembangun objek "kecil" itu harus lebih mudah dikelola, seperti halnya konstruktor / pembangun untuk objek "besar" ini.

Carl
sumber
1
catatan: jarak tempuh untuk jawaban ini mungkin berbeda dengan masalah, basis kode, dan keterampilan pengembang.
Carl
2

Saya menggunakan C #, dan ini adalah pendekatan saya. Mempertimbangkan:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Opsi 1. Pembuat dengan parameter opsional

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Digunakan sebagai mis new Foo(5, b: new Bar(whatever)). Bukan untuk Java atau C # versi sebelum 4.0. tetapi tetap layak untuk ditampilkan, karena ini adalah contoh bagaimana tidak semua solusi bersifat tanpa bahasa.

Opsi 2. Pembuat mengambil objek parameter tunggal

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Contoh penggunaan:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

C # dari 3.0 ke atas membuatnya lebih elegan dengan sintaks penginisialisasi objek (secara semantik setara dengan contoh sebelumnya):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

Opsi 3:
Rancang ulang kelas Anda agar tidak membutuhkan banyak parameter. Anda dapat membagi tanggung jawabnya menjadi beberapa kelas. Atau berikan parameter bukan ke konstruktor tetapi hanya ke metode tertentu, sesuai permintaan. Tidak selalu dapat dijalankan, tetapi jika memungkinkan, itu layak dilakukan.

Joren
sumber