Cách cải thiện Mô hình Trình tạo của Bloch, để làm cho nó phù hợp hơn để sử dụng trong các lớp có khả năng mở rộng cao


34

Tôi đã bị ảnh hưởng rất nhiều bởi cuốn sách Java hiệu quả của Joshua Bloch (ấn bản thứ 2), có lẽ nhiều hơn bất kỳ cuốn sách lập trình nào tôi đã đọc. Đặc biệt, Mô hình Builder của anh ấy (mục 2) đã có hiệu quả lớn nhất.

Mặc dù người xây dựng của Bloch giúp tôi đi xa hơn trong vài tháng so với mười năm lập trình trước đây, tôi vẫn thấy mình gặp phải một bức tường: Mở rộng các lớp học bằng các chuỗi phương thức tự quay trở lại là điều đáng ngại nhất, và tồi tệ nhất là cơn ác mộng - đặc biệt là khi thuốc generic phát huy tác dụng, và đặc biệt là với thuốc generic tự giới thiệu (như Comparable<T extends Comparable<T>>).

Có hai nhu cầu chính mà tôi có, chỉ có thứ hai tôi muốn tập trung vào câu hỏi này:

  1. Vấn đề đầu tiên là "làm thế nào để chia sẻ chuỗi phương thức tự trả về mà không phải thực hiện lại chúng trong mỗi ... lớp ... đơn?" Đối với những người có thể tò mò, tôi đã giải quyết phần này ở cuối bài trả lời này, nhưng đó không phải là điều tôi muốn tập trung vào đây.

  2. Vấn đề thứ hai, mà tôi đang yêu cầu bình luận, là "làm thế nào tôi có thể triển khai một trình xây dựng trong các lớp mà bản thân chúng dự định sẽ được mở rộng bởi nhiều lớp khác?" Mở rộng một lớp với một người xây dựng đương nhiên khó khăn hơn so với việc mở rộng một lớp mà không có. Mở rộng một lớp có một trình xây dựng cũng thực hiện Needable, và do đó có các tổng quát đáng kể liên quan đến nó , là khó sử dụng.

Vì vậy, đó là câu hỏi của tôi: Làm thế nào tôi có thể cải thiện (cái mà tôi gọi) Bloch Builder, vì vậy tôi có thể thoải mái gắn trình xây dựng vào bất kỳ lớp nào - ngay cả khi lớp đó có nghĩa là "lớp cơ sở" có thể là mở rộng và mở rộng thêm nhiều lần - mà không làm nản lòng tương lai của tôi, hoặc người dùng thư viện của tôi , vì hành lý bổ sung mà người xây dựng (và các thế hệ tiềm năng của nó) áp đặt cho họ?


Phụ lục
Câu hỏi của tôi tập trung vào phần 2 ở trên, nhưng tôi muốn giải thích một chút về vấn đề thứ nhất, bao gồm cả cách tôi giải quyết nó:

Vấn đề đầu tiên là "làm thế nào để chia sẻ chuỗi phương thức tự trả về mà không phải thực hiện lại chúng trong mỗi ... lớp ... đơn?" Điều này không phải để ngăn các lớp mở rộng phải thực hiện lại các chuỗi này, mà, tất nhiên, chúng phải - thay vào đó, làm thế nào để ngăn chặn các lớp không phụ , muốn tận dụng các chuỗi phương thức này, khỏi phải tái - Thực hiện mọi chức năng tự quay lại để người dùng của họ có thể tận dụng lợi thế của họ? Đối với điều này, tôi đã đưa ra một thiết kế cần thiết mà tôi sẽ in các bộ xương giao diện cho đây và để nó ở đó ngay bây giờ. Nó đã làm việc tốt với tôi (thiết kế này đã được thực hiện trong nhiều năm ... phần khó nhất là tránh phụ thuộc vòng tròn):

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);
}

Câu trả lời:


21

Tôi đã tạo ra những gì, đối với tôi, là một cải tiến lớn so với Mô hình xây dựng của Josh Bloch. Không nói theo bất kỳ cách nào là "tốt hơn", chỉ là trong một tình huống rất cụ thể , nó mang lại một số lợi thế - điều lớn nhất là nó tách người xây dựng khỏi lớp được xây dựng.

Tôi đã ghi chép kỹ lưỡng sự thay thế này bên dưới, mà tôi gọi là Mô hình Trình tạo mù.


Mẫu thiết kế: Blind Builder

