เหตุใดฉันจึงควรหลีกเลี่ยงการสืบทอดหลายรายการใน C ++


167

เป็นแนวคิดที่ดีที่จะใช้การสืบทอดหลาย ๆ แบบหรือฉันสามารถทำสิ่งอื่นแทนได้หรือไม่?

คำตอบ:


259

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

สรุป

  1. พิจารณาองค์ประกอบของคุณสมบัติแทนการสืบทอด
  2. ระวังเพชรแห่งความกลัว
  3. พิจารณาการสืบทอดหลายอินเตอร์เฟสแทนวัตถุ
  4. บางครั้งมรดกหลายอย่างเป็นสิ่งที่ถูกต้อง ถ้าเป็นเช่นนั้นใช้มัน
  5. เตรียมพร้อมที่จะปกป้องสถาปัตยกรรมที่สืบทอดหลายของคุณในการตรวจสอบโค้ด

1. บางทีองค์ประกอบ?

สิ่งนี้เป็นจริงสำหรับการสืบทอดดังนั้นมันจึงเป็นความจริงสำหรับการสืบทอดหลาย ๆ อย่าง

วัตถุของคุณจำเป็นต้องสืบทอดจากสิ่งอื่นจริง ๆ หรือไม่? Carไม่จำเป็นต้องสืบทอดจากการทำงานหรือจากEngine มีและสี่WheelCarEngineWheel

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

2. เพชรแห่งความหวาดกลัว

โดยปกติแล้วคุณได้เรียนAแล้วBและทั้งสืบทอดมาจากC Aและ (อย่าถามฉันว่าทำไม) ใครบางคนตัดสินใจว่าDจะต้องสืบทอดทั้งจากBและCและ

ฉันพบปัญหาประเภทนี้สองครั้งในรอบแปดปีและมันสนุกที่ได้เห็นเพราะ:

  1. ความผิดพลาดมากแค่ไหนจากจุดเริ่มต้น (ในทั้งสองกรณีDไม่ควรสืบทอดมาจากทั้งคู่BและC) เพราะนี่เป็นสถาปัตยกรรมที่ไม่ดี (อันที่จริงแล้วCไม่ควรมีอยู่เลย ... )
  2. ผู้ดูแลรักษาจ่ายเท่าไรเพราะใน C ++ คลาสผู้ปกครองAจะปรากฏสองครั้งในคลาสหลานDดังนั้นการอัปเดตฟิลด์ผู้ปกครองหนึ่งรายการจึงA::fieldหมายถึงการอัปเดตสองครั้ง (ผ่านB::fieldและC::field) หรือมีบางสิ่งผิดปกติอย่างเงียบ ๆ และพังภายหลัง (ใหม่ตัวชี้ในB::fieldและลบC::field... )

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

ในลำดับชั้นวัตถุคุณควรพยายามรักษาลำดับชั้นเป็นแบบทรี (โหนดมีผู้ปกครองหนึ่งคน) ไม่ใช่เป็นกราฟ

เพิ่มเติมเกี่ยวกับเพชร (แก้ไข 2017-05-03)

ปัญหาที่แท้จริงกับ Diamond of Dread ใน C ++ ( สมมติว่าการออกแบบนั้นเป็นเสียง - ให้คุณตรวจสอบโค้ดของคุณ! ) คือคุณต้องทำการเลือก :

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

ตัวเลือกนี้มีสาเหตุมาจากปัญหาและใน C ++ ซึ่งแตกต่างจากภาษาอื่น ๆ คุณสามารถทำได้โดยไม่ต้องใช้ความเชื่อในการออกแบบในระดับภาษา

แต่ก็เหมือนกับพลังทั้งหมดด้วยพลังนั้นมาพร้อมความรับผิดชอบ: ให้การออกแบบของคุณได้รับการตรวจสอบ

3. การเชื่อมต่อ

การสืบทอดหลาย ๆ ครั้งของคลาสที่เป็นศูนย์หรือหนึ่งคลาสและอินเทอร์เฟซที่เป็นศูนย์หรือมากกว่านั้นก็โอเคเพราะคุณจะไม่พบกับ Diamond of Dread ที่อธิบายไว้ข้างต้น ในความเป็นจริงนี่คือสิ่งที่ทำใน Java

