วิธีการปรับปรุงตามรูปแบบของตัวสร้างของ Bloch เพื่อให้เหมาะสมกับการใช้งานในชั้นเรียนที่ขยายได้สูง


34

ฉันได้รับอิทธิพลอย่างมากจากหนังสือ Java ที่มีประสิทธิภาพของ Joshua Bloch (ฉบับที่ 2) ซึ่งอาจจะมากกว่าหนังสือโปรแกรมที่ฉันอ่าน โดยเฉพาะรูปแบบตัวสร้างของเขา (รายการ 2) มีผลมากที่สุด

แม้ผู้สร้างของ Bloch จะช่วยให้ฉันออกไปไกลกว่าเดิมในช่วงสองเดือนกว่าในช่วงสิบปีที่ผ่านมาของการเขียนโปรแกรม แต่ฉันก็ยังพบว่าตัวเองกำลังชนกำแพงเดียวกัน: การขยายชั้นเรียนด้วยโซ่ตรวนวิธีการกลับมาด้วยตนเอง - โดยเฉพาะอย่างยิ่งเมื่อ generics เข้ามาเล่นและโดยเฉพาะอย่างยิ่งกับgenerics อ้างอิงตนเอง (เช่นComparable<T extends Comparable<T>>)

มีความต้องการหลักสองประการที่ฉันมีอยู่เพียงข้อที่สองที่ฉันต้องการเน้นในคำถามนี้:

  1. ปัญหาแรกคือ "วิธีการแบ่งปันวิธีการส่งกลับโซ่ตนเองโดยไม่ต้องใช้พวกเขาในทุก ... เดียว ... ชั้น?" สำหรับผู้ที่อาจจะอยากรู้อยากเห็นฉันได้กล่าวถึงส่วนนี้ที่ด้านล่างของคำตอบโพสต์นี้ แต่ไม่ใช่สิ่งที่ฉันต้องการมุ่งเน้นที่นี่

  2. ปัญหาที่สองที่ฉันขอความคิดเห็นคือ "ฉันจะสร้างตัวสร้างในชั้นเรียนที่มีความตั้งใจที่จะขยายโดยชั้นเรียนอื่น ๆ อีกมากมายได้อย่างไร" การขยายชั้นเรียนด้วยตัวสร้างนั้นเป็นเรื่องยากกว่าการขยายชั้นเรียนแบบไม่มี การขยายชั้นเรียนที่มีการสร้างเป็นที่ยังดำเนินการNeedableและดังนั้นจึงมียาชื่อสามัญสำคัญที่เกี่ยวข้องกับมันเป็นเทอะทะ

นั่นคือคำถามของฉัน: ฉันจะปรับปรุง (สิ่งที่ฉันเรียกว่า) ตัวสร้าง Bloch ได้อย่างไรฉันจึงสามารถแนบตัวสร้างกับคลาสใดก็ได้ - แม้ว่าคลาสนั้นจะมีความหมายว่า "คลาสพื้นฐาน" ที่อาจเป็น ขยายและขยายออกไปหลายต่อหลายครั้ง - โดยไม่ทำให้ท้อใจตัวเองในอนาคตของฉันหรือผู้ใช้ห้องสมุดของฉันเนื่องจากสัมภาระเพิ่มเติมที่ผู้สร้าง (และ generics ที่มีศักยภาพ) กำหนดไว้กับพวกเขา?


ภาคผนวก
คำถามของฉันมุ่งเน้นไปที่ส่วนที่ 2 ด้านบน แต่ฉันต้องการที่จะอธิบายรายละเอียดเล็กน้อยเกี่ยวกับปัญหาที่เกิดขึ้นรวมถึงวิธีที่ฉันจัดการกับมัน:

ปัญหาแรกคือ "วิธีการแบ่งปันวิธีการส่งกลับโซ่ตนเองโดยไม่ต้องใช้พวกเขาในทุก ... เดียว ... ชั้น?" นี่ไม่ใช่เพื่อป้องกันไม่ให้คลาสที่ขยายใหม่ต้องนำ chain เหล่านี้กลับมาใช้ใหม่ซึ่งแน่นอนว่าพวกเขาจะต้อง - แต่จะป้องกันวิธีการที่ไม่ใช่คลาสย่อยที่ต้องการใช้ประโยชน์จากเชนของเมธอดเหล่านี้ได้อย่างไร -implement ทุกฟังก์ชั่นในตัวเองกลับมาเพื่อให้พวกเขาให้ผู้ใช้สามารถใช้ประโยชน์จากพวกเขา? สำหรับสิ่งนี้ฉันได้มาพร้อมกับการออกแบบที่ต้องการผู้ใช้ที่ฉันจะพิมพ์โครงร่างอินเตอร์เฟสสำหรับที่นี่และปล่อยไว้ที่ตอนนี้ มันทำงานได้ดีสำหรับฉัน (การออกแบบนี้เป็นเวลาหลายปีในการสร้าง ... ส่วนที่ยากที่สุดคือการหลีกเลี่ยงการพึ่งพาแบบวงกลม):

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

คำตอบ:


21

ฉันได้สร้างสิ่งที่สำหรับฉันคือการปรับปรุงใหญ่กว่ารูปแบบการสร้างของ Josh Bloch ไม่พูดในทางที่ว่า "ดีกว่า" เพียงแค่ว่าในสถานการณ์ที่เฉพาะเจาะจงมันให้ประโยชน์บางอย่าง - สิ่งที่ยิ่งใหญ่ที่สุดที่จะทำลายผู้สร้างจากระดับที่จะสร้าง

