รูปแบบของตัวสร้าง: เมื่อใดจะล้มเหลว


45

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

ก่อนอื่นให้อธิบาย:

  • ด้วยความล้มเหลวในช่วงต้นผมหมายถึงว่าการสร้างวัตถุควรจะล้มเหลวทันทีเป็นพารามิเตอร์ที่ไม่ถูกต้องผ่านใน. SomeObjectBuilderดังนั้นภายใน
  • ด้วยความล้มเหลวในช่วงปลายฉันหมายความว่าการสร้างวัตถุสามารถล้มเหลวในการbuild()โทรเท่านั้น

จากนั้นข้อโต้แย้งบางอย่าง:

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

ข้อสรุปทั่วไปเกี่ยวกับเรื่องนี้คืออะไร?


8
ฉันไม่เห็นประโยชน์ใด ๆ ที่จะล้มเหลวในช่วงปลายปี สิ่งที่บางคนกล่าวว่าคลาสผู้สร้าง "ควร" ไม่ได้มีความสำคัญเหนือกว่าการออกแบบที่ดีและการจับข้อบกพร่องในช่วงต้นนั้นดีกว่าการจับข้อบกพร่องในสายเสมอ
Doval

3
อีกวิธีในการดูนี้คือผู้สร้างอาจไม่ทราบว่ามีข้อมูลอะไรที่ถูกต้อง การล้มเหลว แต่เนิ่นๆในกรณีนี้คือการล้มเหลวมากขึ้นเมื่อคุณรู้ว่ามีข้อผิดพลาด ไม่ได้ล้มเหลวในช่วงต้นจะเป็นผู้สร้างกลับวัตถุเมื่อมีปัญหาในการnull build()
Chris

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

คำตอบ:


34

ลองดูตัวเลือกที่เราสามารถวางรหัสการตรวจสอบ:

  1. ภายใน setters ในผู้สร้าง
  2. ภายในbuild()วิธีการ
  3. ภายในเอนทิตีที่สร้างขึ้น: มันจะถูกเรียกใช้ในbuild()วิธีการเมื่อมีการสร้างเอนทิตี

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

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

จากมุมมองของ SOLID การวางการตรวจสอบความถูกต้องในตัวสร้างยังเป็นการละเมิด SRP: คลาสตัวสร้างมีความรับผิดชอบในการรวบรวมข้อมูลเพื่อสร้างวัตถุ การตรวจสอบกำลังสร้างสัญญาในสถานะภายในของตัวเองมันเป็นความรับผิดชอบใหม่ในการตรวจสอบสถานะของวัตถุอื่น

ดังนั้นจากมุมมองของฉันไม่เพียง แต่จะดีกว่าที่จะล้มเหลวจากมุมมองการออกแบบ แต่ยังดีกว่าที่จะล้มเหลวในเอนทิตีที่สร้างขึ้นแทนที่จะสร้างตัวมันเอง

UPD: ความคิดเห็นนี้ทำให้ฉันนึกถึงความเป็นไปได้อีกหนึ่งเมื่อการตรวจสอบภายในตัวสร้าง (ตัวเลือก 1 หรือ 2) สมเหตุสมผล มันสมเหตุสมผลถ้าผู้สร้างมีสัญญาของตัวเองในวัตถุที่กำลังสร้าง ตัวอย่างเช่นสมมติว่าเรามีการสร้างที่สร้างสตริงที่มีเนื้อหาเฉพาะการพูด, 1-2,3-4,5-6รายการของช่วงตัวเลข addRange(int min, int max)สร้างนี้อาจมีวิธีการเช่น สตริงผลลัพธ์ไม่ทราบอะไรเกี่ยวกับตัวเลขเหล่านี้ไม่ควรรู้ ผู้สร้างเองกำหนดรูปแบบของสตริงและข้อ จำกัด เกี่ยวกับตัวเลข ดังนั้นวิธีการaddRange(int,int)จะต้องตรวจสอบหมายเลขอินพุตและส่งข้อยกเว้นถ้าค่าสูงสุดน้อยกว่าขั้นต่ำ