โดยปกติแล้วสิ่งที่คุณหมายถึงเมื่อ C สืบทอดจากAและBเป็นที่ผู้ใช้สามารถใช้Cเป็นถ้ามันเป็นAและ / Bหรือราวกับว่ามันเป็น

ใน C ++ อินเตอร์เฟสคือคลาสนามธรรมซึ่งมี:

  1. วิธีการทั้งหมดประกาศบริสุทธิ์เสมือน ( ต่อท้ายด้วย = 0) (ลบ 2017-05-03)
  2. ไม่มีตัวแปรสมาชิก

การรับมรดกหลายรายการของวัตถุจริงหนึ่งศูนย์และอินเทอร์เฟซศูนย์หรือมากกว่านั้นไม่ถือว่าเป็น "เหม็น" (อย่างน้อยก็ไม่มาก)

เพิ่มเติมเกี่ยวกับอินเทอร์เฟซ C ++ บทคัดย่อ (แก้ไข 2017-05-03)

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

ประการที่สองใน C ++ มันสมเหตุสมผลที่จะสืบทอดจากอินเตอร์เฟสนามธรรม (แม้จะมีค่าใช้จ่ายเพิ่มเติม / โดยอ้อม) หากคุณไม่มีและมรดกอินเทอร์เฟซปรากฏขึ้นหลายครั้งในลำดับชั้นของคุณคุณจะมีความคลุมเครือ

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

4. คุณต้องการมรดกหลายอันหรือไม่

บางครั้งใช่.

โดยปกติแล้วคุณCเรียนสืบทอดจากAและBและAและBมีวัตถุทั้งสองไม่เกี่ยวข้องกัน (คือไม่ได้อยู่ในลำดับชั้นเดียวกันอะไรกันแนวคิดที่แตกต่างกัน ฯลฯ )

ตัวอย่างเช่นคุณอาจมีระบบที่Nodesมีพิกัด X, Y, Z สามารถทำการคำนวณทางเรขาคณิตจำนวนมาก (อาจเป็นจุดส่วนหนึ่งของวัตถุทางเรขาคณิต) และแต่ละโหนดเป็นตัวแทนอัตโนมัติที่สามารถสื่อสารกับตัวแทนอื่น ๆ

บางทีคุณมีสิทธิ์เข้าถึงไลบรารีสองแห่งแล้วแต่ละแห่งมีเนมสเปซของตัวเอง (อีกเหตุผลที่ใช้เนมสเปซ ... แต่คุณใช้เนมสเปซไม่ใช่หรือ?) อีกอันหนึ่งgeoกับอีกอันai

เพื่อให้คุณมีของคุณเองown::Nodeการสืบทอดมาทั้งจากและai::Agentgeo::Point

นี่คือช่วงเวลาที่คุณควรถามตัวเองว่าคุณไม่ควรใช้การแต่งเพลงแทนหรือไม่ ถ้าown::Nodeเป็นจริงทั้ง a ai::Agentและ a จริงๆgeo::Pointแล้วการจัดองค์ประกอบจะไม่ทำ

จากนั้นคุณจะต้องมีการสืบทอดหลายครั้งโดยให้คุณown::Nodeสื่อสารกับตัวแทนอื่น ๆ ตามตำแหน่งในพื้นที่ 3 มิติ

(คุณจะสังเกตได้ว่าai::Agentและgeo::Pointไม่เกี่ยวข้องทั้งหมดโดยสิ้นเชิง ... สิ่งนี้จะช่วยลดอันตรายจากการสืบทอดหลายอย่างมาก)

กรณีอื่น (แก้ไข 2017-05-03)

มีกรณีอื่น ๆ :

  • การใช้การสืบทอด (หวังว่าเป็นส่วนตัว) เป็นรายละเอียดการใช้งาน
  • สำนวน C ++ บางอย่างเช่นนโยบายสามารถใช้การสืบทอดหลายอย่าง (เมื่อแต่ละส่วนต้องการสื่อสารกับผู้อื่นผ่านthis)
  • การสืบทอดเสมือนจาก std :: exception (การสืบทอดเสมือนจำเป็นสำหรับการยกเว้นหรือไม่ )
  • เป็นต้น

บางครั้งคุณสามารถใช้การจัดองค์ประกอบและบางครั้ง MI ก็ดีกว่า ประเด็นคือ: คุณมีทางเลือก ทำด้วยความรับผิดชอบ (และตรวจสอบรหัสของคุณ)