ฉันได้ทำเอกสารทางเลือกนี้อย่างละเอียดด้านล่างซึ่งฉันเรียกว่า Blind Builder Pattern


รูปแบบการออกแบบ: สร้างตาบอด

เป็นทางเลือกแทนรูปแบบการสร้างของ Joshua Bloch (รายการที่ 2 ใน Effective Java รุ่นที่ 2) ฉันได้สร้างสิ่งที่ฉันเรียกว่า "รูปแบบตัวสร้างคนตาบอด" ซึ่งแบ่งปันผลประโยชน์มากมายของ Bloch Builder และนอกเหนือจากอักขระเดียว ใช้ในวิธีเดียวกันทั้งหมด ผู้สร้างคนตาบอดมีข้อได้เปรียบของ

  • แยกตัวสร้างออกจากคลาสที่ปิดล้อมกำจัดการพึ่งพาแบบวงกลม
  • ช่วยลดขนาดของรหัสที่มาของ (สิ่งที่เป็นไม่ได้ ) ชั้นล้อมรอบและ
  • ช่วยให้ToBeBuiltระดับที่จะขยายได้โดยไม่ต้องขยายสร้างมัน

ในเอกสารฉบับนี้ผมจะหมายถึงระดับความเป็นอยู่ที่สร้างขึ้นเป็น " ToBeBuilt" ชั้น

ชั้นเรียนดำเนินการด้วย Bloch Builder

Bloch Builder เป็นสิ่งที่public static classบรรจุอยู่ภายในคลาสที่มันสร้างขึ้น ตัวอย่าง:

UserConfig ระดับสาธารณะ
   ส่วนตัว sName สตริงสุดท้าย;
   iAge int สุดท้ายส่วนตัว
   String สุดท้ายส่วนตัว sFavColor;
   public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //โอน
         ลอง {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            โยน NullPointerException ใหม่ ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // ประเมินทุกฟิลด์ที่นี่
   }
   String สาธารณะ toString () {
      ส่งคืน "name =" + sName + ", อายุ =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...START
   คลาสคงที่สาธารณะ Cfg {
      สตริง sName ส่วนตัว;
      iAge int ส่วนตัว
      String ส่วนตัว sFavColor;
      Cfg สาธารณะ (สตริง s_name) {
         sName = s_name;
      }
      // setters กลับมาเอง ... เริ่มต้น
         อายุ Cfg สาธารณะ (int i_age) {
            iAge = i_age;
            ส่งคืนสิ่งนี้
         }
         Cfg FavouriteColor (สตริง s_color) {
            sFavColor = s_color;
            ส่งคืนสิ่งนี้
         }
      // setters ที่กลับมาเอง ... END
      สร้าง UserConfig สาธารณะ () {
         กลับมา (ใหม่ UserConfig (นี้));
      }
   }
   //builder...END
}

ยกระดับชั้นเรียนด้วย Bloch Builder

UserConfig uc = ใหม่ UserConfig.Cfg ("Kermit") อายุ (50) .favoriteColor ("สีเขียว"). build ();

ชั้นเรียนเดียวกันนำไปใช้เป็นตัวสร้างคนตาบอด

Blind Builder มีสามส่วนแต่ละส่วนอยู่ในไฟล์ซอร์สโค้ดแยก:

  1. ToBeBuiltระดับ (ในตัวอย่างนี้: UserConfig)
  2. "มันFieldable" อินเตอร์เฟซ
  3. ผู้สร้าง

1. คลาสที่จะสร้าง

คลาส to-be-build ยอมรับFieldableว่าอินเตอร์เฟสเป็นพารามิเตอร์ตัวสร้างเท่านั้น ตัวสร้างกำหนดเขตข้อมูลภายในทั้งหมดจากมันและตรวจสอบแต่ละ สิ่งสำคัญที่สุดคือToBeBuiltคลาสนี้ไม่มีความรู้เกี่ยวกับผู้สร้าง

UserConfig ระดับสาธารณะ
   ส่วนตัว sName สตริงสุดท้าย;
   iAge int สุดท้ายส่วนตัว
   String สุดท้ายส่วนตัว sFavColor;
    public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //โอน
         ลอง {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            โยน NullPointerException ใหม่ ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // ประเมินทุกฟิลด์ที่นี่
   }
   String สาธารณะ toString () {
      ส่งคืน "name =" + sName + ", อายุ =" + iAge + ", sFavColor =" + sFavColor;
   }
}

ตามที่ระบุไว้โดยผู้ประกาศข่าวอัจฉริยะคนหนึ่ง (ซึ่งลบคำตอบของพวกเขาอย่างลึกลับ) ถ้าToBeBuiltชั้นเรียนดำเนินการFieldableด้วยตัวสร้างของมันเพียงหนึ่งเดียวเท่านั้นที่สามารถใช้เป็นทั้งตัวสร้างหลักและตัวคัดลอกได้ (ข้อเสียคือ เป็นที่ทราบกันว่าฟิลด์ในต้นฉบับToBeBuiltนั้นถูกต้อง)

2. Fieldableอินเทอร์เฟซ ""

อินเทอร์เฟซแบบ fieldable คือ "บริดจ์" ระหว่างToBeBuiltคลาสและตัวสร้างโดยกำหนดฟิลด์ทั้งหมดที่จำเป็นในการสร้างวัตถุ อินเทอร์เฟซนี้ต้องการโดยตัวToBeBuiltสร้างคลาสและนำไปใช้โดยตัวสร้าง เนื่องจากอินเทอร์เฟซนี้อาจถูกใช้งานโดยคลาสอื่นที่ไม่ใช่ตัวสร้างคลาสใด ๆ จึงสามารถสร้างคลาสได้อย่างง่ายดายToBeBuiltโดยไม่ต้องถูกบังคับให้ใช้ตัวสร้าง สิ่งนี้ยังช่วยให้ขยายToBeBuiltชั้นเรียนได้ง่ายขึ้นเมื่อการขยายตัวสร้างไม่เป็นที่ต้องการหรือจำเป็น