Thay thế cho Mô hình Trình tạo của Joshua Bloch (mục 2 trong Java hiệu quả, phiên bản 2), tôi đã tạo ra cái mà tôi gọi là "Mô hình Trình tạo mù", chia sẻ nhiều lợi ích của Trình tạo Bloch và, ngoài một ký tự, được sử dụng chính xác theo cùng một cách. Người xây dựng mù có lợi thế về

  • tách rời trình xây dựng khỏi lớp kèm theo của nó, loại bỏ sự phụ thuộc vòng tròn,
  • làm giảm đáng kể kích thước của mã nguồn của (lớp không còn ) của lớp kèm theo và
  • cho phép ToBeBuiltlớp được mở rộng mà không cần phải mở rộng trình xây dựng của nó .

Trong tài liệu này, tôi sẽ đề cập đến lớp được xây dựng là "ToBeBuilt lớp "".

Một lớp được triển khai với Bloch Builder

Một Bloch Builder là một public static classbên trong lớp mà nó xây dựng. Một ví dụ:

lớp công khai UserConfig {
   Chuỗi cuối cùng riêng tư sName;
   chung cuộc int iAge;
   Chuỗi cuối cùng riêng tư sFavColor;
   công khai UserConfig (UserConfig.Cfg uc_c) {// XÂY DỰNG
      //chuyển khoản
         thử {
            sName = uc_c.sName;
         } Catch (NullPulumException rx) {
            ném NullPulumException mới ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // XÁC NHẬN TẤT CẢ CÁC L FINH VỰC TẠI ĐÂY
   }
   chuỗi công khai toString () {
      trả về "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   // xây dựng ...
   lớp tĩnh công cộng Cfg {
      Chuỗi riêng sName;
      int int iAge;
      Chuỗi riêng sFavColor;
      công khai Cfg (Chuỗi s_name) {
         sName = s_name;
      }
      // setters tự trở lại ... BẮT ĐẦU
         tuổi Cfg công khai (int i_age) {
            iAge = i_age;
            trả lại cái này;
         }
         công khai Cfg yêu thích Màu sắc (Chuỗi s_color) {
            sFavColor = s_color;
            trả lại cái này;
         }
      // setters tự trả về ... HẾT
      công khai UserConfig build () {
         return (UserConfig mới (cái này));
      }
   }
   // xây dựng ...
}

Khởi tạo một lớp học với Bloch Builder

UserConfig uc = new UserConfig.Cfg ("Kermit"). Tuổi (50) .favoriteColor ("xanh"). Build ();

Cùng một lớp, được triển khai như một Người xây dựng mù

Có ba phần cho Trình tạo mù, mỗi phần nằm trong một tệp mã nguồn riêng biệt:

  1. Các ToBeBuiltlớp (trong ví dụ này: UserConfig)
  2. FieldableGiao diện " " của nó
  3. Người xây dựng

1. Lớp học được xây dựng

Lớp được xây dựng chấp nhận Fieldablegiao diện của nó như là tham số hàm tạo duy nhất của nó. Hàm tạo đặt tất cả các trường bên trong từ nó và xác thực từng trường. Quan trọng nhất, ToBeBuiltlớp này không có kiến ​​thức về người xây dựng nó.

lớp công khai UserConfig {
   Chuỗi cuối cùng riêng tư sName;
   chung cuộc int iAge;
   Chuỗi cuối cùng riêng tư sFavColor;
    công khai UserConfig (UserConfig_Fieldable uc_f) {// XÂY DỰNG
      //chuyển khoản
         thử {
            sName = uc_f.getName ();
         } Catch (NullPulumException rx) {
            ném NullPulumException mới ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // XÁC NHẬN TẤT CẢ CÁC L FINH VỰC TẠI ĐÂY
   }
   chuỗi công khai toString () {
      trả về "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Theo ghi nhận của một người bình luận thông minh (người không thể giải thích được câu trả lời của họ), nếu ToBeBuiltlớp cũng thực hiện nó Fieldable, thì hàm tạo một và duy nhất của nó có thể được sử dụng làm cả hàm tạo chính sao chép của nó (một nhược điểm là các trường luôn được xác thực, mặc dù các trường luôn được xác thực người ta biết rằng các trường trong bản gốc ToBeBuiltlà hợp lệ).

2. FieldableGiao diện ""

Giao diện lĩnh vực là "cầu nối" giữa ToBeBuiltlớp và trình xây dựng của nó, xác định tất cả các trường cần thiết để xây dựng đối tượng. Giao diện này được yêu cầu bởi hàm tạo của ToBeBuiltlớp và được trình xây dựng thực hiện. Vì giao diện này có thể được thực hiện bởi các lớp khác ngoài trình xây dựng, nên bất kỳ lớp nào cũng có thể dễ dàng khởi tạo ToBeBuiltlớp mà không bị buộc phải sử dụng trình tạo của nó. Điều này cũng làm cho việc mở rộng ToBeBuiltlớp dễ dàng hơn , khi việc mở rộng trình xây dựng của nó là không mong muốn hoặc không cần thiết.

Như được mô tả trong phần dưới đây, tôi hoàn toàn không ghi lại các chức năng trong giao diện này.

giao diện công cộng UserConfig_Fieldable {
   Chuỗi getName ();
   int getAge ();
   Chuỗi getFavoriteColor ();
}

3. Người xây dựng

Người xây dựng thực hiện Fieldablelớp. Nó hoàn toàn không xác nhận, và để nhấn mạnh thực tế này, tất cả các lĩnh vực của nó là công khai và có thể thay đổi. Mặc dù khả năng truy cập công cộng này không phải là một yêu cầu, tôi thích và đề xuất nó, bởi vì nó thực thi lại thực tế là việc xác thực không xảy ra cho đến khi hàm tạo ToBeBuiltcủa được gọi. Điều này rất quan trọng, vì có thể cho một luồng khác thao tác trình xây dựng hơn nữa, trước khi nó được chuyển đến hàm tạo ToBeBuiltcủa. Cách duy nhất để đảm bảo các trường là hợp lệ - giả sử nhà xây dựng không thể "khóa" trạng thái của nó bằng cách nào đó - là cho ToBeBuiltlớp thực hiện kiểm tra cuối cùng.

Cuối cùng, như với Fieldablegiao diện, tôi không ghi lại bất kỳ getters nào của nó.

lớp công khai UserConfig_Cfg thực hiện UserConfig_Fieldable {
   chuỗi công khai sName;
   công khai iAge;
    chuỗi công khai sFavColor;
    công khai UserConfig_Cfg (Chuỗi s_name) {
       sName = s_name;
    }
    // setters tự trở lại ... BẮT ĐẦU
       tuổi UserConfig_Cfg (int i_age) {
          iAge = i_age;
          trả lại cái này;
       }
       công khai UserConfig_Cfg yêu thích Màu sắc (Chuỗi s_color) {
          sFavColor = s_color;
          trả lại cái này;
       }
    // setters tự trả về ... HẾT
    //getters...START
       chuỗi công khai getName () {
          trả về sName;
       }
       công khai int getAge () {
          trả lại iAge;
       }
       chuỗi công khai getFavoriteColor () {
          trả lại sFavColor;
       }
    //getters...END
    công khai UserConfig build () {
       return (UserConfig mới (cái này));
    }
}

Khởi tạo một lớp học với Trình tạo mù

UserConfig uc = new UserConfig_Cfg ("Kermit"). Tuổi (50) .favoriteColor ("xanh"). Build ();

Sự khác biệt duy nhất là " UserConfig_Cfg" thay vì " UserConfig.Cfg"

Ghi chú

Nhược điểm:

  • Người xây dựng mù không thể truy cập các thành viên tư nhân của ToBeBuiltlớp,
  • Chúng dài dòng hơn, vì hiện tại getters được yêu cầu trong cả trình xây dựng và trong giao diện.
  • Tất cả mọi thứ cho một lớp học không còn chỉmột nơi .

Biên dịch Blind Builder rất đơn giản:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Các Fieldablegiao diện là hoàn toàn không bắt buộc

Đối với một ToBeBuiltlớp có ít trường bắt buộc - chẳng hạn như UserConfiglớp ví dụ này , hàm tạo có thể chỉ đơn giản là

công khai UserConfig (Chuỗi s_name, int i_age, Chuỗi s_favColor) {

Và được gọi trong trình xây dựng với

công khai UserConfig build () {
   return (new UserConfig (getName (), getAge (), getFavoriteColor ()));
}

Hoặc thậm chí bằng cách loại bỏ hoàn toàn các getters (trong trình xây dựng):

   return (UserConfig mới (sName, iAge, sFavoriteColor));

Bằng cách chuyển trực tiếp các trường, ToBeBuiltlớp chỉ là "mù" (không biết về trình tạo của nó) như với Fieldablegiao diện. Tuy nhiên, đối với ToBeBuiltcác lớp được dự định là "mở rộng và mở rộng phụ nhiều lần" (nằm trong tiêu đề của bài đăng này), bất kỳ thay đổi nào đối với bất kỳ trường nào đều yêu cầu thay đổi trong mọi lớp con, trong mọi trình xây dựng và hàm ToBeBuilttạo. Khi số lượng các trường và các lớp con tăng lên, điều này trở nên không thực tế để duy trì.

(Thật vậy, với một vài trường cần thiết, việc sử dụng một trình xây dựng hoàn toàn có thể là quá mức cần thiết. Đối với những người quan tâm, đây là một mẫu của một số giao diện Fieldable lớn hơn trong thư viện cá nhân của tôi.)

Các lớp thứ cấp trong gói phụ

Tôi chọn để có tất cả các trình xây dựng và các Fieldablelớp, cho tất cả các Nhà xây dựng mù, trong một gói phụ của ToBeBuiltlớp của họ . Gói phụ luôn được đặt tên " z". Điều này ngăn các lớp thứ cấp này làm lộn xộn danh sách gói JavaDoc. Ví dụ

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

Ví dụ xác nhận

Như đã đề cập ở trên, tất cả các xác nhận xảy ra trong hàm tạo ToBeBuiltcủa. Đây là hàm tạo một lần nữa với mã xác thực ví dụ:

công khai UserConfig (UserConfig_Fieldable uc_f) {
   //chuyển khoản
      thử {
         sName = uc_f.getName ();
      } Catch (NullPulumException rx) {
         ném NullPulumException mới ("uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // xác thực (nên thực sự biên dịch trước các mẫu ...)
      thử {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            ném IllegalArgumentException mới ("uc_f.getName () (\" "+ sName +" \ ") có thể không trống và chỉ chứa các chữ số và dấu gạch dưới.");
         }
      } Catch (NullPulumException rx) {
         ném NullPulumException mới ("uc_f.getName ()");
      }
      if (iAge <0) {
         ném IllegalArgumentException mới ("uc_f.getAge () (" + iAge + ") nhỏ hơn không.");
      }
      thử {
         if (! Pattern.compile ("(?: đỏ | xanh | xanh | hồng nóng)"). bộ so khớp (sFavColor) .matches ()) {
            ném IllegalArgumentException mới ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") không có màu đỏ, xanh dương, xanh lục hoặc hồng nóng.");
         }
      } Catch (NullPulumException rx) {
         ném NullPulumException mới ("uc_f.getFavoriteColor ()");
      }
}

Xây dựng tài liệu

Phần này được áp dụng cho cả Nhà xây dựng Bloch và Nhà xây dựng mù. Nó cho thấy cách tôi ghi lại các lớp trong thiết kế này, tạo setters (trong trình xây dựng) và getters của chúng (trong ToBeBuiltlớp) được tham chiếu chéo trực tiếp với nhau - chỉ bằng một cú nhấp chuột và không cần người dùng biết nơi những chức năng đó thực sự nằm trong - và không có nhà phát triển phải ghi lại bất cứ điều gì dư thừa.

Getters: Chỉ trong ToBeBuiltcác lớp học

Getters chỉ được ghi lại trong ToBeBuiltlớp. Các getters tương đương cả trong _Fieldable_Cfg các lớp được bỏ qua. Tôi không làm tài liệu cho họ cả.

/ **
   <P> Tuổi của người dùng. </ P>
   @return Một int đại diện cho tuổi của người dùng.
   @see UserConfig_Cfg # age (int)
   @see getName ()
 ** /
công khai int getAge () {
   trả lại iAge;
}

Đầu tiên @seelà một liên kết đến setter của nó, nằm trong lớp trình xây dựng.

Setters: Trong lớp xây dựng

Trình thiết lập được ghi lại như thể nó đang ở trong ToBeBuiltlớp và cũng như thể thực hiện xác nhận (điều này thực sự được thực hiện bởi hàm tạo ToBeBuiltcủa). Dấu hoa thị (" *") là một đầu mối trực quan cho biết mục tiêu của liên kết nằm trong một lớp khác.

/ **
   <P> Đặt tuổi của người dùng. </ P>
   @param i_age Có thể không nhỏ hơn 0. Nhận với {@code UserConfig # getName () getName ()} *.
   @see #favoriteColor (Chuỗi)
 ** /
tuổi UserConfig_Cfg (int i_age) {
   iAge = i_age;
   trả lại cái này;
}

Thêm thông tin

Kết hợp tất cả lại với nhau: Nguồn đầy đủ của ví dụ Blind Builder, với tài liệu đầy đủ

UserConfig.java

nhập java.util.regex.Potype;
/ **
   <P> Thông tin về người dùng - <I> [người xây dựng: UserConfig_Cfg] </ I> </ P>
   <P> Xác thực tất cả các trường xảy ra trong hàm tạo của lớp này. Tuy nhiên, mỗi yêu cầu xác thực chỉ là tài liệu trong các hàm setter của trình tạo. </ P>
   <P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
 ** /
lớp công khai UserConfig {
   chung tĩnh static void main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ("Kermit"). Tuổi (50) .favoriteColor ("xanh"). Build ();
      System.out.println (uc);
   }
   Chuỗi cuối cùng riêng tư sName;
   chung cuộc int iAge;
   Chuỗi cuối cùng riêng tư sFavColor;
   / **
      <P> Tạo một ví dụ mới. Điều này đặt và xác thực tất cả các trường. </ P>
      @param uc_f Có thể không phải là {@code null}.
    ** /
   công khai UserConfig (UserConfig_Fieldable uc_f) {
      //chuyển khoản
         thử {
            sName = uc_f.getName ();
         } Catch (NullPulumException rx) {
            ném NullPulumException mới ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // xác thực
         thử {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               ném IllegalArgumentException mới ("uc_f.getName () (\" "+ sName +" \ ") có thể không trống và chỉ chứa các chữ số và dấu gạch dưới.");
            }
         } Catch (NullPulumException rx) {
            ném NullPulumException mới ("uc_f.getName ()");
         }
         if (iAge <0) {
            ném IllegalArgumentException mới ("uc_f.getAge () (" + iAge + ") nhỏ hơn không.");
         }
         thử {
            if (! Pattern.compile ("(?: đỏ | xanh | xanh | hồng nóng)"). bộ so khớp (sFavColor) .matches ()) {
               ném IllegalArgumentException mới ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") không có màu đỏ, xanh dương, xanh lục hoặc hồng nóng.");
            }
         } Catch (NullPulumException rx) {
            ném NullPulumException mới ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> Tên người dùng. </ P>
         @return Một chuỗi không - {@ code null}, chuỗi không trống.
         @see UserConfig_Cfg # UserConfig_Cfg (Chuỗi)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      chuỗi công khai getName () {
         trả về sName;
      }
      / **
         <P> Tuổi của người dùng. </ P>
         @return Một số lớn hơn-hoặc-bằng-không.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      công khai int getAge () {
         trả lại iAge;
      }
      / **
         <P> Màu sắc yêu thích của người dùng. </ P>
         @return Một chuỗi không - {@ code null}, chuỗi không trống.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      chuỗi công khai getFavoriteColor () {
         trả lại sFavColor;
      }
   //getters...END
   chuỗi công khai toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Được yêu cầu bởi {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </ P>
 ** /
giao diện công cộng UserConfig_Fieldable {
   Chuỗi getName ();
   int getAge ();
   Chuỗi getFavoriteColor ();
}

UserConfig_Cfg.java

nhập java.util.regex.Potype;
/ **
   Trình tạo <P> cho {@link UserConfig}. </ P>
   <P> Xác thực tất cả các trường xảy ra trong hàm tạo <CODE> UserConfig </ CODE>. Tuy nhiên, mỗi yêu cầu xác thực chỉ là tài liệu trong các hàm setter lớp này. </ P>
 ** /
lớp công khai UserConfig_Cfg thực hiện UserConfig_Fieldable {
   chuỗi công khai sName;
   công khai iAge;
   chuỗi công khai sFavColor;
   / **
      <P> Tạo một phiên bản mới với tên người dùng. </ P>
      @param s_name Không được là {@code null} hoặc trống và chỉ chứa các chữ cái, chữ số và dấu gạch dưới. Nhận với {@code UserConfig # getName () getName ()} {@ code ()} .
    ** /
   công khai UserConfig_Cfg (Chuỗi s_name) {
      sName = s_name;
   }
   // setters tự trở lại ... BẮT ĐẦU
      / **
         <P> Đặt tuổi của người dùng. </ P>
         @param i_age Có thể không nhỏ hơn 0. Nhận với {@code UserConfig # getName () getName ()} {@ code ()} .
         @see #favoriteColor (Chuỗi)
       ** /
      tuổi UserConfig_Cfg (int i_age) {
         iAge = i_age;
         trả lại cái này;
      }
      / **
         <P> Đặt màu yêu thích của người dùng. </ P>
         @param s_color Phải là {@code "đỏ"}, {@code "xanh"}, {@code xanh} hoặc {@code "hồng nóng"}. Nhận với {@code UserConfig # getName () getName ()} {@ code ()} *.
         @see #age (int)
       ** /
      công khai UserConfig_Cfg yêu thích Màu sắc (Chuỗi s_color) {
         sFavColor = s_color;
         trả lại cái này;
      }
   // setters tự trả về ... HẾT
   //getters...START
      chuỗi công khai getName () {
         trả về sName;
      }
      công khai int getAge () {
         trả lại iAge;
      }
      chuỗi công khai getFavoriteColor () {
         trả lại sFavColor;
      }
   //getters...END
   / **
      <P> Xây dựng UserConfig, như được định cấu hình. </ P>
      @return <CODE> (mới {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (này)) </ CODE>
    ** /
   công khai UserConfig build () {
      return (UserConfig mới (cái này));
   }
}


1
Chắc chắn, đó là một cải tiến. Bloch's Builder, như được triển khai ở đây, kết hợp hai lớp cụ thể , đây là lớp được xây dựng và người xây dựng nó. Đây là thiết kế xấu mỗi se . Trình tạo mù mà bạn mô tả phá vỡ khớp nối đó bằng cách có lớp được xây dựng xác định sự phụ thuộc xây dựng của nó là một sự trừu tượng hóa , mà các lớp khác có thể thực hiện theo kiểu tách rời. Bạn đã áp dụng rất nhiều những gì một hướng dẫn thiết kế hướng đối tượng thiết yếu.
rucamzu

3
Bạn thực sự nên viết blog về điều này ở đâu đó nếu bạn chưa có, thiết kế thuật toán đẹp! Tôi đang chia sẻ nó ngay bây giờ :-).
Martijn Verburg

4
Cảm ơn bạn cho các từ loại. Đây là bài viết đầu tiên trên blog mới của tôi: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind 14/214

Nếu cả hai công cụ xây dựng và xây dựng đều triển khai Fieldable, thì mẫu bắt đầu giống với mẫu mà tôi đã gọi là ReadableFoo / MutableFoo / ImmutableFoo, mặc dù thay vì có phương thức để tạo ra một thứ có thể thay đổi là thành viên "xây dựng" của trình xây dựng, tôi gọi nó asImmutablevà đưa nó vào ReadableFoogiao diện [sử dụng triết lý đó, gọi buildmột đối tượng bất biến sẽ chỉ đơn giản trả về một tham chiếu đến cùng một đối tượng].
supercat

1
@ThomasN Bạn cần mở rộng *_Fieldablevà thêm getters mới vào nó, và mở rộng *_Cfgvà thêm setters mới vào nó, nhưng tôi không hiểu tại sao bạn cần phải sao chép getters và setters hiện có. Chúng được kế thừa và trừ khi chúng cần các chức năng khác nhau, không cần phải tạo lại chúng.
aliteralmind

13

Tôi nghĩ rằng câu hỏi ở đây giả định một cái gì đó ngay từ đầu mà không cố gắng chứng minh nó, rằng mô hình xây dựng vốn đã tốt.

tl; dr Tôi nghĩ rằng mô hình xây dựng hiếm khi là một ý tưởng tốt.


Mục đích xây dựng mô hình

Mục đích của mẫu trình xây dựng là duy trì hai quy tắc sẽ giúp việc tiêu thụ lớp của bạn dễ dàng hơn:

  1. Các đối tượng không thể được xây dựng ở trạng thái không nhất quán / không sử dụng / không hợp lệ.

    • Điều này nói đến kịch bản mà ví dụ một Personđối tượng có thể được xây dựng mà không cần phải nó Idđiền vào, trong khi tất cả các phần của mã mà sử dụng mà đối tượng có thể yêu cầu các Idcông việc chỉ để đúng với Person.
  2. Các hàm tạo đối tượng không nên yêu cầu quá nhiều tham số .

Vì vậy, mục đích của mô hình xây dựng là không gây tranh cãi. Tôi nghĩ phần lớn mong muốn và cách sử dụng của nó dựa trên phân tích đã đi xa về cơ bản: Chúng tôi muốn hai quy tắc này, điều này đưa ra hai quy tắc này - mặc dù tôi nghĩ rằng đáng để nghiên cứu các cách khác để thực hiện hai quy tắc đó.


Tại sao phải nhìn vào các phương pháp khác?

Tôi nghĩ rằng lý do được thể hiện tốt bởi chính thực tế của câu hỏi này; có sự phức tạp và rất nhiều nghi lễ được thêm vào các cấu trúc trong việc áp dụng mô hình xây dựng cho chúng. Câu hỏi này là hỏi làm thế nào để giải quyết một số sự phức tạp đó bởi vì sự phức tạp của nó, nó tạo ra một kịch bản hành xử kỳ lạ (kế thừa). Sự phức tạp này cũng làm tăng chi phí bảo trì (thêm, thay đổi hoặc loại bỏ các thuộc tính phức tạp hơn nhiều so với cách khác).


Các cách tiếp cận khác

Vậy đối với quy tắc số một ở trên, có những cách tiếp cận nào? Chìa khóa mà quy tắc này đề cập đến là khi xây dựng, một đối tượng có tất cả thông tin cần thiết để hoạt động đúng - và sau khi xây dựng, thông tin đó không thể thay đổi bên ngoài (vì vậy đó là thông tin bất biến).

Một cách để cung cấp tất cả thông tin cần thiết cho một đối tượng khi xây dựng chỉ đơn giản là thêm các tham số cho hàm tạo. Nếu thông tin đó được yêu cầu bởi nhà xây dựng, bạn sẽ không thể xây dựng đối tượng này mà không có tất cả thông tin đó, do đó nó sẽ được xây dựng thành trạng thái hợp lệ. Nhưng nếu đối tượng đòi hỏi nhiều thông tin để hợp lệ thì sao? Oh dang, nếu đó là cách tiếp cận này sẽ phá vỡ quy tắc # 2 ở trên .

Ok còn gì nữa không? Chà, bạn có thể chỉ cần lấy tất cả thông tin cần thiết cho đối tượng của bạn ở trạng thái nhất quán và đưa nó vào một đối tượng khác được thực hiện khi xây dựng. Mã của bạn ở trên thay vì có mẫu xây dựng thì sẽ là:

//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;
   }
}

Đây không phải là một sự khác biệt lớn so với mẫu xây dựng, mặc dù nó hơi đơn giản và quan trọng nhất là chúng tôi đang đáp ứng quy tắc số 1 và quy tắc số 2 bây giờ .

Vậy tại sao không đi thêm một chút và làm cho nó đầy đủ trên trình xây dựng? Nó đơn giản là không cần thiết . Tôi hài lòng cả hai mục đích của mẫu xây dựng theo cách tiếp cận này, với một cái gì đó đơn giản hơn một chút, dễ bảo trì hơn và có thể tái sử dụng . Bit cuối cùng đó là chìa khóa, ví dụ này được sử dụng là tưởng tượng và không cho vay vào mục đích ngữ nghĩa trong thế giới thực, vì vậy hãy cho thấy cách tiếp cận này dẫn đến một DTO có thể tái sử dụng thay vì một lớp mục đích duy nhất .

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) {
      ...
   }
}

Vì vậy, khi bạn xây dựng các DTO gắn kết như thế này, cả hai đều có thể đáp ứng mục đích của mẫu xây dựng, đơn giản hơn và với giá trị / tính hữu dụng rộng hơn. Hơn nữa, cách tiếp cận này giải quyết sự phức tạp kế thừa mà mẫu xây dựng dẫn đến:

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);
      ...
   }
}

Bạn có thể thấy DTO không phải lúc nào cũng gắn kết hoặc để làm cho các nhóm thuộc tính gắn kết với nhau, chúng cần được chia thành nhiều DTO - đây thực sự không phải là vấn đề. Nếu đối tượng của bạn yêu cầu 18 thuộc tính và bạn có thể tạo 3 DTO gắn kết với các thuộc tính đó, bạn đã có một cấu trúc đơn giản đáp ứng các mục đích của người xây dựng, sau đó một số. Nếu bạn không thể đưa ra các nhóm gắn kết, đây có thể là dấu hiệu các đối tượng của bạn không gắn kết nếu chúng có các thuộc tính hoàn toàn không liên quan - nhưng ngay cả khi đó, việc tạo một DTO không gắn kết vẫn được ưa thích hơn do việc thực hiện đơn giản hơn giải quyết vấn đề thừa kế của bạn.


Làm thế nào để cải thiện mô hình xây dựng

Ok vì vậy tất cả các vụ lùm xùm sang một bên, bạn có một vấn đề và đang tìm kiếm một phương pháp thiết kế để giải quyết nó. Đề xuất của tôi: các lớp kế thừa đơn giản có thể có một lớp lồng nhau kế thừa từ lớp trình xây dựng của siêu lớp, vì vậy lớp kế thừa về cơ bản có cấu trúc giống như siêu lớp và có một mẫu trình xây dựng có chức năng chính xác giống với các hàm bổ sung cho các thuộc tính bổ sung của lớp con ..


Khi đó là một ý tưởng tốt

Nghiêng sang một bên, mô hình xây dựng có một thích hợp . Tất cả chúng ta đều biết điều đó bởi vì tất cả chúng ta đã học được trình xây dựng cụ thể này ở điểm này hay điểm khác: StringBuilder- ở đây mục đích không phải là xây dựng đơn giản, bởi vì các chuỗi không thể dễ dàng hơn để xây dựng và nối, v.v ... Đây là một trình xây dựng tuyệt vời vì nó có lợi ích hiệu suất .

Do đó, lợi ích về hiệu suất là: Bạn có một loạt các đối tượng, chúng thuộc loại không thay đổi, bạn cần thu gọn chúng xuống một đối tượng thuộc loại không thay đổi. Nếu bạn làm điều đó dần dần bạn sẽ ở đây nhiều đối tượng trung gian được tạo ra, do đó, làm tất cả cùng một lúc sẽ hiệu quả và lý tưởng hơn nhiều.

Vì vậy, tôi nghĩ rằng chìa khóa khi nó là một ý tưởng tốt nằm trong miền vấn đề của StringBuilder: Cần biến nhiều trường hợp của các loại bất biến thành một trường hợp duy nhất của một loại bất biến .


Tôi không nghĩ rằng ví dụ đưa ra của bạn thỏa mãn một trong hai quy tắc. Không có gì ngăn cản tôi tạo Cfg ở trạng thái không hợp lệ và trong khi các tham số đã được chuyển ra khỏi ctor, chúng vừa được chuyển đến một nơi ít thành ngữ và dài dòng hơn. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()cung cấp một API ngắn gọn để xây dựng foos và có thể cung cấp kiểm tra lỗi thực tế trong chính trình xây dựng. Không có trình xây dựng, chính đối tượng phải kiểm tra các đầu vào của nó, điều đó có nghĩa là chúng ta không khá hơn so với trước đây.
Phoshi

Các DTO có thể xác thực các thuộc tính của chúng theo nhiều cách khai báo với các chú thích, trên trình cài đặt, tuy nhiên bạn muốn giải quyết nó - xác thực là một vấn đề riêng biệt và theo cách tiếp cận của người xây dựng, anh ta cho thấy việc xác thực xảy ra trong hàm tạo, logic tương tự sẽ hoàn toàn phù hợp theo cách tiếp cận của tôi. Tuy nhiên, nói chung sẽ tốt hơn khi sử dụng DTO để xác thực nó vì như tôi chỉ ra - DTO có thể được sử dụng để xây dựng nhiều loại và do đó, việc xác thực trên nó sẽ cho vay để xác thực nhiều loại. Trình xây dựng chỉ xác nhận cho một loại cụ thể mà nó được tạo cho.
Jimmy Hoffa

Có lẽ cách linh hoạt nhất là có một chức năng xác nhận tĩnh trong trình xây dựng, chấp nhận một Fieldabletham số duy nhất . tôi sẽ gọi hàm xác nhận này từ hàm ToBeBuilttạo, nhưng nó có thể được gọi bởi bất cứ thứ gì, từ bất cứ đâu. Điều này giúp loại bỏ tiềm năng cho mã dự phòng, mà không buộc phải thực hiện cụ thể. (Và không có gì ngăn bạn chuyển các trường riêng lẻ sang chức năng xác thực, nếu bạn không thích Fieldablekhái niệm này - nhưng bây giờ sẽ có ít nhất ba vị trí trong đó danh sách trường sẽ phải được duy trì.)
aliteralmind

+1 Và một lớp có quá nhiều phụ thuộc trong hàm tạo của nó rõ ràng là không đủ gắn kết và nên được tái cấu trúc thành các lớp nhỏ hơn.
Basilevs

@JimmyHoffa: Ah, tôi hiểu rồi, bạn vừa bỏ qua điều đó. Tôi không chắc chắn tôi thấy sự khác biệt giữa cái này và một cái xây dựng, sau đó, ngoài cái này chuyển một cá thể cấu hình vào ctor thay vì gọi .build trên một số builder, và một builder có một đường dẫn rõ ràng hơn để kiểm tra chính xác dữ liệu. Mỗi biến riêng lẻ có thể nằm trong phạm vi hợp lệ của nó, nhưng không hợp lệ trong hoán vị cụ thể đó. .build có thể kiểm tra điều này, nhưng việc chuyển mục vào ctor yêu cầu kiểm tra lỗi bên trong chính đối tượng - icky!
Phoshi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.