ทำไมเราต้องมีคลาส Builder เมื่อใช้รูปแบบตัวสร้าง?


31

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

ตัวอย่างเช่น:

class Person {

  private String name;
  private Integer age;

  public Person() {
  }

  Person withName(String name) {
    this.name = name;
    return this;
  }

  Person withAge(int age) {
    this.age = age;
    return this;
  }
}

ฉันสามารถพูดได้ Person john = new Person().withName("John");

ทำไมต้องPersonBuilderเรียน?

ประโยชน์อย่างเดียวที่ฉันเห็นคือเราสามารถประกาศPersonเขตข้อมูลได้อย่างfinalมั่นใจ


4
หมายเหตุ: ชื่อปกติสำหรับรูปแบบนี้คือchainable setters: D
Mooing Duck

4
หลักการความรับผิดชอบเดี่ยว ถ้าคุณใส่ตรรกะในคลาสบุคคลมันมีมากกว่าหนึ่งงานที่ต้องทำ
reggaeguitar

1
ผลประโยชน์ของคุณสามารถมีได้ทั้งสองทาง: ฉันสามารถwithNameคืนสำเนาของบุคคลโดยเปลี่ยนเฉพาะชื่อฟิลด์ ในคำอื่น ๆPerson john = new Person().withName("John");สามารถทำงานได้แม้ว่าPersonจะไม่เปลี่ยนรูป (และนี่คือรูปแบบทั่วไปในการเขียนโปรแกรมการทำงาน)
Brian McCutchon

9
@MooingDuck: อีกคำทั่วไปสำหรับมันเป็นอินเตอร์เฟซได้อย่างคล่องแคล่ว
Mac

2
@Mac ฉันคิดว่าส่วนต่อประสานที่คล่องแคล่วกว่าทั่วไปเล็กน้อย - คุณหลีกเลี่ยงvoidวิธีการ ดังนั้นสำหรับตัวอย่างเช่นถ้ามีวิธีที่พิมพ์ชื่อของพวกเขาคุณยังสามารถโยงกับอินเตอร์เฟซเหมือนเจ้าของภาษาPerson person.setName("Alice").sayName().setName("Bob").sayName()โดยวิธีการที่ฉันทำคำอธิบายประกอบเหล่านั้นใน JavaDoc กับข้อเสนอแนะของคุณ@return Fluent interface- มันเป็นเรื่องทั่วไปและชัดเจนพอเมื่อมันใช้กับวิธีการใด ๆ ที่ทำreturn thisในตอนท้ายของการดำเนินการและมันค่อนข้างชัดเจน ดังนั้นตัวสร้างจะทำส่วนต่อประสานที่คล่องแคล่ว
VLAZ

คำตอบ:


27

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

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

สิ่งนี้จะช่วยป้องกันไม่ให้บุคคลนั้นถูกปิดจนกว่าจะมีการตั้งค่าสถานะและเมื่อตั้งค่าแล้วจะไม่อนุญาตให้คุณเปลี่ยนแปลง แต่ทุกฟิลด์จะมีป้ายกำกับชัดเจน คุณไม่สามารถทำได้ด้วยคลาสเดียวใน Java

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

แน่นอนคุณสามารถสร้างวัตถุของคุณโดยไม่ต้องใช้คลาสอื่น แต่คุณต้องเลือก คุณสูญเสียความสามารถในการจำลองพารามิเตอร์ที่มีชื่อในภาษาที่ไม่มี (เช่น Java) หรือคุณสูญเสียความสามารถในการคงไม่เปลี่ยนรูปตลอดอายุการใช้งานของวัตถุ

ตัวอย่างที่เปลี่ยนไม่ได้ไม่มีชื่อสำหรับพารามิเตอร์

Person p = new Person("Arthur Dent", 42);

ที่นี่คุณกำลังสร้างทุกสิ่งด้วยคอนสตรัคเตอร์ตัวเดียว สิ่งนี้จะช่วยให้คุณไม่เปลี่ยนรูป แต่คุณจะสูญเสียการจำลองพารามิเตอร์ที่มีชื่อ ที่อ่านยากด้วยพารามิเตอร์จำนวนมาก คอมพิวเตอร์ไม่สนใจ แต่มันยากสำหรับมนุษย์