ตามที่อธิบายไว้ในส่วนด้านล่างฉันไม่บันทึกฟังก์ชั่นในส่วนต่อประสานนี้เลย

ส่วนต่อประสานสาธารณะ UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

3. ผู้สร้าง

ผู้สร้างใช้Fieldableคลาส มันไม่ผ่านการตรวจสอบ แต่อย่างใดและเพื่อเน้นความจริงข้อนี้เขตข้อมูลทั้งหมดจะเป็นแบบสาธารณะและไม่แน่นอน ในขณะที่การเข้าถึงสาธารณะไม่ได้เป็นข้อกำหนดฉันชอบและแนะนำเพราะมันบังคับใช้จริงความจริงที่ว่าการตรวจสอบจะไม่เกิดขึ้นจนกว่าToBeBuiltจะมีการเรียกตัวสร้างของ สิ่งนี้สำคัญเนื่องจากเป็นไปได้ที่เธรดอื่นจะจัดการกับตัวสร้างเพิ่มเติมก่อนที่จะถูกส่งไปยังตัวToBeBuiltสร้างของ วิธีเดียวที่จะรับประกันว่าเขตข้อมูลนั้นถูกต้อง - โดยผู้สร้างไม่สามารถ "ล็อค" สถานะได้ - สำหรับToBeBuiltชั้นเรียนที่ทำการตรวจสอบขั้นสุดท้าย

ในที่สุดเช่นเดียวกับFieldableอินเทอร์เฟซฉันไม่เอกสารใด ๆ ของ getters

UserConfig_Cfg คลาสสาธารณะใช้ UserConfig_Fieldable {
   sName สตริงสาธารณะ
   iAge int สาธารณะ
    String สาธารณะ sFavColor;
    UserConfig_Cfg สาธารณะ (String s_name) {
       sName = s_name;
    }
    // setters กลับมาเอง ... เริ่มต้น
       อายุ UserConfig_Cfg สาธารณะ (int i_age) {
          iAge = i_age;
          ส่งคืนสิ่งนี้
       }
       public UserConfig_Cfg favouriteColor (String s_color) {
          sFavColor = s_color;
          ส่งคืนสิ่งนี้
       }
    // setters ที่กลับมาเอง ... END
    //getters...START
       ประชาชน String getName () {
          ส่งคืน sName;
       }
       public int getAge () {
          ส่งคืน iAge
       }
       ประชาชน String getFavoriteColor () {
          ส่งคืน sFavColor;
       }
    //getters...END
    สร้าง UserConfig สาธารณะ () {
       กลับมา (ใหม่ UserConfig (นี้));
    }
}

ยกระดับชั้นเรียนด้วยตัวสร้างคนตาบอด

UserConfig uc = ใหม่ UserConfig_Cfg ("Kermit") อายุ (50) .favoriteColor ("สีเขียว"). build ();

ข้อแตกต่างคือ " UserConfig_Cfg" แทนที่จะเป็น " UserConfig.Cfg"

หมายเหตุ

ข้อเสีย:

  • Blind Builders ไม่สามารถเข้าถึงสมาชิกส่วนตัวของToBeBuiltชั้นเรียนได้
  • พวกเขามีความละเอียดมากขึ้นเนื่องจาก getters จำเป็นต้องใช้ทั้งในตัวสร้างและในส่วนต่อประสาน
  • ทุกอย่างสำหรับชั้นเดียวไม่ได้อยู่ในเพียงที่เดียว

การรวบรวมตัวสร้างตาบอดตรงไปข้างหน้า:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Fieldableอินเตอร์เฟซเป็นตัวเลือกทั้งหมด

สำหรับToBeBuiltคลาสที่มีฟิลด์ที่ต้องการเพียงเล็กน้อยUserConfigเช่นคลาสตัวอย่างตัวสร้างอาจเป็น

UserConfig สาธารณะ (สตริง s_name, int i_age, สตริง s_favColor) {

และเรียกในตัวสร้างด้วย

สร้าง UserConfig สาธารณะ () {
   ส่งคืน (ใหม่ UserConfig (getName (), getAge (), getFavoriteColor ())))
}

หรือแม้กระทั่งโดยการกำจัดผู้ได้รับ (ในผู้สร้าง) ทั้งหมด:

   คืนค่า (UserConfig ใหม่ (sName, iAge, sFavoriteColor));

โดยการส่งฟิลด์โดยตรงToBeBuiltคลาสจะเหมือนกับ "blind" (ไม่รู้ตัวสร้าง) เหมือนกับFieldableอินเทอร์เฟซ อย่างไรก็ตามสำหรับToBeBuiltการเรียนและซึ่งมีวัตถุประสงค์ที่จะ "ขยายและย่อยขยายหลายครั้ง" (ซึ่งอยู่ในชื่อของบทความนี้) การเปลี่ยนแปลงใด ๆใด ๆความจำเป็นข้อมูลการเปลี่ยนแปลงในทุกชั้นย่อยในทุกสร้างและToBeBuiltคอนสตรัค เมื่อจำนวนฟิลด์และคลาสย่อยเพิ่มขึ้นสิ่งนี้จะไม่สามารถทำได้