5. ดังนั้นฉันควรทำมรดกหลายอย่างหรือไม่

ส่วนใหญ่แล้วในประสบการณ์ของฉันไม่มี MI ไม่ใช่เครื่องมือที่ถูกต้องแม้ว่าจะดูเหมือนว่าใช้งานได้เพราะขี้เกียจสามารถใช้งานร่วมกันได้โดยไม่ต้องคำนึงถึงผลที่จะตามมา (เช่นทำให้Carทั้งคู่Engineและ a Wheel)

แต่บางครั้งก็ใช่ และในเวลานั้นไม่มีอะไรจะทำงานได้ดีกว่า MI

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


4
ฉันจะยืนยันว่ามันไม่จำเป็นเลยและไม่ค่อยมีประโยชน์เลยแม้ว่ามันจะเป็นแบบที่สมบูรณ์แบบการประหยัดมากกว่าการมอบหมายจะไม่ชดเชยความสับสนให้กับโปรแกรมเมอร์ที่ไม่ได้ใช้มัน แต่ก็เป็นข้อสรุปที่ดี
Bill K

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

" เพราะคุณจะไม่พบกับ Diamond of Dread ที่อธิบายไว้ข้างต้น " ทำไมล่ะ?
curiousguy

1
ฉันใช้ MI สำหรับรูปแบบผู้สังเกตการณ์
Calmarius

13
@ บิลล์: แน่นอนว่ามันไม่จำเป็น หากคุณมีการเปลี่ยนภาษาโดยไม่ใช้ MI คุณสามารถทำสิ่งใดก็ได้กับภาษาที่ MI ทำได้ ใช่มันไม่จำเป็น เคย. ที่กล่าวมาอาจเป็นเครื่องมือที่มีประโยชน์มากและเพชรที่เรียกว่า Dreaded Diamond ... ฉันไม่เคยเข้าใจเลยจริงๆว่าทำไมคำว่า "หวั่น" ถึงที่นั่น จริงๆแล้วมันค่อนข้างง่ายที่จะให้เหตุผลและมักจะไม่มีปัญหา
Thomas Eding

145

จากการสัมภาษณ์กับ Bjarne Stroustrup :

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


6
มีคุณสมบัติบางอย่างของ C ++ ที่ฉันพลาดใน C # และ Java แต่การมีพวกมันจะทำให้การออกแบบยากขึ้น / ซับซ้อนขึ้น ตัวอย่างเช่นการที่ไม่สามารถเปลี่ยนแปลงได้รับประกันได้const- ฉันต้องเขียนวิธีแก้ปัญหาแบบ clunky (โดยปกติจะใช้ส่วนต่อประสานและองค์ประกอบ)เมื่อชั้นเรียนจำเป็นต้องมีตัวแปรที่ไม่แน่นอนและไม่เปลี่ยนรูป อย่างไรก็ตามฉันไม่เคยพลาดการรับมรดกหลายครั้งและไม่เคยรู้สึกว่าฉันต้องเขียนวิธีแก้ปัญหาเนื่องจากขาดคุณสมบัตินี้ นั่นคือความแตกต่าง ในทุกกรณีที่ฉันเคยเห็นไม่ได้ใช้ MI เป็นตัวเลือกที่ดีกว่าการออกแบบไม่ได้แก้ปัญหา
BlueRaja - Danny Pflughoeft

24
+1: คำตอบที่สมบูรณ์แบบ: หากคุณไม่ต้องการใช้มันก็ไม่ควรใช้
Thomas Eding

38

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

สิ่งที่ยิ่งใหญ่ที่สุดคือเพชรแห่งความตาย:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

ตอนนี้คุณมี "สำเนา" ของ GrandParent ภายในชุดย่อย

C ++ มีความคิดในเรื่องนี้และให้คุณทำเสมือนการสืบทอดเพื่อแก้ไขปัญหา

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

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


21
และถ้าคุณห้ามมรดกและองค์ประกอบการใช้งานเพียง แต่คุณมักจะมีสองในGrandParent Childมีความกลัว MI เพราะคนคิดว่าพวกเขาอาจไม่เข้าใจกฎภาษา แต่ทุกคนที่ไม่สามารถได้รับกฎง่ายๆเหล่านี้ก็ไม่สามารถเขียนโปรแกรมที่ไม่สำคัญ
curiousguy