ตัวอย่างพารามิเตอร์แบบจำลองที่มีชื่อพร้อมตัวตั้งค่าดั้งเดิม ไม่เปลี่ยนรูป

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

ที่นี่คุณกำลังสร้างทุกสิ่งด้วย setters และกำลังจำลองพารามิเตอร์ที่มีชื่อ แต่คุณจะไม่เปลี่ยนรูปอีกต่อไป การใช้ setter แต่ละครั้งจะเปลี่ยนสถานะวัตถุ

ดังนั้นสิ่งที่คุณจะได้รับจากการเพิ่มคลาสคือคุณสามารถทำได้ทั้งคู่

การตรวจสอบสามารถทำได้ในbuild()หากข้อผิดพลาดรันไทม์สำหรับเขตข้อมูลอายุที่ขาดหายไปนั้นเพียงพอสำหรับคุณ คุณสามารถอัพเกรดและบังคับใช้age()เรียกว่ามีข้อผิดพลาดคอมไพเลอร์ ไม่เพียงแค่มีรูปแบบการสร้างของ Josh Bloch

เพื่อที่คุณจะต้องมีภาษาเฉพาะโดเมนภายใน (iDSL)

นี้ช่วยให้คุณเรียกร้องให้พวกเขาเรียกage()และก่อนที่จะเรียกname() build()แต่คุณไม่สามารถทำได้เพียงแค่กลับมาthisในแต่ละครั้ง แต่ละสิ่งที่ส่งกลับจะส่งกลับสิ่งต่าง ๆ ที่บังคับให้คุณเรียกสิ่งต่อไป

การใช้งานอาจมีลักษณะเช่นนี้:

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

แต่นี่:

Person p = personBuilder
    .age(42)
    .build()
;

เกิดข้อผิดพลาดคอมไพเลอร์เพราะจะใช้ได้เฉพาะเพื่อเรียกร้องให้ชนิดที่ส่งกลับโดยage()name()

iDSLs เหล่านี้มีประสิทธิภาพอย่างมาก ( เช่น JOOQหรือJava8 Streams ) และใช้งานได้ดีโดยเฉพาะอย่างยิ่งถ้าคุณใช้ IDE ที่มีการกรอกโค้ด แต่เป็นงานที่ค่อนข้างดีในการตั้งค่า ฉันขอแนะนำให้บันทึกไว้สำหรับสิ่งต่าง ๆ ที่จะมีรหัสแหล่งที่มาที่เป็นธรรมกับพวกเขา


3
แล้วมีคนถามว่าทำไมคุณต้องตั้งชื่อก่อนอายุและคำตอบเดียวคือ "เพราะการระเบิดแบบ combinatorial" ฉันค่อนข้างแน่ใจว่าจะสามารถหลีกเลี่ยงได้ในแหล่งที่มาหากมีเทมเพลตที่เหมาะสมเช่น C ++ หรือกลับไปใช้ประเภทที่แตกต่างกันสำหรับทุกสิ่ง
Deduplicator

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

ข้อดีอย่างหนึ่งของการเรียนที่ไม่ทราบว่ากฎการก่อสร้างของพวกเขาคือเมื่อกฎเหล่านั้นเปลี่ยนพวกเขาไม่สนใจ ฉันสามารถเพิ่มสิ่งAnonymousPersonBuilderที่มีชุดของกฎเอง
candied_orange

การออกแบบคลาส / ระบบแสดงการใช้งานที่ลึกของ "เพิ่มการติดต่อกันและลดการเชื่อมต่อให้น้อยที่สุด" การออกแบบและรันไทม์มีความหลากหลายและสามารถแก้ไขข้อบกพร่องได้หาก OO maxims และแนวทางปฏิบัติถูกนำไปใช้กับความรู้สึกว่าเป็นเศษส่วนหรือ "มันเป็นเต่าตลอดทาง"
Radarbob