ที่กล่าวว่ากฎทั่วไปจะตรวจสอบเฉพาะสัญญาที่กำหนดโดยผู้สร้างเอง


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

หากผู้สร้าง URI ส่งข้อยกเว้นหากมีการส่งสตริงที่ว่างเปล่านี่เป็นการละเมิด SOLID หรือไม่ ขยะ
Gusdor

@Gusdor ใช่ถ้ามันมีข้อผิดพลาดตัวเอง อย่างไรก็ตามจากมุมมองของผู้ใช้ตัวเลือกทั้งหมดดูเหมือนว่าข้อยกเว้นจะถูกโยนโดยผู้สร้าง
Ivan Gammel

เหตุใดจึงไม่มีการตรวจสอบ () ที่ถูกเรียกโดย build () วิธีนี้จะมีการทำซ้ำเล็กน้อยความสอดคล้องและไม่มีการละเมิด SRP นอกจากนี้ยังทำให้สามารถตรวจสอบข้อมูลได้โดยไม่ต้องพยายามสร้างและการตรวจสอบความถูกต้องใกล้กับการสร้าง
StellarVortex

@StellarVortex ในกรณีนี้มันจะถูกตรวจสอบสองครั้ง - หนึ่งครั้งใน builder.build () และถ้าข้อมูลนั้นถูกต้องและเราดำเนินการสร้างนวกรรมิกของวัตถุในนวกรรมิกนั้น
Ivan Gammel

34

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

เช่นเดียวกับคอนสตรัคเตอร์ผู้สร้างสามารถกำหนดค่าคงที่ของพารามิเตอร์ วิธีการสร้างสามารถตรวจสอบค่าคงที่เหล่านี้ จำเป็นอย่างยิ่งที่จะต้องตรวจสอบหลังจากคัดลอกพารามิเตอร์จากตัวสร้างไปยังวัตถุและต้องถูกตรวจสอบในฟิลด์วัตถุแทนฟิลด์ตัวสร้าง (รายการ 39) หากผู้รุกรานใด ๆ ถูกละเมิดวิธีการสร้างควรโยนIllegalStateException(รายการ 60) วิธีรายละเอียดของข้อยกเว้นควรระบุว่ามีการละเมิดค่าใด (ข้อ 63)

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

หมายเหตุตามคำอธิบายการแก้ไขเกี่ยวกับบทความนี้ "รายการ" ในใบเสนอราคาดังกล่าวข้างต้นหมายถึงกฎระเบียบที่นำเสนอในที่มีประสิทธิภาพ Java, Second Edition

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

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

พิจารณาว่ารถสปอร์ตไม่สามารถมีมากกว่า 2 ที่นั่งได้ใครจะรู้ได้ว่าsetSeats(4)ดีหรือไม่? มันอยู่ที่การสร้างเมื่อใครสามารถรู้ได้อย่างแน่นอนว่าsetSportsCar()ถูกเรียกหรือไม่ความหมายว่าจะโยนTooManySeatsExceptionหรือไม่


3
+1 สำหรับการแนะนำประเภทของข้อยกเว้นที่จะโยนสิ่งที่ฉันกำลังค้นหา
Xantix

ไม่แน่ใจว่าฉันได้รับทางเลือก ดูเหมือนว่าจะเป็นการพูดอย่างแท้จริงว่าเมื่อค่าคงที่สามารถตรวจสอบได้ในกลุ่มเท่านั้น ผู้สร้างยอมรับคุณสมบัติเดียวเมื่อพวกเขาไม่เกี่ยวข้องใด ๆ และยอมรับเฉพาะกลุ่มของแอตทริบิวต์เมื่อกลุ่มมีค่าคงที่ในตัวเอง ในกรณีนี้แอตทริบิวต์เดียวควรส่งข้อยกเว้นก่อนการสร้างหรือไม่
Didier A.

19

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

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

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

