Cara meningkatkan Pola Builder Bloch, untuk membuatnya lebih tepat untuk digunakan di kelas yang sangat luas

34

Saya telah sangat dipengaruhi oleh buku Java Efektif Joshua Bloch (edisi ke-2), mungkin lebih dari buku pemrograman yang saya baca. Secara khusus, Pola Pembuatnya (item 2) memiliki efek terbesar.

Meskipun pembangun Bloch membuat saya lebih jauh dalam beberapa bulan daripada dalam sepuluh tahun terakhir pemrograman saya, saya masih menemukan diri saya mengenai dinding yang sama: Memperluas kelas dengan rantai metode pengembalian sendiri paling tidak mengecilkan hati, dan paling buruk mimpi buruk --terutama ketika obat generik ikut bermain, dan terutama dengan obat referensial mandiri (seperti Comparable<T extends Comparable<T>>).

Ada dua kebutuhan utama yang saya miliki, hanya yang kedua yang ingin saya fokuskan dalam pertanyaan ini:

  1. Masalah pertama adalah "bagaimana cara berbagi rantai metode yang kembali sendiri, tanpa harus mengimplementasikannya kembali di setiap ... satu ... kelas?" Bagi mereka yang mungkin penasaran, saya sudah membahas bagian ini di bagian bawah pos jawaban ini, tetapi bukan itu yang ingin saya fokuskan di sini.

  2. Masalah kedua, yang saya minta komentar, adalah "bagaimana saya bisa mengimplementasikan pembangun di kelas yang sendiri dimaksudkan untuk diperpanjang oleh banyak kelas lainnya?" Memperluas kelas dengan pembangun secara alami lebih sulit daripada memperpanjang tanpa. Memperluas kelas yang memiliki pembangun yang juga mengimplementasikan Needable, dan karena itu memiliki generik signifikan yang terkait dengannya , sulit digunakan.

Jadi itulah pertanyaan saya: Bagaimana saya bisa meningkatkan (apa yang saya sebut) Bloch Builder, sehingga saya bisa merasa bebas untuk memasang builder ke kelas mana pun - bahkan ketika kelas itu dimaksudkan sebagai "kelas dasar" yang mungkin diperpanjang dan diperluas berkali-kali lipat - tanpa mengecilkan masa depan saya sendiri, atau pengguna perpustakaan saya , karena bagasi tambahan yang dibuat oleh pembangun (dan generik potensial)?


Tambahan
Pertanyaan saya berfokus pada bagian 2 di atas, tetapi saya ingin menguraikan sedikit masalah, termasuk bagaimana saya mengatasinya:

Masalah pertama adalah "bagaimana cara berbagi rantai metode yang kembali sendiri, tanpa harus mengimplementasikannya kembali di setiap ... satu ... kelas?" Ini bukan untuk mencegah perluasan kelas dari harus mengimplementasikan kembali rantai ini, yang, tentu saja, mereka harus - lebih tepatnya, bagaimana mencegah non-sub-kelas , yang ingin mengambil keuntungan dari rantai metode ini, dari keharusan untuk me -menerapkan setiap fungsi yang dikembalikan sendiri agar pengguna mereka dapat memanfaatkannya? Untuk ini saya telah datang dengan desain yang sangat dibutuhkan yang saya akan mencetak kerangka antarmuka untuk di sini, dan biarkan saja sampai sekarang. Ini telah bekerja dengan baik untuk saya (desain ini bertahun-tahun dalam pembuatan ... bagian tersulit adalah menghindari ketergantungan melingkar):

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}
Aliteralmind
sumber

Jawaban:

21

Saya telah menciptakan apa yang, bagi saya, merupakan peningkatan besar di atas Pola Pembangun Josh Bloch. Tidak mengatakan dengan cara apa pun bahwa itu "lebih baik", hanya saja dalam situasi yang sangat spesifik , itu memang memberikan beberapa keuntungan - yang terbesar adalah bahwa ia memisahkan pembangun dari kelas yang akan dibangun.