1
@radarbob คลาสที่ถูกสร้างขึ้นยังรู้วิธีการสร้างตัวเอง ตัวสร้างมันมีหน้าที่รับผิดชอบในการตรวจสอบความสมบูรณ์ของวัตถุ คลาสตัวสร้างเป็นเพียงตัวช่วยสำหรับตัวสร้างเนื่องจากตัวสร้างมักใช้พารามิเตอร์จำนวนมากซึ่งไม่ได้ทำเพื่อ API ที่ดีมาก
ไม่มี

57

ทำไมต้องใช้ / จัดให้มีคลาสผู้สร้าง:

  • ในการสร้างวัตถุที่ไม่เปลี่ยนรูปแบบ - ประโยชน์ที่คุณระบุไว้แล้ว มีประโยชน์หากการก่อสร้างมีหลายขั้นตอน FWIW การเปลี่ยนแปลงไม่ได้ควรเห็นเป็นเครื่องมือสำคัญในการค้นหาของเราเพื่อเขียนโปรแกรมที่รักษาได้และปราศจากข้อบกพร่อง
  • หากการแสดงรันไทม์ของวัตถุสุดท้าย (อาจไม่เปลี่ยนรูป) จะได้รับการปรับให้เหมาะสมสำหรับการอ่านและ / หรือการใช้พื้นที่ แต่ไม่ใช่สำหรับการอัปเดต String และ StringBuilder เป็นตัวอย่างที่ดีที่นี่ สตริงที่ต่อกันซ้ำหลายครั้งไม่มีประสิทธิภาพมากดังนั้น StringBuilder จึงใช้การเป็นตัวแทนภายในที่แตกต่างกันซึ่งดีสำหรับการผนวก - แต่ไม่ดีเท่าการใช้พื้นที่และไม่ดีสำหรับการอ่านและการใช้เป็นคลาส String ปกติ
  • เพื่อแยกวัตถุที่สร้างอย่างชัดเจนออกจากวัตถุที่กำลังก่อสร้าง วิธีนี้ต้องมีการเปลี่ยนจากการก่อสร้างไปสู่การก่อสร้าง สำหรับผู้บริโภคไม่มีทางที่จะสับสนระหว่างการสร้างวัตถุกับวัตถุที่ถูกสร้าง: ระบบประเภทจะบังคับใช้สิ่งนี้ ซึ่งหมายความว่าบางครั้งเราสามารถใช้วิธีนี้เพื่อ "ตกอยู่ในหลุมแห่งความสำเร็จ" เหมือนเดิมและเมื่อใช้สิ่งที่เป็นนามธรรมสำหรับผู้อื่น (หรือตัวเราเอง) ในการใช้ (เช่น API หรือเลเยอร์) นี่อาจเป็นสิ่งที่ดีมาก สิ่ง.

4
เกี่ยวกับสัญลักษณ์แสดงหัวข้อสุดท้าย ดังนั้นความคิดที่ถูกที่PersonBuilderไม่เคยมีใคร getters และวิธีเดียวที่จะตรวจสอบค่าปัจจุบันคือการเรียกกลับ.Build() Personวิธีนี้. บิลด์สามารถตรวจสอบวัตถุที่สร้างขึ้นอย่างถูกต้อง, ถูกต้อง? เช่นนี้เป็นกลไกที่มีวัตถุประสงค์เพื่อป้องกันการใช้วัตถุ "กำลังก่อสร้าง" หรือไม่?
AaronLS

@AaronLS ใช่แน่นอน; การตรวจสอบที่ซับซ้อนสามารถนำไปใช้ในการBuildดำเนินการเพื่อป้องกันไม่ให้วัตถุที่ไม่ดีและ / หรือที่สร้างไม่ได้
Erik Eidt

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