1
ฟังดูรุนแรงกฏ C ++ ทุกรอบนั้นซับซ้อนมาก ตกลงดังนั้นแม้ว่ากฎของแต่ละคนจะง่าย แต่ก็มีหลายคนที่ทำให้ทุกอย่างไม่ง่ายอย่างน้อยสำหรับมนุษย์ที่จะปฏิบัติตาม
Zebrafish

11

ดูที่ w: การสืบทอดหลายอย่าง

การสืบทอดหลายครั้งได้รับการวิจารณ์และไม่ได้นำมาใช้ในหลายภาษา การวิจารณ์รวมถึง:

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

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

วิธีที่ทันสมัยในการแก้ไขปัญหานี้เพื่อใช้อินเทอร์เฟซ (คลาสนามธรรมที่เป็นนามธรรม) เช่น COM และ Java อินเตอร์เฟส

ฉันสามารถทำสิ่งอื่นแทนสิ่งนี้ได้ไหม

ใช่คุณสามารถ. ฉันกำลังจะไปขโมยจากGoF

  • โปรแกรมไปยังส่วนต่อประสานไม่ใช่การใช้งาน
  • ชอบการแต่งมากกว่าการสืบทอด

8

การสืบทอดสาธารณะเป็นความสัมพันธ์แบบ IS-A และบางครั้งคลาสจะเป็นประเภทของคลาสที่แตกต่างกันหลายครั้งและบางครั้งก็เป็นสิ่งสำคัญที่จะสะท้อนสิ่งนี้

"Mixins" ก็มีประโยชน์เช่นกัน โดยทั่วไปแล้วจะเป็นชั้นเรียนขนาดเล็กซึ่งมักจะไม่สืบทอดจากสิ่งใด ๆ

ตราบใดที่ลำดับชั้นการสืบทอดค่อนข้างตื้น (และเกือบจะทุกครั้ง) และมีการจัดการที่ดีคุณไม่น่าจะได้รับมรดกเพชรที่น่ากลัว ข้าวหลามตัดไม่ได้มีปัญหากับทุกภาษาที่ใช้การสืบทอดหลายแบบ แต่การรักษา C ++ นั้นค่อนข้างแปลกและบางครั้งก็ทำให้งงงวย

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


6