Saya telah mendokumentasikan alternatif ini di bawah ini, yang saya sebut Pola Pembuat Blind.


Pola Desain: Blind Builder

Sebagai alternatif dari Joshua Bloch's Builder Pattern (item 2 di Java Efektif, edisi ke-2), saya telah menciptakan apa yang saya sebut "Blind Builder Pattern", yang berbagi banyak manfaat dari Bloch Builder dan, selain dari satu karakter, digunakan dengan cara yang persis sama. Blind Builders memiliki kelebihan

  • memisahkan pembangun dari kelas terlampir, menghilangkan ketergantungan melingkar,
  • sangat mengurangi ukuran kode sumber dari (apa yang tidak lagi ) kelas melampirkan, dan
  • memungkinkan ToBeBuiltkelas untuk diperpanjang tanpa harus memperpanjang pembangunnya .

Dalam dokumentasi ini, saya akan merujuk kelas yang sedang dibangun sebagai kelas " ToBeBuilt".

Kelas diimplementasikan dengan Bloch Builder

Bloch Builder adalah yang public static classterkandung di dalam kelas yang dibangunnya. Sebuah contoh:

UserConfig kelas publik {
   sName String pribadi akhir;
   iAge akhir pribadi;
   private final String sFavColor;
   UserConfig publik (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //transfer
         coba {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            melempar NullPointerException baru ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // VALIDASI SEMUA BIDANG DI SINI
   }
   public String toString () {
      kembalikan "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...MULAI
   public static class Cfg {
      private String sName;
      iAge pribadi int;
      String pribadi sFavColor;
      Cfg publik (String s_name) {
         sName = s_name;
      }
      // setters yang kembali sendiri ... MULAI
         usia Cfg publik (int i_age) {
            iAge = i_age;
            kembalikan ini;
         }
         Warna Cfg publik (String s_color) {
            sFavColor = s_color;
            kembalikan ini;
         }
      // pengaturan kembali sendiri ... AKHIR
      build UserConfig publik () {
         return (UserConfig baru (ini));
      }
   }
   //builder...END
}

Instantiating kelas dengan Bloch Builder

UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();

Kelas yang sama, diimplementasikan sebagai Blind Builder

Ada tiga bagian Blind Builder, yang masing-masingnya ada dalam file kode-sumber yang terpisah:

  1. The ToBeBuiltclass (dalam contoh ini: UserConfig)
  2. FieldableAntarmukanya " "
  3. Pembangun

1. Kelas yang akan dibangun

Kelas yang akan dibangun menerima Fieldableantarmuka sebagai satu-satunya parameter konstruktor. Konstruktor menetapkan semua bidang internal dari itu, dan memvalidasi masing-masing. Yang paling penting, ToBeBuiltkelas ini tidak memiliki pengetahuan tentang pembangunnya.

UserConfig kelas publik {
   sName String pribadi akhir;
   iAge akhir pribadi;
   private final String sFavColor;
    UserConfig publik (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //transfer
         coba {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            melempar NullPointerException baru ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // VALIDASI SEMUA BIDANG DI SINI
   }
   public String toString () {
      kembalikan "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Seperti dicatat oleh satu komentator cerdas (yang secara tidak dapat dijelaskan menghapus jawaban mereka), jika ToBeBuiltkelas juga mengimplementasikannya Fieldable, konstruktor satu-satunya dapat digunakan baik sebagai konstruktor utama maupun salin (kelemahannya adalah bidang selalu divalidasi, meskipun diketahui bahwa bidang dalam aslinya asli ToBeBuilt).

2. FieldableAntarmuka " "

Antarmuka fieldable adalah "jembatan" antara ToBeBuiltkelas dan pembangunnya, mendefinisikan semua bidang yang diperlukan untuk membangun objek. Antarmuka ini diperlukan oleh ToBeBuiltkonstruktor kelas, dan diimplementasikan oleh pembangun. Karena antarmuka ini dapat diimplementasikan oleh kelas selain pembangun, setiap kelas dapat dengan mudah membuat instance ToBeBuiltkelas, tanpa dipaksa untuk menggunakan pembangunnya. Ini juga memudahkan untuk memperluas ToBeBuiltkelas, ketika memperluas pembangunnya tidak diinginkan atau diperlukan.

Seperti yang dijelaskan di bagian di bawah ini, saya tidak mendokumentasikan fungsi-fungsi di antarmuka ini sama sekali.

antarmuka publik UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

3. Pembangun

Pembangun mengimplementasikan Fieldablekelas. Tidak ada validasi sama sekali, dan untuk menekankan fakta ini, semua bidangnya bersifat publik dan dapat berubah. Walaupun aksesibilitas publik ini bukan keharusan, saya lebih suka dan merekomendasikannya, karena ini menegaskan kembali fakta bahwa validasi tidak terjadi sampai ToBeBuiltkonstruktor dipanggil. Ini penting, karena ada kemungkinan utas lain untuk memanipulasi pembangun lebih lanjut, sebelum diteruskan ke ToBeBuiltkonstruktor. Satu-satunya cara untuk menjamin bidang adalah valid - dengan asumsi pembangun tidak dapat "mengunci" keadaannya - adalah bagi ToBeBuiltkelas untuk melakukan pemeriksaan terakhir.

Akhirnya, seperti halnya Fieldableantarmuka, saya tidak mendokumentasikan getternya.

UserConfig_Cfg kelas publik mengimplementasikan UserConfig_Fieldable {
   public String sName;
   public int iAge;
    String publik sFavColor;
    UserConfig_Cfg publik (String s_name) {
       sName = s_name;
    }
    // setters yang kembali sendiri ... MULAI
       Usia UserConfig_Cfg publik (int i_age) {
          iAge = i_age;
          kembalikan ini;
       }
       UserConfig_Cfg publicColor publik (String s_color) {
          sFavColor = s_color;
          kembalikan ini;
       }
    // pengaturan kembali sendiri ... AKHIR
    //getters...START
       public String getName () {
          return sName;
       }
       public int getAge () {
          mengembalikan iAge;
       }
       public String getFavoriteColor () {
          kembalikan sFavColor;
       }
    //getters...END
    build UserConfig publik () {
       return (UserConfig baru (ini));
    }
}

Instantiating kelas dengan Blind Builder

UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();

Satu-satunya perbedaan adalah " UserConfig_Cfg" bukan " UserConfig.Cfg"

Catatan

Kekurangan:

  • Pembangun Buta tidak dapat mengakses anggota pribadi dari ToBeBuiltkelasnya,
  • Mereka lebih bertele-tele, karena getter sekarang diperlukan baik di builder maupun di interface.
  • Semuanya untuk satu kelas tidak lagi hanya di satu tempat .

Mengkompilasi Blind Builder sangat mudah:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

The Fieldableantarmuka sepenuhnya opsional

Untuk ToBeBuiltkelas dengan beberapa bidang wajib - seperti UserConfigkelas contoh ini , konstruktornya bisa saja

UserConfig publik (String s_name, int i_age, String s_favColor) {

Dan memanggil pembangun dengan

build UserConfig publik () {
   return (UserConfig baru (getName (), getAge (), getFavoriteColor ()));
}

Atau bahkan dengan menghilangkan getter (di builder) sama sekali:

   return (UserConfig baru (sName, iAge, sFavoriteColor));

Dengan melewati bidang secara langsung, ToBeBuiltkelas sama "buta" (tidak menyadari pembangunnya) seperti halnya dengan Fieldableantarmuka. Namun, untuk ToBeBuiltkelas yang dan dimaksudkan untuk "diperluas dan disubstitusi berkali-kali" (yang ada dalam judul tulisan ini), setiap perubahan pada bidang apa pun mengharuskan perubahan di setiap sub-kelas, di setiap pembangun dan ToBeBuiltkonstruktor. Karena jumlah bidang dan subkelas meningkat, ini menjadi tidak praktis untuk dipertahankan.

(Memang, dengan beberapa bidang yang diperlukan, menggunakan pembangun sama sekali mungkin berlebihan. Bagi mereka yang tertarik, berikut adalah contoh dari beberapa antarmuka Fieldable yang lebih besar di perpustakaan pribadi saya.)

Kelas menengah dalam sub-paket

Saya memilih untuk memiliki semua pembangun dan Fieldablekelas, untuk semua Pembuat Buta, dalam sub-paket ToBeBuiltkelas mereka . Sub-paket selalu dinamai " z". Ini mencegah kelas-kelas sekunder dari mengacaukan daftar paket JavaDoc. Sebagai contoh

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

Contoh validasi

Seperti disebutkan di atas, semua validasi terjadi di ToBeBuilt's konstruktor. Berikut ini konstruktor lagi dengan contoh kode validasi:

UserConfig publik (UserConfig_Fieldable uc_f) {
   //transfer
      coba {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         melempar NullPointerException baru ("uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // validasi (harus benar-benar melakukan pre-kompilasi pola ...)
      coba {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            melempar IllegalArgumentException baru ("uc_f.getName () (\" "+ sName +" \ ") mungkin tidak kosong, dan harus berisi hanya angka digit dan garis bawah.");
         }
      } catch (NullPointerException rx) {
         melempar NullPointerException baru ("uc_f.getName ()");
      }
      if (iAge <0) {
         lempar IllegalArgumentException baru ("uc_f.getAge () (" + iAge + ") kurang dari nol.");
      }
      coba {
         if (! Pattern.compile ("(?: red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
            melempar IllegalArgumentException baru ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") bukan merah, biru, hijau, atau hot pink.");
         }
      } catch (NullPointerException rx) {
         melempar NullPointerException baru ("uc_f.getFavoriteColor ()");
      }
}

Mendokumentasikan Pembangun

Bagian ini berlaku untuk Bloch Builders dan Blind Builders. Ini menunjukkan bagaimana saya mendokumentasikan kelas-kelas dalam desain ini, membuat setter (dalam pembangun) dan getter mereka (dalam ToBeBuiltkelas) secara langsung direferensikan silang satu sama lain - dengan satu klik mouse, dan tanpa pengguna perlu tahu di mana fungsi-fungsi tersebut sebenarnya berada - dan tanpa pengembang harus mendokumentasikan apa pun secara berlebihan.

Getters: Hanya di ToBeBuiltkelas

Getters didokumentasikan hanya di dalam ToBeBuiltkelas. Getter yang setara baik di kelas _Fieldabledan_Cfg diabaikan. Saya tidak mendokumentasikannya sama sekali.

/ **
   <P> Usia pengguna. </P>
   @return An int mewakili usia pengguna.
   @lihat UserConfig_Cfg # age (int)
   @lihat getName ()
 ** /
public int getAge () {
   mengembalikan iAge;
}

Yang pertama @seeadalah tautan ke setter-nya, yang ada di kelas builder.

Setter: Di kelas pembangun

Setter didokumentasikan seolah-olah itu adalah di ToBeBuiltkelas , dan juga seolah itu melakukan validasi (yang benar-benar dilakukan oleh ToBeBuilt's konstruktor). Tanda bintang (" *") adalah petunjuk visual yang menunjukkan bahwa target tautan ada di kelas lain.

/ **
   <P> Tetapkan usia pengguna. </P>
   @param i_age Mungkin tidak kurang dari nol. Dapatkan dengan {@code UserConfig # getName () getName ()} *.
   @lihat #favoriteColor (String)
 ** /
Usia UserConfig_Cfg publik (int i_age) {
   iAge = i_age;
   kembalikan ini;
}

Informasi lebih lanjut

Menyatukan semuanya: Sumber lengkap contoh Blind Builder, dengan dokumentasi lengkap

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P> Informasi tentang pengguna - <I> [builder: UserConfig_Cfg] </I> </P>
   <P> Validasi semua bidang terjadi di konstruktor kelas ini. Namun, setiap persyaratan validasi hanya dokumen dalam fungsi setter pembuat. </P>
   <P> {@code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </P>
 ** /
UserConfig kelas publik {
   public static final void main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
      System.out.println (uc);
   }
   sName String pribadi akhir;
   iAge akhir pribadi;
   private final String sFavColor;
   / **
      <P> Buat instance baru. Ini menetapkan dan memvalidasi semua bidang. </P>
      @param uc_f Mungkin bukan {@code null}.
    ** /
   UserConfig publik (UserConfig_Fieldable uc_f) {
      //transfer
         coba {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            melempar NullPointerException baru ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      //mengesahkan
         coba {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               melempar IllegalArgumentException baru ("uc_f.getName () (\" "+ sName +" \ ") mungkin tidak kosong, dan harus berisi hanya angka digit dan garis bawah.");
            }
         } catch (NullPointerException rx) {
            melempar NullPointerException baru ("uc_f.getName ()");
         }
         if (iAge <0) {
            lempar IllegalArgumentException baru ("uc_f.getAge () (" + iAge + ") kurang dari nol.");
         }
         coba {
            if (! Pattern.compile ("(?: red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
               melempar IllegalArgumentException baru ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") bukan merah, biru, hijau, atau hot pink.");
            }
         } catch (NullPointerException rx) {
            melempar NullPointerException baru ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> Nama pengguna. </P>
         @ return A non - {@ code null}, string tidak kosong.
         @lihat UserConfig_Cfg # UserConfig_Cfg (String)
         @lihat #getAge ()
         @lihat #getFavoriteColor ()
       ** /
      public String getName () {
         return sName;
      }
      / **
         <P> Usia pengguna. </P>
         @ return A angka yang lebih besar dari atau sama dengan nol.
         @lihat UserConfig_Cfg # age (int)
         @lihat #getName ()
       ** /
      public int getAge () {
         mengembalikan iAge;
      }
      / **
         <P> Warna favorit pengguna. </P>
         @ return A non - {@ code null}, string tidak kosong.
         @lihat UserConfig_Cfg # age (int)
         @lihat #getName ()
       ** /
      public String getFavoriteColor () {
         kembalikan sFavColor;
      }
   //getters...END
   public String toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Diperlukan oleh konstruktor {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </P>
 ** /
antarmuka publik UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> Pembuat untuk {@link UserConfig}. </P>
   <P> Validasi semua bidang terjadi di konstruktor <CODE> UserConfig </CODE>. Namun, setiap persyaratan validasi hanya dokumen dalam fungsi setter kelas ini. </P>
 ** /
UserConfig_Cfg kelas publik mengimplementasikan UserConfig_Fieldable {
   public String sName;
   public int iAge;
   String publik sFavColor;
   / **
      <P> Buat instance baru dengan nama pengguna. </P>
      @param s_name Tidak boleh {@code null} atau kosong, dan hanya boleh berisi huruf, angka, dan garis bawah. Dapatkan dengan {@code UserConfig # getName () getName ()} {@ code ()} .
    ** /
   UserConfig_Cfg publik (String s_name) {
      sName = s_name;
   }
   // setters yang kembali sendiri ... MULAI
      / **
         <P> Tetapkan usia pengguna. </P>
         @param i_age Mungkin tidak kurang dari nol. Dapatkan dengan {@code UserConfig # getName () getName ()} {@ code ()} .
         @lihat #favoriteColor (String)
       ** /
      Usia UserConfig_Cfg publik (int i_age) {
         iAge = i_age;
         kembalikan ini;
      }
      / **
         <P> Tetapkan warna favorit pengguna. </P>
         @param s_color Harus {@code "red"}, {@code "blue"}, {@code green}, atau {@code "hot pink"}. Dapatkan dengan {@code UserConfig # getName () getName ()} {@ code ()} *.
         @lihat #age (int)
       ** /
      UserConfig_Cfg publicColor publik (String s_color) {
         sFavColor = s_color;
         kembalikan ini;
      }
   // pengaturan kembali sendiri ... AKHIR
   //getters...START
      public String getName () {
         return sName;
      }
      public int getAge () {
         mengembalikan iAge;
      }
      public String getFavoriteColor () {
         kembalikan sFavColor;
      }
   //getters...END
   / **
      <P> Bangun UserConfig, sesuai konfigurasi. </P>
      @return <CODE> (baru {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (ini)) </CODE>
    ** /
   build UserConfig publik () {
      return (UserConfig baru (ini));
   }
}

Aliteralmind
sumber
1
Jelas, ini merupakan peningkatan. The Bloch's Builder, sebagaimana diterapkan di sini, memasangkan dua kelas konkret , ini menjadi kelas yang harus dibangun dan pembangunnya. Ini adalah desain yang buruk per se . Blind Builder yang Anda deskripsikan istirahat yang menyertai dengan memiliki kelas yang akan dibangun mendefinisikan ketergantungan konstruksinya sebagai abstraksi , yang dapat diterapkan oleh kelas-kelas lain secara terpisah. Anda telah sangat menerapkan apa yang merupakan pedoman desain berorientasi objek yang penting.
rucamzu
3
Anda harus benar-benar blog tentang ini di suatu tempat jika Anda belum, desain algoritma yang bagus! Saya tidak akan membagikannya sekarang :-).
Martijn Verburg
4
Terima kasih atas kata-kata baiknya. Sekarang ini adalah posting pertama di blog baru saya: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind
Jika pembangun dan objek yang dibangun mengimplementasikan Fieldable, polanya mulai menyerupai yang saya sebut sebagai ReadableFoo / MutableFoo / ImmutableFoo, meskipun alih-alih memiliki metode untuk membuat hal yang bisa berubah menjadi "build" anggota pembangun, saya menyebutnya asImmutabledan memasukkannya dalam ReadableFooantarmuka [menggunakan filosofi itu, memanggil buildobjek yang tidak dapat diubah hanya akan mengembalikan referensi ke objek yang sama].
supercat
1
@ThomasN Anda perlu memperluas *_Fieldabledan menambahkan getter baru ke dalamnya, dan memperpanjang *_Cfg, dan menambahkan setter baru ke dalamnya, tapi saya tidak melihat mengapa Anda perlu mereproduksi getter dan setter yang ada. Mereka diwarisi, dan kecuali mereka membutuhkan fungsionalitas yang berbeda, tidak perlu membuatnya kembali.
aliteralmind
13

Saya pikir pertanyaan di sini mengasumsikan sesuatu dari awal tanpa berusaha membuktikannya, bahwa pola pembangun pada dasarnya baik.

tl; dr Saya pikir pola pembangun jarang merupakan ide yang bagus.


Tujuan pola pembangun

Tujuan dari pola pembangun adalah untuk mempertahankan dua aturan yang akan membuat konsumsi kelas Anda lebih mudah:

  1. Objek seharusnya tidak dapat dibangun dalam keadaan tidak konsisten / tidak dapat digunakan / tidak valid.

    • Ini merujuk pada skenario di mana misalnya sebuah Personobjek dapat dibangun tanpa Iddiisi, sementara semua bagian kode yang menggunakan objek itu mungkin memerlukan hak Iduntuk bekerja dengan benar Person.
  2. Konstruktor objek seharusnya tidak memerlukan terlalu banyak parameter .

Jadi tujuan dari pola pembangun adalah non-kontroversial. Saya pikir banyak keinginan dan penggunaannya didasarkan pada analisis yang pada dasarnya telah berjalan sejauh ini: Kami menginginkan dua aturan ini, ini memberikan dua aturan ini - meskipun saya pikir perlu menyelidiki cara lain untuk menyelesaikan kedua aturan itu.


Mengapa repot-repot melihat pendekatan lain?

Saya pikir alasannya ditunjukkan dengan baik oleh fakta dari pertanyaan ini sendiri; ada kerumitan dan banyak upacara ditambahkan ke struktur dalam menerapkan pola pembangun kepada mereka. Pertanyaan ini menanyakan bagaimana menyelesaikan beberapa kompleksitas itu karena seperti kompleksitas tidak, itu membuat skenario yang berperilaku aneh (mewarisi). Kompleksitas ini juga meningkatkan overhead pemeliharaan (menambah, mengubah, atau menghapus properti jauh lebih kompleks daripada yang lainnya).


Pendekatan lain

Jadi untuk aturan nomor satu di atas, pendekatan apa yang ada? Kunci aturan ini merujuk adalah bahwa pada konstruksi, sebuah objek memiliki semua informasi yang diperlukan untuk berfungsi dengan baik - dan setelah konstruksi bahwa informasi tidak dapat diubah secara eksternal (jadi itu informasi abadi).

Salah satu cara untuk memberikan semua informasi yang diperlukan ke objek pada konstruksi adalah dengan menambahkan parameter ke konstruktor. Jika informasi itu diminta oleh konstruktor, Anda tidak akan dapat membangun objek ini tanpa semua informasi itu, oleh karena itu akan dibangun menjadi keadaan yang valid. Tetapi bagaimana jika objek tersebut membutuhkan banyak informasi agar valid? Oh sial, jika itu masalahnya pendekatan ini akan melanggar aturan # 2 di atas .

Ok apa lagi yang ada? Anda dapat mengambil semua informasi yang diperlukan agar objek Anda berada dalam keadaan konsisten, dan menggabungkannya ke objek lain yang diambil pada waktu konstruksi. Kode Anda di atas alih-alih memiliki pola builder akan menjadi:

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

Ini tidak jauh berbeda dari pola builder, meskipun sedikit lebih sederhana, dan yang paling penting kami memenuhi aturan # 1 dan aturan # 2 sekarang .

Jadi mengapa tidak melakukan sedikit tambahan dan membuatnya menjadi pembangun penuh? Itu tidak perlu . Saya memuaskan kedua tujuan pola builder dalam pendekatan ini, dengan sesuatu yang sedikit lebih sederhana, lebih mudah untuk dipelihara, dan dapat digunakan kembali . Itu bit terakhir adalah kunci, contoh ini digunakan adalah imajiner dan tidak cocok untuk tujuan semantik dunia nyata, jadi mari kita tunjukkan bagaimana pendekatan ini menghasilkan DTO yang dapat digunakan kembali daripada kelas tujuan tunggal .

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

Jadi ketika Anda membangun DTO yang kohesif seperti ini, keduanya dapat memenuhi tujuan pola pembangun, lebih sederhana, dan dengan nilai / kegunaan yang lebih luas. Lebih jauh lagi, pendekatan ini memecahkan kompleksitas warisan yang dihasilkan oleh pola pembangun:

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

Anda mungkin menemukan DTO tidak selalu kohesif, atau untuk membuat pengelompokan properti kohesif, mereka perlu dipecah di beberapa DTO - ini bukan masalah. Jika objek Anda memerlukan 18 properti dan Anda bisa membuat 3 DTO yang kohesif dengan properti itu, Anda punya konstruksi sederhana yang memenuhi tujuan pembangun, dan kemudian beberapa. Jika Anda tidak dapat membuat pengelompokan kohesif, ini mungkin merupakan tanda bahwa objek Anda tidak kohesif jika mereka memiliki properti yang benar-benar tidak terkait - tetapi bahkan kemudian membuat satu DTO non-kohesif masih lebih disukai karena penerapan yang lebih sederhana plus menyelesaikan masalah warisan Anda.


Cara memperbaiki pola pembangun

Ok jadi semua rimble mengibas ke samping, Anda memiliki masalah dan mencari pendekatan desain untuk menyelesaikannya. Saran saya: mewarisi kelas dapat memiliki kelas bertingkat yang mewarisi dari kelas pembangun kelas super, sehingga kelas pewaris pada dasarnya memiliki struktur yang sama dengan kelas super dan memiliki pola pembangun yang harus berfungsi sama persis dengan fungsi tambahan untuk properti tambahan dari sub-kelas ..


Ketika itu ide yang bagus

Mengesampingkan, pola pembangun memiliki ceruk . Kita semua tahu itu karena kita semua mempelajari pembangun khusus ini pada satu titik atau yang lain: StringBuilder- di sini tujuannya bukan konstruksi sederhana, karena string tidak dapat lebih mudah untuk dibangun dan digabungkan dll. Ini adalah pembangun yang hebat karena memiliki manfaat kinerja .

Karenanya, manfaat kinerjanya adalah: Anda memiliki banyak objek, mereka adalah tipe yang tidak dapat diubah, Anda perlu mengelompokkannya menjadi satu objek dari tipe yang tidak dapat diubah. Jika Anda melakukannya secara bertahap, Anda akan membuat banyak objek perantara yang dibuat di sini, jadi melakukannya sekaligus jauh lebih berkinerja dan ideal.

Jadi saya pikir kunci ketika itu adalah ide yang baik adalah dalam domain masalah StringBuilder: Membutuhkan untuk mengubah beberapa instance dari tipe yang tidak dapat diubah menjadi sebuah instance tunggal dari tipe yang tidak dapat diubah .

Jimmy Hoffa
sumber
Saya tidak berpikir contoh yang diberikan Anda memenuhi aturan baik. Tidak ada yang menghentikan saya membuat Cfg dalam keadaan tidak valid, dan sementara parameter telah dipindahkan dari ctor mereka baru saja dipindahkan ke tempat yang kurang idiomatis dan lebih bertele-tele. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()menawarkan API ringkas untuk membuat foo dan dapat menawarkan pengecekan kesalahan aktual di pembangun itu sendiri. Tanpa pembangun objek itu sendiri harus memeriksa inputnya, yang berarti kita tidak lebih baik dari dulu.
Phoshi
DTO dapat memiliki sifat mereka divalidasi dalam banyak cara secara deklaratif dengan anotasi, pada setter, namun Anda ingin melakukannya - validasi adalah masalah yang terpisah dan dalam pendekatan pembangunnya ia menunjukkan validasi yang terjadi di konstruktor, bahwa logika yang sama akan cocok dengan baik dalam pendekatan saya. Namun pada umumnya akan lebih baik menggunakan DTO untuk memvalidasinya karena seperti yang saya tunjukkan - DTO dapat digunakan untuk membangun beberapa jenis dan dengan demikian memiliki validasi di atasnya akan cocok untuk memvalidasi beberapa jenis. Builder hanya memvalidasi untuk jenis tertentu yang dibuat untuknya.
Jimmy Hoffa
Mungkin cara yang paling fleksibel adalah memiliki fungsi validasi statis di builder, yang menerima satu Fieldableparameter. Saya akan memanggil fungsi validasi ini dari ToBeBuiltkonstruktor, tetapi bisa dipanggil oleh apa saja, dari mana saja. Ini menghilangkan potensi kode redundan, tanpa memaksakan implementasi tertentu. (Dan tidak ada yang menghentikan Anda dari meneruskan bidang individual ke fungsi validasi, jika Anda tidak menyukai Fieldablekonsep - tetapi sekarang akan ada setidaknya tiga tempat di mana daftar bidang harus dipertahankan.)
aliteralmind
+1 Dan sebuah kelas yang memiliki terlalu banyak dependensi dalam konstruktornya jelas tidak cukup kohesif dan harus di refactored ke kelas yang lebih kecil.
Basilevs
@ JimmyHoffa: Ah, begitu, Anda baru saja menghilangkannya. Saya tidak yakin saya melihat perbedaan antara ini dan pembangun, kemudian, selain ini melewati contoh konfigurasi ke dalam ctor alih-alih memanggil .build pada beberapa pembangun, dan bahwa pembangun memiliki jalur yang lebih jelas untuk pemeriksaan kebenaran pada semua data. Setiap variabel individu dapat berada dalam rentang yang valid, tetapi tidak valid dalam permutasi tertentu. .build dapat memeriksa ini, tetapi mengirimkan item ke dalam ctor memerlukan pengecekan error di dalam objek itu sendiri - icky!
Phoshi