อย่างไรก็ตามนี่เป็นวิธีปฏิบัติที่ดีที่สุดมากกว่าสิ่งใด ฉันหวังว่าจะตอบคำถามของคุณ


11

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

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

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

  • ต้องการให้แอตทริบิวต์ทั้งสอง (หรือมากกว่า) พร้อมกัน (เช่นการเรียกใช้วิธีการเดียว)
  • ทดสอบความถูกต้องทันทีที่คุณรู้ว่าไม่มีการเปลี่ยนแปลงใด ๆ ที่เข้ามาอีก: เมื่อbuild()ถูกเรียกใช้

เช่นเดียวกับสิ่งต่างๆส่วนใหญ่นี่เป็นการตัดสินใจในบริบท หากบริบททำให้อึดอัดหรือซับซ้อนที่จะล้มเหลวก่อนกำหนดสามารถทำการแลกเปลี่ยนเพื่อเลื่อนการตรวจสอบในภายหลัง แต่ความล้มเหลวอย่างรวดเร็วควรเป็นค่าเริ่มต้น


ดังนั้นเพื่อสรุปคุณกำลังบอกว่ามันมีเหตุผลที่จะตรวจสอบโดยเร็วที่สุดเท่าที่จะทำได้ทุกอย่างที่อาจได้รับการครอบคลุมในประเภทวัตถุ / ดั้งเดิม? ชอบunsigned, @NonNullฯลฯ
skiwi

2
@skiwi สวยมากใช่ ตรวจสอบโดเมนตรวจสอบโมฆะสิ่งนั้น ฉันจะไม่แนะนำให้ใส่มากไปกว่านั้น: โดยทั่วไปแล้วผู้สร้างมักจะเป็นสิ่งที่เรียบง่าย
JvR

1
มันอาจจะเป็นที่น่าสังเกตว่าถ้าความถูกต้องของพารามิเตอร์หนึ่งขึ้นอยู่กับมูลค่าของอีกเพียงคนเดียวที่ถูกต้องตามกฎหมายสามารถปฏิเสธค่าพารามิเตอร์ถ้าใครรู้ว่าอีกคือ "จริงๆ" ที่จัดตั้งขึ้น ถ้ามันเป็นที่อนุญาตในการตั้งค่าพารามิเตอร์หลายครั้ง [กับการตั้งค่าล่าสุดเอาแบบอย่าง] จากนั้นในบางกรณีวิธีธรรมชาติมากที่สุดในการตั้งค่าวัตถุอาจจะพารามิเตอร์การตั้งค่าXให้เป็นค่าที่ไม่ถูกต้องให้มูลค่าปัจจุบันของY, แต่ก่อนที่จะโทรbuild()ตั้งYค่าที่จะทำให้Xถูกต้อง
supercat

หากผู้สร้างShapeและผู้สร้างมีWithLeftและWithRightคุณสมบัติและต้องการที่จะปรับผู้สร้างเพื่อสร้างวัตถุในสถานที่ที่แตกต่างกันซึ่งต้องWithRightเรียกว่าเป็นครั้งแรกเมื่อเคลื่อนย้ายวัตถุที่ถูกต้องและWithLeftเมื่อเคลื่อนย้ายไปทางซ้ายจะเพิ่มความซับซ้อนที่ไม่จำเป็น เปรียบเทียบกับการอนุญาตให้WithLeftตั้งค่าขอบซ้ายไปทางขวาของขอบด้านขวาเก่าโดยให้WithRightแก้ไขขอบขวาก่อนที่buildจะเรียก
supercat

0

กฎพื้นฐานคือ "ล้มเหลวเร็ว"

กฎขั้นสูงกว่าเล็กน้อยคือ "ล้มเหลวเร็วที่สุด"

หากคุณสมบัติไม่ถูกต้องภายใน ...

CarBuilder.numberOfWheels( -1 ). ...  

... จากนั้นคุณปฏิเสธทันที

กรณีอื่น ๆ อาจต้องมีการตรวจสอบค่ารวมกันและอาจอยู่ในเมธอด build () ที่ดีกว่า:

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.