คุณไม่ควร "หลีกเลี่ยง" การสืบทอดหลายอย่าง แต่คุณควรตระหนักถึงปัญหาที่อาจเกิดขึ้นเช่น 'ปัญหาเพชร' ( http://en.wikipedia.org/wiki/Diamond_problem ) และรักษาพลังที่คุณได้รับด้วยความระมัดระวัง เท่าที่คุณควรจะมีพลังทั้งหมด


1
+1: อย่างที่คุณไม่ควรหลีกเลี่ยงพอยน์เตอร์ คุณแค่ต้องระวังเรื่องการแตกกิ่งก้านของมัน ผู้คนจำเป็นต้องหยุดใช้วลีที่ไม่หยุดหย่อน (เช่น "MI is evil", "dont optimization", "goto is evil") พวกเขาเรียนรู้จากอินเทอร์เน็ตเพียงเพราะพวกฮิปปี้บางคนกล่าวว่าไม่ดีต่อสุขอนามัยของพวกเขา ส่วนที่แย่ที่สุดคือพวกเขาไม่เคยลองใช้สิ่งเหล่านั้นและเอามันไปราวกับว่าพูดจากพระคัมภีร์
Thomas Eding

1
ฉันเห็นด้วย. อย่า "หลีกเลี่ยง" แต่รู้วิธีที่ดีที่สุดในการจัดการปัญหา บางทีฉันอาจต้องการใช้คุณสมบัติการแต่งเพลง แต่ C ++ ไม่มีสิ่งนั้น บางทีฉันอาจต้องการใช้การมอบหมายอัตโนมัติ 'จัดการ' แต่ C ++ ไม่มีสิ่งนั้น ดังนั้นฉันจึงใช้เครื่องมือทั่วไปและเครื่องมือทื่อของ MI เพื่อวัตถุประสงค์ที่แตกต่างกันหลายอย่างบางทีอาจพร้อมกัน
JDługosz

3

ด้วยความเสี่ยงที่จะเกิดความเป็นนามธรรมขึ้นมาฉันคิดว่ามันเป็นความคิดที่น่าสนใจเกี่ยวกับการสืบทอดภายในกรอบของทฤษฎีหมวดหมู่

หากเรานึกถึงคลาสและลูกศรทั้งหมดของเราที่อยู่ระหว่างพวกเขาเพื่อแสดงถึงความสัมพันธ์ในการถ่ายทอดทางพันธุกรรมสิ่งนี้

A --> B

หมายความว่ามาจากclass B class Aโปรดทราบว่าให้

A --> B, B --> C

เราบอกว่า C มาจาก B ซึ่งมาจาก A ดังนั้น C ก็บอกว่ามาจาก A ดังนั้น

A --> C

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

นั่นคือการตั้งค่าเล็กน้อย แต่ถ้าอย่างนั้นเรามาดู Diamond of Doom ของเรา:

C --> D
^     ^
|     |
A --> B

มันเป็นแผนภาพดูร่มรื่น แต่มันจะทำ ดังนั้นDสืบทอดจากทั้งหมดของA, และB Cนอกจากนี้และได้รับการใกล้ชิดเพื่อที่อยู่คำถามของ OP, Dยังสืบทอดจาก superclass Aใด เราสามารถวาดไดอะแกรม

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

ตอนนี้ปัญหาที่เกี่ยวข้องกับ Diamond of Death ที่นี่คือเมื่อใดCและBแบ่งปันชื่อทรัพย์สิน / วิธีการและสิ่งต่าง ๆ ที่คลุมเครือ อย่างไรก็ตามหากเราย้ายพฤติกรรมที่ใช้ร่วมกันไปสู่Aความคลุมเครือจะหายไป

ใส่ในแง่เด็ดขาดเราต้องการA, BและCเป็นเช่นว่าถ้าBและCสืบทอดจากQนั้นสามารถเขียนเท่าที่เป็นรองA Qนี้จะทำให้Aสิ่งที่เรียกว่าpushout

นอกจากนี้ยังมีการก่อสร้างสมมาตรบนDเรียกว่าดึง นี้เป็นหลักใหญ่ระดับประโยชน์ทั่วไปที่คุณสามารถสร้างซึ่งสืบทอดจากทั้งสองและB Cนั่นคือถ้าคุณมีระดับอื่น ๆRคูณสืบทอดจากBและCจากนั้นDเป็นชั้นที่สามารถเขียนเท่าที่เป็นรองRD

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

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

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

TL; DRคิดถึงความสัมพันธ์ในการรับมรดกในโปรแกรมของคุณในรูปของหมวดหมู่ จากนั้นคุณสามารถหลีกเลี่ยงปัญหาของ Diamond of Doom ได้โดยการเพิ่มคลาสที่สืบทอดมาหลายครั้งและทำให้เป็นคลาสแม่แบบทั่วไปซึ่งเป็นการดึงกลับ


3

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

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

ในขณะที่คุณกำลังมองหา: จดบันทึกเป็นพิเศษเกี่ยวกับความปลอดภัยเป็นโมฆะและการกำจัดของตัวชี้ Null dereferencing ในขณะที่เรากำลังเต้นรำไปรอบ ๆ MI พอยน์เตอร์ของคุณก็หลงทาง! :-)


2

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

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

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

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

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

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

ตอนนี้ฉันไม่ต้องการให้ความประทับใจว่านี่เป็นวิธีเดียวที่จะใช้การสืบทอดหลายอย่างได้อย่างปลอดภัย เป็นวิธีเดียวที่ตรวจสอบได้ค่อนข้างง่าย


2

คุณควรใช้อย่างระมัดระวังมีบางกรณีเช่นปัญหาเพชรเมื่อสิ่งต่าง ๆ มีความซับซ้อน

ข้อความแสดงแทน
(ที่มา: learncpp.com )


12
นี่เป็นตัวอย่างที่ไม่ดีเนื่องจากเครื่องถ่ายเอกสารควรประกอบด้วยสแกนเนอร์และเครื่องพิมพ์ไม่ใช่รับมาจากพวกเขา
Svante

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


1

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