(อันที่จริงแล้วมีฟิลด์ที่จำเป็นเพียงไม่กี่อย่างการใช้ตัวสร้างเลยอาจจะเกินความจำเป็นสำหรับผู้ที่สนใจนี่คือตัวอย่างอินเทอร์เฟซ Fieldable ที่มีขนาดใหญ่กว่าในห้องสมุดส่วนตัวของฉัน)

คลาสรองในแพ็คเกจย่อย

ฉันเลือกที่จะมีผู้สร้างและFieldableชั้นเรียนสำหรับผู้สร้างตาบอดทั้งหมดในแพ็คเกจย่อยของToBeBuiltชั้นเรียนของพวกเขา แพ็คเกจย่อยจะมีชื่อว่า " z" เสมอ สิ่งนี้ป้องกันคลาสรองเหล่านี้จากการทำให้รายการแพ็กเกจ JavaDoc ยุ่งเหยิง ตัวอย่างเช่น

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

ตัวอย่างการตรวจสอบ

ดังกล่าวข้างต้นการตรวจสอบทั้งหมดเกิดขึ้นในตัวToBeBuiltสร้างของ นี่คือตัวสร้างอีกครั้งพร้อมตัวอย่างรหัสตรวจสอบ:

public UserConfig (UserConfig_Fieldable uc_f) {
   //โอน
      ลอง {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         โยน NullPointerException ใหม่ ("uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // ตรวจสอบ (ควรรวบรวมรูปแบบล่วงหน้าจริง ๆ ... )
      ลอง {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            โยน IllegalArgumentException ใหม่ ("uc_f.getName () (\" "+ sName +" \ ") อาจไม่ว่างเปล่าและต้องมีตัวเลขตัวอักษรและขีดเส้นใต้เท่านั้น");
         }
      } catch (NullPointerException rx) {
         โยน NullPointerException ใหม่ ("uc_f.getName ()");
      }
      ถ้า (iAge <0) {
         โยน IllegalArgumentException ใหม่ ("uc_f.getAge () (" + iAge + ") น้อยกว่าศูนย์");
      }
      ลอง {
         if (! Pattern.compile ("(?: red | blue | เขียว | hot pink)")) matcher (sFavColor) .matches ()) {
            โยน IllegalArgumentException ใหม่ ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") ไม่ใช่สีแดงน้ำเงินเขียวหรือชมพูร้อน");
         }
      } catch (NullPointerException rx) {
         โยน NullPointerException ใหม่ ("uc_f.getFavoriteColor ()");
      }
}

ผู้สร้างเอกสาร

ส่วนนี้ใช้ได้กับทั้งผู้สร้าง Bloch และผู้สร้างคนตาบอด มันแสดงให้เห็นถึงวิธีการที่ฉันเอกสารชั้นเรียนในการออกแบบนี้ทำให้ setters (ในตัวสร้าง) และ getters ของพวกเขา (ในToBeBuiltชั้นเรียน) โดยตรงอ้างอิงข้ามซึ่งกันและกัน - ด้วยการคลิกเมาส์เพียงครั้งเดียวและไม่มีผู้ใช้จำเป็นต้องรู้ ฟังก์ชั่นเหล่านั้นมีอยู่จริงและไม่มีนักพัฒนาที่ต้องทำเอกสารอะไรซ้ำซ้อน

Getters: ในToBeBuiltชั้นเรียนเท่านั้น

Getters มีการบันทึกไว้ในToBeBuiltชั้นเรียนเท่านั้น getters ที่เทียบเท่าทั้งใน_Fieldableและ_Cfgคลาสจะถูกละเว้น ฉันไม่ได้ทำเอกสารเลย

/ **
   <P> อายุของผู้ใช้ </P>
   @return int แสดงอายุของผู้ใช้
   @see UserConfig_Cfg # age (int)
   @see getName ()
 ** /
public int getAge () {
   ส่งคืน iAge
}

อย่างแรก@seeคือลิงค์ไปยัง setter ของมันซึ่งอยู่ในคลาส builder

Setters: ในตัวสร้างคลาส

ตัวตั้งค่ามีการบันทึกไว้เหมือนอยู่ในToBeBuiltคลาสและเหมือนกับว่ามันใช้การตรวจสอบความถูกต้อง (ซึ่งเป็นตัวToBeBuiltสร้างของจริง) เครื่องหมายดอกจัน (" *") เป็นเงื่อนงำภาพที่แสดงให้เห็นว่าเป้าหมายของการเชื่อมโยงอยู่ในชั้นเรียนอื่น