2
@AaronLS ผู้สร้างสามารถมี getters; นั่นไม่ใช่ประเด็น. มันไม่จำเป็นต้อง แต่ถ้าผู้สร้างมี getters คุณต้องระวังว่าการโทรหาgetAgeผู้สร้างอาจกลับมาnullเมื่อคุณสมบัตินี้ยังไม่ได้ระบุ ในทางตรงกันข้ามPersonชั้นอาจบังคับให้ค่าคงที่อายุไม่เคยnullเป็นไปไม่ได้ซึ่งเป็นไปไม่ได้เมื่อผู้สร้างและวัตถุผสมกันเช่นกับnew Person() .withName("John")ตัวอย่างของ OP ซึ่งสร้างความสุขPersonโดยไม่มีอายุ สิ่งนี้ใช้ได้กับคลาสที่ไม่แน่นอนเนื่องจากตัวตั้งค่าอาจบังคับค่าคงที่ แต่ไม่สามารถบังคับใช้ค่าเริ่มต้นได้
Holger

1
@Holger คะแนนของคุณเป็นจริง แต่ส่วนใหญ่สนับสนุนอาร์กิวเมนต์ที่ Builder ควรหลีกเลี่ยง getters
user949300

21

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

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

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


7
ตัวอย่างในคำถามมีการละเมิดกฎง่ายๆ: ไม่มีการกำหนดอายุสำหรับjohn
Caleth

6
+1 และมันเป็นเรื่องยากมากที่จะทำกฎสำหรับสองฟิลด์พร้อมกันโดยไม่มีคลาสตัวสร้าง (ฟิลด์ X + Y ต้องไม่มากกว่า 10)
Reginald Blue

7
@ReginaldBl มันยิ่งยากกว่านี้ถ้าพวกมันถูก "x + y" เงื่อนไขเริ่มต้นของเราที่มี (0,0) ก็โอเคสถานะ (1,1) ก็โอเคเช่นกัน แต่ไม่มีลำดับของการอัปเดตตัวแปรเดี่ยวที่ทั้งสองรักษาสถานะที่ถูกต้องและทำให้เราจาก (0,0) ถึง (1) 1)
Michael Anderson

ฉันเชื่อว่านี่เป็นข้อได้เปรียบที่ใหญ่ที่สุด อื่น ๆ ทั้งหมดเป็นนิสัยดี
Giacomo Alzetta

5

มุมที่แตกต่างเล็กน้อยจากสิ่งที่ฉันเห็นในคำตอบอื่น ๆ

withFooวิธีการที่นี่เป็นปัญหาเพราะพวกเขาทำตัวเหมือน setters แต่จะมีการกำหนดในลักษณะที่ทำให้มันปรากฏชั้นสนับสนุนการไม่เปลี่ยนรูป ในคลาส Java ถ้าเมธอดแก้ไขคุณสมบัติมันเป็นประเพณีที่จะเริ่มเมธอดด้วย 'set' ฉันไม่เคยรักมันเป็นมาตรฐาน แต่ถ้าคุณทำอย่างอื่นมันจะทำให้คนแปลกใจและมันก็ไม่ดี มีอีกวิธีที่คุณสามารถรองรับการเปลี่ยนแปลงไม่ได้กับ API พื้นฐานที่คุณมีที่นี่ ตัวอย่างเช่น:

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

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

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


2

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

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


"[... ] สามารถยืนยันได้ว่าฟิลด์ทั้งหมดถูกตั้งค่า [... ]" จุดของรูปแบบตัวสร้างคือคุณไม่ต้องตั้งค่าฟิลด์ทั้งหมดด้วยตัวเองและคุณสามารถย้อนกลับไปเป็นค่าเริ่มต้นที่สมเหตุสมผลได้ หากคุณจำเป็นต้องตั้งค่าทุกฟิลด์คุณอาจไม่ควรใช้รูปแบบนั้น!
Vincent Savard

@VincentSavard แน่นอน แต่ในการประเมินของฉันนั่นคือการใช้งานที่พบบ่อยที่สุด เพื่อตรวจสอบว่ามีการตั้งค่าชุด "แกนกลาง" ไว้แล้วและทำสิ่งที่น่าสนใจรอบ ๆ ตัวเลือกที่ไม่จำเป็น (การตั้งค่าเริ่มต้นการรับค่าของพวกเขาจากค่าอื่น ๆ เป็นต้น)
Alexander - Reinstate Monica

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