องค์ประกอบนั้นง่ายต่อการเข้าใจเข้าใจและอธิบายโดยเนื้อแท้ มันน่าเบื่อที่จะเขียนโค้ดสำหรับ แต่ IDE ที่ดี (มันไม่กี่ปีที่ผ่านมาตั้งแต่ฉันทำงานกับ Visual Studio แต่แน่นอนว่า Java IDEs ทั้งหมดมีเครื่องมือทางลัดองค์ประกอบอัตโนมัติที่ยอดเยี่ยม) ควรจะได้รับคุณข้ามอุปสรรค์นั้น

นอกจากนี้ในแง่ของการบำรุงรักษา "ปัญหาเพชร" เกิดขึ้นในอินสแตนซ์ที่ไม่ใช่ตัวอักษรเช่นกัน ตัวอย่างเช่นหากคุณมี A และ B และคลาส C ของคุณขยายทั้งสองและ A มีวิธี 'makeJuice' ซึ่งทำให้น้ำส้มและคุณขยายที่จะทำให้น้ำส้มกับมะนาวบิด: เกิดอะไรขึ้นเมื่อนักออกแบบสำหรับ ' B 'เพิ่มวิธี' makeJuice 'ซึ่งสร้างและกระแสไฟฟ้า? 'A' และ 'B' อาจเข้ากันได้กับ "ผู้ปกครอง" ในตอนนี้แต่นั่นไม่ได้หมายความว่าพวกเขาจะเป็นเช่นนั้นเสมอ!

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


what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?เอ่อคุณได้รับข้อผิดพลาดในการรวบรวมแน่นอน (ถ้าใช้ไม่ชัดเจน)
Thomas Eding

1

ปัญหาสำคัญกับ MI ของวัตถุที่เป็นรูปธรรมก็คือคุณไม่ค่อยมีวัตถุที่ถูกต้องตามกฎหมายควร "เป็น A และเป็น B" ดังนั้นจึงไม่ค่อยแก้ปัญหาที่ถูกต้องในเหตุผลเชิงตรรกะ บ่อยครั้งที่คุณมีวัตถุ C ที่เชื่อฟัง "C สามารถทำหน้าที่เป็น A หรือ B" ซึ่งคุณสามารถบรรลุได้ผ่านการสืบทอดและองค์ประกอบของส่วนต่อประสาน แต่ไม่ผิดพลาด - การสืบทอดของอินเตอร์เฟสหลาย ๆ ตัวยังคงเป็น MI เพียงส่วนหนึ่งของมัน

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

class B : public A, public A {};

ผิดรูปแบบโดยคำจำกัดความ แปลเป็นภาษาอังกฤษนี่คือ "B คือ A และ A" ดังนั้นแม้ในภาษามนุษย์มีความกำกวมอย่างรุนแรง คุณหมายถึง "B has 2 As" หรือเพียงแค่ "B is A"? การอนุญาตให้ใช้รหัสทางพยาธิวิทยาดังกล่าวและทำให้ตัวอย่างเป็นตัวอย่างการใช้งานที่แย่ลงทำให้ C ++ ไม่ได้รับประโยชน์เมื่อมันมาถึงการสร้างเคสเพื่อรักษาคุณลักษณะในภาษาที่สืบทอดมา


0

คุณสามารถใช้องค์ประกอบในการตั้งค่าการสืบทอด

ความรู้สึกทั่วไปคือการจัดองค์ประกอบที่ดีกว่าและมันเป็นเรื่องที่ดีมาก


4
-1: The general feeling is that composition is better, and it's very well discussed.นั่นไม่ได้หมายความว่าองค์ประกอบจะดีกว่า
Thomas Eding

ยกเว้นว่าเป็นจริงดีกว่า
Ali Afshar

12
ยกเว้นกรณีที่มันไม่ดีขึ้น
Thomas Eding

0

ใช้เวลา 4/8 ไบต์ต่อคลาสที่เกี่ยวข้อง (หนึ่งตัวชี้นี้ต่อคลาส)

สิ่งนี้อาจไม่น่าเป็นห่วง แต่ถ้าวันหนึ่งคุณมีโครงสร้างข้อมูลขนาดเล็กซึ่งมีเวลานับพันล้านครั้ง


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

3
@DavidThornley " คุณมีโครงสร้างข้อมูลขนาดเล็กหลายพันล้านครั้ง " เมื่อคุณสร้างข้อโต้แย้งกับ MI
curiousguy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.