/ **
   <P> กำหนดอายุของผู้ใช้ </P>
   @param i_age ต้องไม่น้อยกว่าศูนย์ รับด้วย {@code UserConfig # getName () getName ()} *
   @see #favoriteColor (สตริง)
 ** /
อายุ UserConfig_Cfg สาธารณะ (int i_age) {
   iAge = i_age;
   ส่งคืนสิ่งนี้
}

ข้อมูลเพิ่มเติม

รวบรวมทั้งหมดเข้าด้วยกัน: แหล่งที่มาแบบเต็มของตัวอย่าง Blind Builder พร้อมเอกสารประกอบที่สมบูรณ์

UserConfig.java

นำเข้า java.util.regex.Pattern;
/ **
   <P> ข้อมูลเกี่ยวกับผู้ใช้ - <I> [ผู้สร้าง: UserConfig_Cfg] </I> </P>
   <P> การตรวจสอบความถูกต้องของฟิลด์ทั้งหมดเกิดขึ้นในตัวสร้างคลาสนี้ อย่างไรก็ตามข้อกำหนดการตรวจสอบแต่ละรายการเป็นเอกสารเฉพาะในฟังก์ชันตัวตั้งค่าของผู้สร้าง </P>
   <P> {@code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </P>
 ** /
UserConfig ระดับสาธารณะ
   โมฆะสาธารณะสุดท้ายคงที่หลัก (String [] igno_red) {
      UserConfig uc = ใหม่ UserConfig_Cfg ("Kermit") อายุ (50) .favoriteColor ("สีเขียว"). build ();
      System.out.println (UC);
   }
   ส่วนตัว sName สตริงสุดท้าย;
   iAge int สุดท้ายส่วนตัว
   String สุดท้ายส่วนตัว sFavColor;
   / **
      <P> สร้างอินสแตนซ์ใหม่ ชุดนี้และตรวจสอบความถูกต้องของทุกช่อง </P>
      @param uc_f ต้องไม่เป็น {@code null}
    ** /
   public UserConfig (UserConfig_Fieldable uc_f) {
      //โอน
         ลอง {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            โยน NullPointerException ใหม่ ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // ตรวจสอบ
         ลอง {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               โยน IllegalArgumentException ใหม่ ("uc_f.getName () (\" "+ sName +" \ ") อาจไม่ว่างเปล่าและต้องมีตัวเลขตัวอักษรและขีดเส้นใต้เท่านั้น");
            }
         } catch (NullPointerException rx) {
            โยน NullPointerException ใหม่ ("uc_f.getName ()");
         }
         ถ้า (iAge <0) {
            โยน IllegalArgumentException ใหม่ ("uc_f.getAge () (" + iAge + ") น้อยกว่าศูนย์");
         }
         ลอง {
            if (! Pattern.compile ("(?: red | blue | เขียว | hot pink)")) matcher (sFavColor) .matches ()) {
               โยน IllegalArgumentException ใหม่ ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") ไม่ใช่สีแดงน้ำเงินเขียวหรือชมพูร้อน");
            }
         } catch (NullPointerException rx) {
            โยน NullPointerException ใหม่ ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> ชื่อผู้ใช้ </P>
         @return ไม่ใช่ - {@ code null}, สตริงที่ไม่ว่างเปล่า
         @see UserConfig_Cfg # UserConfig_Cfg (สตริง)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      ประชาชน String getName () {
         ส่งคืน sName;
      }
      / **
         <P> อายุของผู้ใช้ </P>
         @return ตัวเลขที่มากกว่าหรือเท่ากับศูนย์
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      public int getAge () {
         ส่งคืน iAge
      }
      / **
         <P> สีโปรดของผู้ใช้ </P>
         @return ไม่ใช่ - {@ code null}, สตริงที่ไม่ว่างเปล่า
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      ประชาชน String getFavoriteColor () {
         ส่งคืน sFavColor;
      }
   //getters...END
   String สาธารณะ toString () {
      ส่งคืน "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> ต้องการโดย {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable) คอนสตรัคเตอร์} </P>
 ** /
ส่วนต่อประสานสาธารณะ UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

UserConfig_Cfg.java

นำเข้า java.util.regex.Pattern;
/ **
   <P> เครื่องมือสร้างสำหรับ {@link UserConfig}. </P>
   <P> การตรวจสอบความถูกต้องของฟิลด์ทั้งหมดเกิดขึ้นในตัวสร้าง <CODE> UserConfig </CODE> อย่างไรก็ตามข้อกำหนดการตรวจสอบแต่ละรายการเป็นเอกสารเฉพาะในฟังก์ชันตัวตั้งค่าคลาสนี้เท่านั้น </P>
 ** /
UserConfig_Cfg คลาสสาธารณะใช้ UserConfig_Fieldable {
   sName สตริงสาธารณะ
   iAge int สาธารณะ
   String สาธารณะ sFavColor;
   / **
      <P> สร้างอินสแตนซ์ใหม่ด้วยชื่อผู้ใช้ </P>
      @param s_name ต้องไม่เป็น {@code null} หรือว่างเปล่าและต้องมีเฉพาะตัวอักษรตัวเลขและขีดล่างเท่านั้น รับกับ {@code UserConfig # getName () getName ()} {@ รหัส ()}
    ** /
   UserConfig_Cfg สาธารณะ (String s_name) {
      sName = s_name;
   }
   // setters กลับมาเอง ... เริ่มต้น
      / **
         <P> กำหนดอายุของผู้ใช้ </P>
         @param i_age ต้องไม่น้อยกว่าศูนย์ รับกับ {@code UserConfig # getName () getName ()} {@ รหัส ()}
         @see #favoriteColor (สตริง)
       ** /
      อายุ UserConfig_Cfg สาธารณะ (int i_age) {
         iAge = i_age;
         ส่งคืนสิ่งนี้
      }
      / **
         <P> ตั้งค่าสีโปรดของผู้ใช้ </P>
         @param s_color ต้องเป็น {@code "red"}, {@code "blue"}, {@code green} หรือ {@code "hot pink"} รับด้วย {@code UserConfig # getName () getName ()} {@ code ()} *
         @see #age (int)
       ** /
      public UserConfig_Cfg favouriteColor (String s_color) {
         sFavColor = s_color;
         ส่งคืนสิ่งนี้
      }
   // setters ที่กลับมาเอง ... END
   //getters...START
      ประชาชน String getName () {
         ส่งคืน sName;
      }
      public int getAge () {
         ส่งคืน iAge
      }
      ประชาชน String getFavoriteColor () {
         ส่งคืน sFavColor;
      }
   //getters...END
   / **
      <P> สร้าง UserConfig ตามที่กำหนดค่า </P>
      @return <CODE> (ใหม่ {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (นี่)) </CODE>
    ** /
   สร้าง UserConfig สาธารณะ () {
      กลับมา (ใหม่ UserConfig (นี้));
   }
}


1
แน่นอนมันเป็นการปรับปรุง เครื่องมือสร้างของ Bloch ดังที่มีการใช้งานที่นี่มาเป็นสองชั้นเรียนที่เป็นรูปธรรมซึ่งเป็นสิ่งที่ต้องสร้างและผู้สร้าง นี้คือการออกแบบที่ไม่ดีต่อ se ตัวสร้างคนตาบอดที่คุณอธิบายถึงตัวแบ่งที่มีเพศสัมพันธ์โดยการมีคลาสที่จะสร้างกำหนดพึ่งพาการก่อสร้างเป็นสิ่งที่เป็นนามธรรมซึ่งชั้นอื่น ๆ สามารถนำมาใช้ในแบบแยกอิสระ คุณได้ประยุกต์ใช้แนวทางการออกแบบเชิงวัตถุที่สำคัญอย่างยิ่ง
rucamzu

3
คุณควรบล็อกเรื่องนี้ที่ไหนสักแห่งถ้าคุณยังไม่ได้ออกแบบขั้นตอนวิธีที่ดี! ฉันไม่แชร์เลยตอนนี้ :-)
Martijn Verburg

4
ขอบคุณสำหรับคำใจดี นี่เป็นตอนแรกที่โพสต์บนบล็อกใหม่ของฉัน: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind

หากทั้งผู้สร้างและวัตถุที่สร้างขึ้นใช้ Fieldable รูปแบบจะเริ่มคล้ายกับที่ฉันเรียกว่า ReadableFoo / MutableFoo / ImmutableFoo แม้ว่าแทนที่จะมีวิธีที่จะทำให้สิ่งที่ไม่แน่นอนเป็นสมาชิก "build" ของตัวสร้างฉัน เรียกมันasImmutableและรวมไว้ในReadableFooอินเตอร์เฟส [โดยใช้ปรัชญานั้นการเรียกbuildใช้วัตถุที่ไม่เปลี่ยนรูปนั้นจะส่งคืนการอ้างอิงไปยังวัตถุเดียวกัน]
supercat

1
@ThomasN คุณจำเป็นต้องขยาย*_Fieldableและเพิ่มตัวเชื่อมต่อใหม่และขยายตัว *_Cfgและเพิ่มตัวตั้งค่าใหม่ แต่ฉันไม่เห็นสาเหตุที่คุณจะต้องสร้างตัวเชื่อมต่อและตัวตั้งค่าที่มีอยู่ พวกมันได้รับการสืบทอดและหากไม่ต้องการฟังก์ชันการทำงานที่แตกต่างกันก็ไม่จำเป็นต้องสร้างขึ้นมาใหม่
58

13

ฉันคิดว่าคำถามที่นี่ถือว่าบางอย่างจากเริ่มแรกโดยไม่ต้องพยายามพิสูจน์ว่ารูปแบบการสร้างเป็นสิ่งที่ดีโดยเนื้อแท้

tl; dr ฉันคิดว่ารูปแบบของตัวสร้างนั้นไม่ค่อยเกิดขึ้นถ้าเป็นความคิดที่ดี


วัตถุประสงค์รูปแบบการสร้าง

วัตถุประสงค์ของรูปแบบการสร้างคือการรักษากฎสองข้อที่จะทำให้ชั้นเรียนของคุณง่ายขึ้น:

  1. วัตถุไม่ควรถูกสร้างขึ้นในสภาวะที่ไม่สอดคล้อง / ใช้ไม่ได้ / ไม่ถูกต้อง

    • นี้หมายถึงสถานการณ์ที่ตัวอย่างเช่นPersonวัตถุที่อาจถูกสร้างขึ้นโดยไม่ต้องมันIdเติมเต็มในขณะที่ชิ้นส่วนทั้งหมดของรหัสที่ใช้วัตถุที่อาจต้องทำงานเพียงเพื่อให้ถูกต้องกับIdPerson
  2. การก่อสร้างวัตถุไม่ได้ควรจะต้องมีค่ามากเกินไป

ดังนั้นจุดประสงค์ของรูปแบบตัวสร้างจึงไม่ใช่ข้อโต้แย้งที่ดี ฉันคิดว่าความต้องการและการใช้งานส่วนใหญ่นั้นขึ้นอยู่กับการวิเคราะห์ที่ผ่านมาไกลขนาดนี้แล้ว: เราต้องการกฎสองข้อนี้ให้กฎทั้งสองนี้ - แม้ว่าฉันคิดว่ามันคุ้มค่าที่จะตรวจสอบวิธีอื่น ๆ


ทำไมต้องมองหาวิธีอื่น?

ฉันคิดว่าเหตุผลแสดงให้เห็นอย่างดีจากความจริงของคำถามนี้ มีความซับซ้อนและพิธีจำนวนมากถูกเพิ่มเข้ากับโครงสร้างในการใช้รูปแบบตัวสร้างกับพวกเขา คำถามนี้ถามว่าจะแก้ไขความซับซ้อนบางอย่างได้อย่างไรเนื่องจากความซับซ้อนนั้นเกิดขึ้นสำหรับสถานการณ์ที่มีพฤติกรรมแปลก ๆ (สืบทอดมา) ความซับซ้อนนี้ยังเพิ่มค่าใช้จ่ายในการบำรุงรักษา (การเพิ่มการเปลี่ยนแปลงหรือการลบคุณสมบัตินั้นซับซ้อนกว่าอย่างอื่นมาก)


แนวทางอื่น ๆ

ดังนั้นสำหรับกฎข้อที่หนึ่งด้านบนมีวิธีอะไรบ้าง? กุญแจสำคัญที่กฎนี้อ้างถึงคือเมื่อการก่อสร้างวัตถุมีข้อมูลทั้งหมดที่จำเป็นในการทำงานอย่างถูกต้อง - และหลังจากการก่อสร้างข้อมูลนั้นไม่สามารถเปลี่ยนแปลงภายนอกได้ (ดังนั้นจึงเป็นข้อมูลที่ไม่เปลี่ยนรูป)

วิธีหนึ่งในการให้ข้อมูลที่จำเป็นทั้งหมดแก่วัตถุที่กำลังก่อสร้างคือการเพิ่มพารามิเตอร์ให้กับตัวสร้าง หากข้อมูลนั้นต้องการตัวสร้างคุณจะไม่สามารถสร้างวัตถุนี้ได้หากไม่มีข้อมูลทั้งหมดดังนั้นมันจะถูกสร้างขึ้นในสถานะที่ถูกต้อง แต่ถ้าวัตถุต้องการข้อมูลจำนวนมากให้ถูกต้อง โอ้แดงถ้าเป็นกรณีที่วิธีการนี้จะทำให้กฎ # 2 ข้างต้น

ตกลงมีอะไรอีกไหม คุณสามารถนำข้อมูลทั้งหมดที่จำเป็นสำหรับวัตถุของคุณไปอยู่ในสถานะที่สอดคล้องกันและรวมมันเข้ากับวัตถุอื่นซึ่งถูกนำมาใช้ในเวลาก่อสร้าง รหัสของคุณด้านบนแทนที่จะมีรูปแบบการสร้างจะเป็น:

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

นี้ไม่ได้เป็นเรื่องที่แตกต่างจากรูปแบบที่ดีในการสร้างถึงแม้ว่ามันจะง่ายขึ้นเล็กน้อยและที่สำคัญที่สุดที่เรากำลังสร้างความพึงพอใจกฎ # 1 และ # 2 ปกครองในขณะนี้

ดังนั้นทำไมไม่ไปเพิ่มบิตและทำให้เต็มในการสร้าง? มันไม่จำเป็นเลย ฉันพอใจกับวัตถุประสงค์ทั้งสองของรูปแบบการสร้างในแนวทางนี้โดยมีสิ่งที่ง่ายกว่าเล็กน้อยง่ายต่อการดูแลรักษาและนำกลับมาใช้ใหม่ได้ บิตสุดท้ายนั้นเป็นกุญแจสำคัญตัวอย่างที่ใช้นี้เป็นจินตภาพและไม่ให้ยืมเพื่อวัตถุประสงค์ทางความหมายในโลกแห่งความเป็นจริงดังนั้นเราจะแสดงให้เห็นว่าวิธีการนี้ส่งผลให้ DTO สามารถนำกลับมาใช้ใหม่ได้อย่างไร

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

ดังนั้นเมื่อคุณสร้างDTO ที่เหนียวแน่นเช่นนี้พวกเขาสามารถตอบสนองวัตถุประสงค์ของรูปแบบการสร้างได้ง่ายขึ้นและมีคุณค่า / ประโยชน์ที่กว้างขึ้น นอกจากนี้วิธีการนี้ยังช่วยแก้ไขความซับซ้อนในการสืบทอดซึ่งผลลัพธ์ของรูปแบบตัวสร้าง:

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

คุณอาจพบว่า DTO นั้นไม่เชื่อมโยงกันเสมอไปหรือเพื่อให้การจัดกลุ่มคุณสมบัติมีความเหนียวแน่นพวกเขาจำเป็นต้องทำลาย DTO หลาย ๆ อัน - นี่ไม่ใช่ปัญหาจริงๆ หากวัตถุของคุณต้องการคุณสมบัติ 18 อย่างและคุณสามารถสร้าง DTO 3 ตัวที่มีคุณสมบัติเหล่านั้นได้คุณจะมีโครงสร้างที่เรียบง่ายที่ตรงตามวัตถุประสงค์ของผู้สร้าง หากคุณไม่สามารถจัดกลุ่มที่เหนียวแน่นได้นี่อาจเป็นสัญญาณบ่งบอกว่าวัตถุของคุณไม่เหนียวแน่นถ้าพวกมันมีคุณสมบัติที่ไม่เกี่ยวข้องอย่างสมบูรณ์ - แต่ถึงอย่างนั้นการทำ DTO ที่ไม่เหนียวแน่นเพียงอย่างเดียวก็ยังเป็นที่นิยมมากกว่า การแก้ไขปัญหาการสืบทอดของคุณ


วิธีการปรับปรุงรูปแบบการสร้าง

ตกลงดังนั้นการเดินเล่นริมขอบทั้งหมดคุณมีปัญหาและกำลังมองหาวิธีการออกแบบเพื่อแก้ปัญหา คำแนะนำของฉัน: การสืบทอดคลาสสามารถมีคลาสที่ซ้อนอยู่ซึ่งสืบทอดจากคลาส builder ของ super class ดังนั้นคลาสที่สืบทอดจะมีโครงสร้างเดียวกันกับคลาสซูเปอร์คลาสและมีรูปแบบตัวสร้างที่ควรทำหน้าที่เดียวกันกับฟังก์ชันเพิ่มเติมอย่างแม่นยำ สำหรับคุณสมบัติเพิ่มเติมของคลาสย่อย


เมื่อมันเป็นความคิดที่ดี

คุยโวกันรูปแบบที่ผู้สร้างมีเฉพาะ เราทุกคนรู้เพราะเราทุกคนได้เรียนรู้ผู้สร้างคนนี้ในจุดหนึ่ง: StringBuilder- นี่คือจุดประสงค์ไม่ใช่การก่อสร้างที่เรียบง่ายเพราะสตริงไม่สามารถสร้างและต่อกันได้ง่ายกว่า ฯลฯ นี่คือตัวสร้างที่ยอดเยี่ยมเพราะมันมีประโยชน์ด้านประสิทธิภาพ .

ประโยชน์ที่ได้รับจากการใช้งานคือ: คุณมีวัตถุมากมายมันเป็นวัตถุที่ไม่เปลี่ยนรูปแบบคุณต้องยุบวัตถุเหล่านั้นเป็นวัตถุประเภทที่ไม่เปลี่ยนรูปแบบ หากคุณเพิ่มขึ้นเรื่อย ๆ คุณจะพบกับวัตถุตัวกลางจำนวนมากที่สร้างขึ้นดังนั้นการทำมันทั้งหมดในครั้งเดียวจะมีประสิทธิภาพและเหมาะสมกว่า

ดังนั้นผมคิดว่าสำคัญของเมื่อมันเป็นความคิดที่ดีอยู่ในโดเมนปัญหาของStringBuilder: จำเป็นต้องที่จะเปิดกรณีหลายประเภทไม่เปลี่ยนรูปเป็นเช่นเดียวของชนิดไม่เปลี่ยนรูป


ฉันไม่คิดว่าตัวอย่างที่คุณให้มาเป็นไปตามกฎทั้งสองข้อ ไม่มีสิ่งใดที่จะหยุดฉันในการสร้าง Cfg ในสถานะที่ไม่ถูกต้องและในขณะที่พารามิเตอร์ถูกย้ายออกจาก ctor พวกเขาเพิ่งถูกย้ายไปยังสถานที่ที่มีสำนวนน้อยและ verbose มากขึ้น fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()มี API แบบย่อสำหรับการสร้าง foos และสามารถเสนอการตรวจสอบข้อผิดพลาดจริงในตัวสร้าง หากไม่มีตัวสร้างวัตถุนั้นจะต้องตรวจสอบอินพุตของมันซึ่งหมายความว่าเราไม่ได้ดีไปกว่าที่เคยเป็น
Phoshi

DTO สามารถตรวจสอบคุณสมบัติของพวกเขาได้หลายวิธีด้วยคำอธิบายประกอบบนตัวตั้งค่าอย่างไรก็ตามคุณต้องการดำเนินการต่อ - การตรวจสอบความถูกต้องเป็นปัญหาที่แยกจากกันและในวิธีการสร้างของเขา ในแนวทางของฉัน อย่างไรก็ตามโดยทั่วไปแล้วจะเป็นการดีกว่าถ้าใช้ DTO เพื่อตรวจสอบความถูกต้องเนื่องจากในขณะที่ฉันแสดง - DTO สามารถใช้เพื่อสร้างหลายประเภทได้ดังนั้นเมื่อมีการตรวจสอบความถูกต้องแล้ว ตัวสร้างจะตรวจสอบความถูกต้องสำหรับประเภทเฉพาะที่สร้างขึ้นเท่านั้น
Jimmy Hoffa

บางทีวิธีที่ยืดหยุ่นที่สุดคือการมีฟังก์ชั่นการตรวจสอบความถูกต้องคงที่ในตัวสร้างซึ่งยอมรับFieldableพารามิเตอร์เดียว ฉันจะเรียกใช้ฟังก์ชั่นการตรวจสอบความถูกต้องนี้จากToBeBuiltนวกรรมิก แต่มันสามารถเรียกได้จากทุกที่ สิ่งนี้จะช่วยลดโอกาสที่จะเกิดรหัสซ้ำซ้อนโดยไม่บังคับใช้เฉพาะ (และไม่มีอะไรที่จะหยุดคุณไม่ให้ผ่านแต่ละเขตข้อมูลไปยังฟังก์ชันการตรวจสอบความถูกต้องหากคุณไม่ชอบFieldableแนวคิด - แต่ตอนนี้จะมีอย่างน้อยสามแห่งที่ต้องมีการบำรุงรักษารายชื่อเขตข้อมูล)
aliteralmind

+1 และคลาสที่มีการพึ่งพามากเกินไปในคอนสตรัคเตอร์นั้นไม่ชัดเจนพอและควรจะปรับโครงสร้างเป็นคลาสที่เล็กกว่า
Basilevs

@JimmyHoffa: อ้าฉันเข้าใจแล้วคุณแค่ละเว้นไป ฉันไม่แน่ใจว่าฉันเห็นความแตกต่างระหว่างสิ่งนี้และตัวสร้างจากนั้นสิ่งนี้จะส่งผ่านอินสแตนซ์ config เข้าไปใน ctor แทนที่จะเรียกว่า. build บนตัวสร้างบางตัวและผู้สร้างมีเส้นทางที่ชัดเจนกว่าสำหรับการตรวจสอบความถูกต้องทั้งหมด ข้อมูล. แต่ละตัวแปรอาจอยู่ในช่วงที่ถูกต้อง แต่ไม่ถูกต้องในการเปลี่ยนแปลงที่เฉพาะเจาะจง . บิวด์สามารถตรวจสอบสิ่งนี้ได้ แต่การส่งไอเท็มไปยัง ctor ต้องมีการตรวจสอบข้อผิดพลาดภายในตัววัตถุ - icky!
Phoshi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.