ตัวสร้างของคลาสที่ถูกสร้างขึ้นในท้ายที่สุดรับผิดชอบต่อความถูกต้องของข้อโต้แย้ง การตรวจสอบความถูกต้องในตัวสร้างนั้นซ้ำซ้อนที่สุดและสามารถซิงค์ได้ง่ายกับความต้องการของตัวสร้าง
ไม่มี

@TKK แน่นอน แต่พวกเขาตรวจสอบสิ่งต่าง ๆ ในหลายกรณีที่ฉันใช้ Builder ในงานของผู้สร้างคือการสร้างอินพุตที่ในที่สุดได้รับการกำหนด เช่นตัวสร้างถูกกำหนดค่าด้วย a URI, a File, หรือ a FileInputStream, และใช้สิ่งใดก็ตามที่ถูกจัดเตรียมเพื่อให้ได้มาFileInputStreamซึ่งในที่สุดจะเข้าสู่การเรียก constructor เป็น arg
Alexander - Reinstate Monica

2

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


2

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

ถ้าเราไม่ใช้คลาส builder และวางเมธอดคลาส builder โดยตรงกับคลาส Person เองเราต้องสร้าง Object ก่อนแล้วจึงเรียกใช้เมธอด setter บนวัตถุที่สร้างขึ้นซึ่งจะนำไปสู่สถานะที่ไม่สอดคล้องกันของวัตถุระหว่างการสร้าง ของวัตถุและการตั้งค่าคุณสมบัติ

ดังนั้นโดยใช้คลาส builder (เช่นเอนทิตีภายนอกอื่นนอกเหนือจากคลาสบุคคล) เรารับประกันว่าวัตถุจะไม่อยู่ในสถานะไม่สอดคล้องกัน


@DawidO คุณมีจุดของฉัน จุดเน้นหลักที่ฉันพยายามวางที่นี่อยู่ในสถานะที่ไม่สอดคล้องกัน และนั่นเป็นหนึ่งในข้อได้เปรียบหลักของการใช้รูปแบบตัวสร้าง
Shubham Bondarde

2

ใช้ตัวสร้างวัตถุอีกครั้ง

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

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


1

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

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

แน่นอนนี่หมายความว่าคุณจะมีวัตถุใหม่จำนวนมากและคุณไม่ควรทำสิ่งนี้หากวัตถุของคุณมีราคาแพงในการสร้าง

ฉันใช้รหัสทดสอบในการสร้างHamcrest matchersสำหรับวัตถุทางธุรกิจของฉัน ฉันไม่จำรหัสที่แน่นอน แต่ดูเหมือนว่า (ง่าย):

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

ฉันจะใช้แบบนี้ในการทดสอบหน่วยของฉัน (ด้วยการนำเข้าคงที่ที่เหมาะสม):

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));

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

@BILLK จริง - วัตถุที่มีตัวตั้งค่าที่ไม่เปลี่ยนรูปแบบ (ที่ส่งคืนวัตถุใหม่ทุกครั้ง) หมายความว่าแต่ละวัตถุที่ส่งคืนควรถูกต้อง หากคุณส่งคืนวัตถุที่ไม่ถูกต้อง (เช่นบุคคลที่มีnameแต่ไม่มีage) แนวคิดจะไม่ทำงาน จริง ๆ แล้วคุณสามารถคืนสิ่งที่ชอบได้PartiallyBuiltPersonในขณะที่มันไม่ถูกต้อง แต่ดูเหมือนว่าแฮ็คที่จะปกปิดผู้สร้าง
VLAZ

@BillK นี่คือสิ่งที่ฉันหมายถึง"ถ้ามีวิธีการรับวัตถุเริ่มต้น (ถูกต้อง / มีประโยชน์)" - ฉันคิดว่าทั้งวัตถุเริ่มต้นและวัตถุที่ส่งคืนจากฟังก์ชั่นการสร้างแต่ละที่ถูกต้อง / สอดคล้อง งานของคุณในฐานะผู้สร้างคลาสดังกล่าวคือจัดเตรียมวิธีการสร้างเท่านั้นซึ่งทำการเปลี่ยนแปลงที่สอดคล้องกันเหล่านั้น
Paŭlo Ebermann
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.