ชอบการแต่งมากกว่าการสืบทอด?


1604

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



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

1
ในการสืบทอดประโยคหนึ่งเป็นสาธารณะถ้าคุณมีวิธีการสาธารณะและคุณเปลี่ยนมันเปลี่ยน api เผยแพร่ หากคุณมีองค์ประกอบและวัตถุที่ประกอบมีการเปลี่ยนแปลงคุณไม่จำเป็นต้องเปลี่ยน API ที่เผยแพร่ของคุณ
Tomer Ben David

คำตอบ:


1188

ชอบการแต่งมากกว่าการสืบทอดเนื่องจากสามารถแก้ไขได้ง่ายกว่า / ดัดแปลงได้ในภายหลัง แต่อย่าใช้วิธีการเขียนแบบเสมอ ด้วยการจัดวางองค์ประกอบทำให้ง่ายต่อการเปลี่ยนพฤติกรรมได้ทันทีด้วย Dependency Injection / Setters การสืบทอดมีความเข้มงวดมากขึ้นเนื่องจากภาษาส่วนใหญ่ไม่อนุญาตให้คุณได้รับมากกว่าหนึ่งประเภท ดังนั้นห่านจะสุกมากขึ้นหรือน้อยลงเมื่อคุณได้รับจาก TypeA

การทดสอบกรดของฉันสำหรับด้านบนคือ:

  • TypeB ต้องการเปิดเผยอินเตอร์เฟสที่สมบูรณ์ (วิธีสาธารณะทั้งหมดไม่น้อยกว่า) ของ TypeA เช่นนั้น TypeB สามารถใช้งานได้ตามที่คาดว่า TypeA หรือไม่ บ่งบอกถึงการถ่ายทอดทางพันธุกรรม

    • เช่นเครื่องบิน Bessane Cessna จะเปิดเผยส่วนต่อประสานที่สมบูรณ์ของเครื่องบินหากไม่มากขึ้น นั่นทำให้มันเหมาะที่จะได้มาจาก Airplane
  • TypeB ต้องการบางส่วน / ส่วนหนึ่งของพฤติกรรมที่เปิดเผยโดย TypeA หรือไม่ บ่งบอกถึงความต้องการองค์ประกอบ

    • เช่นนกอาจต้องการเพียงพฤติกรรมการบินของเครื่องบิน ในกรณีนี้มันเหมาะสมที่จะดึงมันออกมาเป็น interface / class / both และทำให้มันเป็นสมาชิกของทั้งสองคลาส

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


81
ตัวอย่างที่สองตรงจากรูปแบบการออกแบบ Head First ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) หนังสือ :) ฉันอยากจะแนะนำหนังสือเล่มนี้ให้กับทุกคนที่ googling คำถามนี้
Jeshurun

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

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

22
@Alexey - ประเด็นคือ 'ฉันจะส่งเครื่องบิน Cessna ให้กับลูกค้าทุกคนที่คาดหวังเครื่องบินได้หรือไม่?' ถ้าใช่คุณมีโอกาสได้รับมรดก
Gishu

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

414

นึกถึงการกักกันว่ามีความสัมพันธ์ รถยนต์ "มี" เครื่องยนต์บุคคล "มีชื่อ" ฯลฯ

คิดว่าในฐานะที่เป็นมรดกเป็นความสัมพันธ์ รถยนต์ "คือ" ยานพาหนะบุคคล "คือ" สัตว์เลี้ยงลูกด้วยนม ฯลฯ

ฉันไม่ได้รับเครดิตสำหรับวิธีการนี้ ฉันเอามันมาจากCode สมบูรณ์ฉบับที่สองโดยSteve McConnell , มาตรา 6.3


107
นี่ไม่ใช่วิธีที่สมบูรณ์แบบเสมอไปมันเป็นแนวทางที่ดี หลักการชดเชย Liskov นั้นแม่นยำกว่า (ล้มเหลวน้อยกว่า)
Bill K

40
"รถของฉันมียานพาหนะ" หากคุณพิจารณาว่าเป็นประโยคแยกต่างหากไม่ใช่ในบริบทของการเขียนโปรแกรมซึ่งไม่มีเหตุผล และนั่นคือจุดรวมของเทคนิคนี้ ถ้ามันฟังดูน่าอึดอัดใจมันอาจจะผิด
Nick Zalutskiy

36
@Nick Sure แต่ "รถของฉันมีพาหนะเป็นยานพาหนะช่วยชีวิต" มีเหตุผลมากกว่านี้ (ฉันคิดว่าคลาส "ยานพาหนะ" ของคุณอาจมีชื่อว่า "VehicleBehavior") ดังนั้นคุณไม่สามารถยึดการตัดสินใจของคุณใน "has" vs "คือ" การเปรียบเทียบคุณต้องใช้ LSP มิฉะนั้นคุณจะทำผิดพลาด
ละคร

35
แทนที่จะ "เป็น" ความคิด "ก็จะทำตัวเหมือนกัน การสืบทอดเป็นเรื่องเกี่ยวกับการสืบทอดพฤติกรรมไม่ใช่แค่ความหมาย
ybakos

4
@ybakos "Behaves like" สามารถทำได้ผ่านทางอินเตอร์เฟสโดยไม่จำเป็นต้องสืบทอด จากวิกิพีเดีย : "การดำเนินการตามองค์ประกอบของการถ่ายทอดทางพันธุกรรมมักเริ่มต้นด้วยการสร้างอินเทอร์เฟซต่าง ๆ ที่เป็นตัวแทนของพฤติกรรมที่ระบบต้องแสดง ... ดังนั้นพฤติกรรมของระบบจะถูกรับรู้โดยไม่ต้องรับมรดก"
DavidRR

210

ถ้าคุณเข้าใจความแตกต่างมันจะอธิบายได้ง่ายกว่า

รหัสขั้นตอน

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

มรดก

สิ่งนี้ส่งเสริมการใช้คลาส การสืบทอดเป็นหนึ่งในสามหลักของการออกแบบ OO (การสืบทอด, ความหลากหลาย, การห่อหุ้ม)

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

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

ส่วนประกอบ

องค์ประกอบเป็นที่โปรดปรานมากกว่ามรดก เพื่อให้ง่ายมากคุณจะต้อง:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

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

องค์ประกอบมากกว่ามรดก

ตอนนี้พูดว่าคุณต้องการสร้างประเภทผู้จัดการดังนั้นคุณจะได้:

class Manager : Person, Employee {
   ...
}

ตัวอย่างนี้จะใช้งานได้ดี แต่จะเกิดอะไรขึ้นถ้าทั้งบุคคลและพนักงานประกาศTitle? Manager.Title ควรส่งคืน "Manager of Operations" หรือ "Mr. " หรือไม่ ภายใต้องค์ประกอบความคลุมเครือนี้จะถูกจัดการที่ดีกว่า:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

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


6
สำหรับการสืบทอด: ไม่มีความกำกวม คุณกำลังใช้คลาสผู้จัดการตามข้อกำหนด ดังนั้นคุณจะกลับมา "ผู้จัดการฝ่ายปฏิบัติการ" ถ้านั่นคือสิ่งที่คุณระบุความต้องการอื่น ๆ ที่คุณจะใช้การดำเนินงานของชั้นฐาน นอกจากนี้คุณสามารถทำให้ Person เป็นคลาสนามธรรมและทำให้แน่ใจว่าคลาส down-stream ใช้คุณสมบัติ Title
Raj Rao

68
มันเป็นเรื่องสำคัญที่จะต้องจำไว้ว่าใครบางคนอาจพูดว่า "การประพันธ์ต่อการสืบทอด" แต่นั่นไม่ได้หมายความว่า "Is" หมายถึงการสืบทอดและนำไปสู่การใช้ซ้ำรหัส ลูกจ้างคือคน (พนักงานไม่มีคน)
Raj Rao

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

15
ฉันไม่เห็นด้วยกับตัวอย่างนี้ ลูกจ้างคือบุคคลซึ่งเป็นกรณีศึกษาของการใช้มรดกอย่างเหมาะสม ฉันยังคิดว่า "ปัญหา" การกำหนดนิยามใหม่ของฟิลด์ชื่อเรื่องไม่สมเหตุสมผล ความจริงที่ว่า Employee.Title เงา Person.Title เป็นสัญลักษณ์ของการเขียนโปรแกรมที่ไม่ดี ท้ายที่สุดก็คือ "นาย" และ "ผู้จัดการฝ่ายปฏิบัติการ" หมายถึงแง่มุมเดียวกันของบุคคล (ตัวพิมพ์เล็ก) จริงหรือ? ฉันจะเปลี่ยนชื่อ Employee.Title และสามารถอ้างอิงแอตทริบิวต์ Title และ JobTitle ของพนักงานซึ่งทั้งสองอย่างนี้สมเหตุสมผลในชีวิตจริง นอกจากนี้ไม่มีเหตุผลสำหรับผู้จัดการ (ต่อ ... )
Radon Rosborough

9
(... ต่อ) เพื่อสืบทอดจากทั้งบุคคลและพนักงาน - หลังจากทั้งหมดพนักงานสืบทอดมาจากบุคคลแล้ว ในรูปแบบที่ซับซ้อนมากขึ้นซึ่งบุคคลอาจเป็นผู้จัดการและตัวแทนมันเป็นความจริงที่สามารถใช้การสืบทอดหลายอย่าง (อย่างระมัดระวัง!) แต่มันจะดีกว่าในหลาย ๆ สภาพแวดล้อมเพื่อให้มีคลาสนามธรรมที่ผู้จัดการ (มีพนักงาน) s / เขาจัดการ) และตัวแทน (มีสัญญาและข้อมูลอื่น ๆ ) สืบทอด จากนั้นพนักงานคือบุคคลที่มีหลายบทบาท ดังนั้นทั้งองค์ประกอบและมรดกจึงถูกใช้อย่างเหมาะสม
Radon Rosborough

142

ด้วยผลประโยชน์ที่ไม่อาจปฏิเสธได้จากการสืบทอดนี่คือข้อเสียบางประการ

ข้อเสียของการสืบทอด:

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

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


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

3
ย่อหน้าสุดท้ายในโพสต์นี้คลิกสำหรับฉันจริงๆ ขอบคุณ.
Salx

87

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


81

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

คุณควรเลือกมันฝรั่งมากกว่าโคคาโคล่าเมื่อคุณต้องการที่จะกินและโคคาโคล่ามากกว่ามันฝรั่งเมื่อคุณต้องการที่จะดื่ม

การสร้างคลาสย่อยควรมีความหมายมากกว่าเพียงแค่วิธีที่สะดวกในการเรียกเมธอดซูเปอร์คลาส คุณควรใช้การสืบทอดเมื่อ subclass "is-a" super class มีทั้งโครงสร้างและหน้าที่เมื่อมันสามารถใช้เป็น superclass และคุณจะใช้มัน ถ้าไม่ใช่ในกรณี - มันไม่ใช่มรดก แต่อย่างอื่น องค์ประกอบคือเมื่อวัตถุของคุณประกอบด้วยวัตถุอื่นหรือมีความสัมพันธ์บางอย่างกับพวกเขา

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


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

61

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

การสืบทอดอาจเป็นรูปแบบที่เลวร้ายที่สุดของการมีเพศสัมพันธ์ที่คุณสามารถมี

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

บทความInheritance is Evil: The Epic Fail ของ DataAnnotationsModelBinderเดินผ่านตัวอย่างของสิ่งนี้ใน C # มันแสดงให้เห็นถึงการใช้มรดกเมื่อองค์ประกอบควรจะถูกนำมาใช้และวิธีที่จะสามารถ refactored


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

42

ใน Java หรือ C # วัตถุไม่สามารถเปลี่ยนชนิดของมันได้เมื่อวัตถุนั้นถูกสร้างขึ้นมาแล้ว

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

หากวัตถุต้องเป็นประเภทเดียวกันให้ใช้การสืบทอดหรือการนำอินเตอร์เฟสมาใช้


10
+1 ฉันพบว่ามรดกน้อยลงในสถานการณ์ส่วนใหญ่ ฉันชอบอินเทอร์เฟซที่ใช้ร่วมกัน / สืบทอดมาและองค์ประกอบของวัตถุ .... หรือเรียกว่าการรวมตัวหรือไม่ อย่าถามฉันฉันได้รับปริญญา EE !!
kenny

ฉันเชื่อว่านี่เป็นสถานการณ์ที่พบได้บ่อยที่สุดที่มีการใช้ "การรวมตัวกับการสืบทอด" เนื่องจากทั้งสองอาจเหมาะสมในทางทฤษฎี Clientยกตัวอย่างเช่นในระบบการตลาดที่คุณอาจมีแนวความคิดของการเป็น จากนั้นแนวคิดใหม่ของการPreferredClientปรากฏขึ้นในภายหลัง ควรPreferredClientได้รับมรดกClient? ไคลเอนต์ที่ต้องการ 'คือ' ไคลเอนต์ afterall ไม่? ไม่เร็วนัก ... อย่างที่คุณพูดว่าวัตถุไม่สามารถเปลี่ยนคลาสได้ในขณะทำงาน คุณจะทำแบบจำลองการclient.makePreferred()ทำงานอย่างไร บางทีคำตอบอยู่ในการใช้ประกอบกับแนวคิดที่หายไปเป็นAccountบางที?
plalx

แทนที่จะมีประเภทที่แตกต่างกันของClientการเรียนอาจจะมีเพียงหนึ่งที่ห่อหุ้มแนวคิดของนั้นAccountซึ่งอาจจะเป็นStandardAccountหรือPreferredAccount...
plalx

40

ไม่พบคำตอบที่น่าพอใจที่นี่ดังนั้นฉันจึงเขียนคำตอบใหม่

เพื่อให้เข้าใจว่าทำไม " ชอบองค์ประกอบมากกว่าการสืบทอด" ก่อนอื่นเราต้องกลับสมมติฐานที่ละเว้นในสำนวนที่สั้นกว่านี้

มีประโยชน์สองประการของการสืบทอด: การพิมพ์ย่อยและการจัดคลาสย่อย

  1. การพิมพ์ย่อยหมายถึงการปฏิบัติตามลายเซ็นประเภท (อินเตอร์เฟส) เช่นชุดของ API และสามารถแทนที่ส่วนหนึ่งของลายเซ็นเพื่อให้ได้ polymorphism ชนิดย่อย

  2. การจัดคลาสย่อยหมายถึงการนำการใช้งานมาใช้ซ้ำโดยนัย

ด้วยประโยชน์ทั้งสองประการนั้นมีวัตถุประสงค์ที่แตกต่างกันสองประการสำหรับการรับมรดก: การพิมพ์ย่อยและการใช้รหัสซ้ำ

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

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

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

  1. Inheritance จัดให้มีการนำรหัสมาใช้อย่างตรงไปตรงมาหากไม่ได้ถูกเขียนทับในขณะที่การเรียบเรียงจะต้องกำหนดรหัสใหม่ทุก API แม้ว่าจะเป็นเพียงการมอบหมายงานง่ายๆ

  2. มรดกให้ตรงไปตรงมาrecursion เปิดผ่านทางเว็บไซต์ภายใน polymorphic thisคืออัญเชิญเอาชนะวิธี (หรือแม้กระทั่งประเภท ) ในการทำงานของสมาชิกคนอื่น ๆ ทั้งภาครัฐและเอกชน (แม้ว่าท้อแท้ ) การเรียกซ้ำแบบเปิดสามารถจำลองได้ผ่านการจัดวางองค์ประกอบแต่ต้องใช้ความพยายามเป็นพิเศษและอาจไม่สามารถทำงานได้ (?) นี้คำตอบของคำถามที่ซ้ำกันพูดถึงสิ่งที่คล้ายกัน

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

  4. องค์ประกอบมี BeFit ผกผันของการควบคุมและการพึ่งพาสามารถฉีดแบบไดนามิกเป็นที่แสดงในรูปแบบมัณฑนากรและรูปแบบการพร็อกซี่

  5. องค์ประกอบที่มีประโยชน์ของCombinator ที่มุ่งเน้นการเขียนโปรแกรมคือการทำงานในลักษณะเหมือนเป็นรูปแบบคอมโพสิต

  6. องค์ประกอบทันทีตามการเขียนโปรแกรมเพื่อติดต่อ

  7. องค์ประกอบมีประโยชน์ในการที่ง่ายมรดกหลาย

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


34

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

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

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

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

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


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

25

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

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

คำตอบที่สมบูรณ์และเป็นรูปธรรมมากขึ้นจาก Tim Boudreau of Sun:

ปัญหาที่พบบ่อยในการใช้การสืบทอดตามที่ฉันเห็นคือ:

  • การกระทำที่ไร้เดียงสาสามารถให้ผลลัพธ์ที่ไม่คาดคิดได้ตัวอย่างคลาสสิกของสิ่งนี้คือการเรียกใช้วิธี overridable จาก superclass constructor ก่อนที่ฟิลด์อินสแตนซ์คลาสย่อยจะถูกเตรียมใช้งาน ในโลกที่สมบูรณ์แบบไม่มีใครเคยทำเช่นนั้น นี่ไม่ใช่โลกที่สมบูรณ์แบบ
  • มันเสนอการล่อลวงที่ผิดปกติสำหรับ subclassers เพื่อตั้งสมมติฐานเกี่ยวกับลำดับของการเรียกเมธอดและเช่น - สมมติฐานดังกล่าวมีแนวโน้มที่จะไม่เสถียรถ้าซุปเปอร์คลาสอาจพัฒนาไปตามกาลเวลา ดูเพิ่มเติมปิ้งขนมปังและหม้อกาแฟของฉันเปรียบเทียบ
  • ชั้นเรียนหนักขึ้น - คุณไม่จำเป็นต้องรู้ว่าซุปเปอร์คลาสของคุณกำลังทำอะไรอยู่ในคอนสตรัคเตอร์หรือหน่วยความจำที่ใช้งานอยู่ ดังนั้นการสร้างวัตถุน้ำหนักเบาที่ไร้เดียงสาจะมีราคาแพงกว่าที่คุณคิดและสิ่งนี้อาจเปลี่ยนแปลงได้เมื่อเวลาผ่านไปหากซุปเปอร์คลาสเปลี่ยนไป
  • มันส่งเสริมให้เกิดการระเบิดของ subclasses เวลาในการโหลดคลาส, หน่วยความจำเพิ่มขึ้น นี่อาจไม่ใช่ปัญหาจนกว่าคุณจะจัดการกับแอปในระดับ NetBeans แต่ที่นั่นเรามีปัญหาจริงเช่นเมนูช้าเนื่องจากการแสดงครั้งแรกของเมนูทำให้เกิดการโหลดคลาสขนาดใหญ่ เราแก้ไขสิ่งนี้โดยการย้ายไปยังไวยากรณ์ที่เปิดเผยมากขึ้นและเทคนิคอื่น ๆ แต่ใช้เวลาในการแก้ไขเช่นกัน
  • มันทำให้ยากที่จะเปลี่ยนแปลงสิ่งต่าง ๆ ในภายหลัง - ถ้าคุณได้เปิดเผยให้ชั้นเรียนการแลกเปลี่ยน superclass จะทำลายคลาสย่อย - เป็นทางเลือกที่เมื่อคุณทำให้รหัสสาธารณะคุณแต่งงานแล้ว ดังนั้นหากคุณไม่ได้เปลี่ยนฟังก์ชั่นการใช้งานจริงเป็นซูเปอร์คลาสของคุณคุณจะมีอิสระมากขึ้นในการเปลี่ยนสิ่งต่าง ๆ ในภายหลังหากคุณใช้แทนที่จะขยายสิ่งที่คุณต้องการ ยกตัวอย่างเช่นการจัดคลาส JPanel - นี่เป็นความผิดปกติ และถ้าคลาสย่อยนั้นเป็นที่สาธารณะคุณจะไม่มีโอกาสได้กลับมาตัดสินใจอีก หากเข้าถึงได้ในรูปแบบของ JComponent getThePanel () คุณยังสามารถทำได้ (คำใบ้: เปิดเผยโมเดลสำหรับส่วนประกอบภายในเป็น API ของคุณ)
  • ลำดับชั้นของวัตถุไม่ได้ปรับขนาด (หรือทำให้ขยายได้ในภายหลังยากกว่าการวางแผนล่วงหน้า) - นี่คือปัญหา "เลเยอร์มากเกินไป" แบบคลาสสิก ฉันจะไปที่ด้านล่างนี้และวิธีการที่รูปแบบ AskTheOracle สามารถแก้ไขได้อย่างไร

...

สิ่งที่ฉันต้องทำถ้าคุณยอมให้รับมรดกซึ่งคุณสามารถเอาไปกับเม็ดเกลือได้:

  • เปิดเผยไม่มีฟิลด์ยกเว้นค่าคงที่
  • วิธีการจะเป็นนามธรรมหรือสุดท้าย
  • ไม่มีวิธีการโทรจากตัวสร้าง superclass

...

ทั้งหมดนี้ใช้กับโครงการขนาดเล็กน้อยกว่าโครงการขนาดใหญ่และชั้นเรียนส่วนตัวน้อยกว่าโครงการสาธารณะ


25

ทำไมถึงชอบแต่งเพลงมากกว่ามรดก?

ดูคำตอบอื่น ๆ

คุณสามารถใช้มรดกได้เมื่อใด

มันมักจะพูดว่าชั้นBarสามารถสืบทอดชั้นFooเมื่อประโยคต่อไปนี้เป็นจริง:

  1. แถบคือ foo

น่าเสียดายที่การทดสอบข้างต้นเพียงอย่างเดียวไม่น่าเชื่อถือ ใช้สิ่งต่อไปนี้แทน:

  1. แถบคือ foo และ
  2. บาร์สามารถทำทุกอย่างที่ foos ทำได้

เพื่อให้แน่ใจว่าการทดสอบครั้งแรกทั้งหมดgettersของFooให้ความรู้สึกในการBar(= คุณสมบัติที่ใช้ร่วมกัน) ในขณะที่การทดสอบที่สองทำให้แน่ใจว่าทุกsettersของFooให้ความรู้สึกในการBar(= การทำงานร่วมกัน)

ตัวอย่างที่ 1: สุนัข -> สัตว์

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

ตัวอย่างที่ 2: วงกลม - / -> วงรี

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

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

คุณควรใช้มรดกเมื่อใด

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

ตามกฎของหัวแม่มือฉันมักจะเลือกสืบทอดมากกว่าองค์ประกอบเมื่อใช้ polymorphic คาดว่าจะพบบ่อยมากในกรณีที่พลังของการจัดส่งแบบไดนามิกสามารถนำไปสู่ ​​API ที่อ่านได้และสง่างามมากขึ้น ตัวอย่างเช่นการมีคลาส polymorphic Widgetในกรอบงาน GUI หรือคลาส polymorphic Nodeในไลบรารี XML อนุญาตให้มี API ซึ่งอ่านได้ง่ายกว่าและใช้งานได้ง่ายกว่าสิ่งที่คุณมีกับการแก้ปัญหาล้วนๆตามองค์ประกอบ

หลักการชดเชย Liskov

วิธีการอื่นที่ใช้ในการพิจารณาว่าการสืบทอดนั้นเป็นไปได้หรือไม่นั้นเรียกว่าหลักการทดแทน Liskov :

ฟังก์ชันที่ใช้พอยน์เตอร์หรือการอ้างอิงไปยังคลาสพื้นฐานต้องสามารถใช้วัตถุของคลาสที่ได้รับโดยไม่ต้องรู้

โดยพื้นฐานแล้วนี่หมายความว่าการสืบทอดเป็นไปได้ถ้าคลาสฐานสามารถใช้ polymorphically ได้ซึ่งฉันเชื่อว่าเทียบเท่ากับการทดสอบของเรา "บาร์เป็นฟูและบาร์สามารถทำทุกอย่างที่ foos สามารถทำได้"


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

@FuzzyLogic เห็นด้วย แต่ในความเป็นจริงโพสต์ของฉันไม่เคยสนับสนุนการใช้องค์ประกอบในกรณีนี้ ฉันแค่บอกว่าปัญหาวงกลมวงรีเป็นตัวอย่างที่ดีว่าทำไม "is-a" ไม่ใช่การทดสอบที่ดีเพียงอย่างเดียวที่จะสรุปได้ว่า Circle ควรมาจาก Ellipse เมื่อเราสรุปได้ว่าจริง ๆ แล้ว Circle ไม่ควรได้รับจาก Ellipse เนื่องจากการละเมิด LSP ตัวเลือกที่เป็นไปได้คือการคว่ำความสัมพันธ์หรือใช้การจัดองค์ประกอบหรือใช้คลาสเทมเพลตหรือใช้การออกแบบที่ซับซ้อนมากขึ้นที่เกี่ยวข้องกับคลาสเพิ่มเติมหรือฟังก์ชันตัวช่วย ฯลฯ ... และการตัดสินใจที่ชัดเจนควรดำเนินการเป็นกรณี ๆ ไป
Boris Dalstein

1
@FuzzyLogic และหากคุณสงสัยเกี่ยวกับสิ่งที่ฉันอยากจะสนับสนุนในกรณีเฉพาะของ Circle-Ellipse: ฉันจะไม่สนับสนุนการนำคลาส Circle มาใช้ ปัญหากับ inverting ความสัมพันธ์ก็คือว่ามันยังเป็นการละเมิด LSP: computeArea(Circle* c) { return pi * square(c->radius()); }จินตนาการฟังก์ชั่น เห็นได้ชัดว่ามันหักถ้าผ่านวงรี (รัศมี) หมายถึงอะไร? วงรีไม่ใช่วงกลมและเป็นเช่นนั้นไม่ควรได้รับจาก Circle
Boris Dalstein

computeArea(Circle *c) { return pi * width * height / 4.0; }ตอนนี้มันเป็นเรื่องธรรมดา
Fuzzy Logic

2
@FuzzyLogic ผมไม่เห็นด้วย: คุณตระหนักว่านี้หมายถึงว่าชนชั้นวงกลมคาดว่าการดำรงอยู่ของชั้นเรียนมาวงรีและให้จึงwidth()และheight()? เกิดอะไรขึ้นถ้าตอนนี้ผู้ใช้ห้องสมุดตัดสินใจที่จะสร้างคลาสอื่นที่เรียกว่า "EggShape" มันควรจะมาจาก "Circle" หรือไม่? ไม่แน่นอน รูปร่างไข่ไม่ใช่วงกลมและวงรีไม่ใช่วงกลมเช่นกันดังนั้นจึงไม่มีใครควรได้รับจาก Circle เนื่องจากมันแบ่ง LSP วิธีการดำเนินการกับคลาส Circle * ทำให้สมมติฐานที่แข็งแกร่งเกี่ยวกับสิ่งที่เป็นวงกลมและการทำลายสมมติฐานเหล่านี้จะนำไปสู่ข้อบกพร่องอย่างแน่นอน
Boris Dalstein

19

การสืบทอดมีประสิทธิภาพมาก แต่คุณไม่สามารถบังคับได้ (ดู: ปัญหาวงกลมวงรี ) หากคุณไม่แน่ใจอย่างแท้จริงเกี่ยวกับความสัมพันธ์แบบย่อย "is-a" จริงคุณควรใช้การแต่งเพลง


15

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

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

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

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


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

1
@ BitMask777: รูปแบบย่อยหลายประเภทเป็นรูปแบบความหลากหลายเพียงรูปแบบหนึ่งรูปแบบอื่นจะเป็นรูปแบบความแปรปรวนคุณไม่ต้องการรับมรดกจากสิ่งนั้น ที่สำคัญยิ่งกว่า: เมื่อพูดถึงการสืบทอดสิ่งหนึ่งหมายถึงการสืบทอดคลาส .ie คุณสามารถมี polymorphism ชนิดย่อยได้โดยมีอินเตอร์เฟสทั่วไปสำหรับหลายคลาสและคุณไม่ได้รับปัญหาเรื่องการสืบทอด
egaga

2
@engaga: ฉันตีความความคิดเห็นของคุณ Inheritance is sometimes useful... That way you can have polymorphismว่าเป็นการเชื่อมโยงอย่างหนักเกี่ยวกับแนวคิดของการสืบทอดและความหลากหลาย (การพิมพ์ย่อยให้ตามบริบท) ความคิดเห็นของฉันมีจุดมุ่งหมายเพื่อชี้ให้เห็นสิ่งที่คุณชี้แจงในความคิดเห็นของคุณ: การสืบทอดไม่ใช่วิธีเดียวที่จะใช้ความหลากหลายและในความเป็นจริงไม่จำเป็นต้องเป็นปัจจัยกำหนดเมื่อตัดสินใจเลือกระหว่างองค์ประกอบและการสืบทอด
BitMask777

15

สมมติว่าเครื่องบินมีเพียงสองส่วน: เครื่องยนต์และปีก
จากนั้นมีสองวิธีในการออกแบบคลาสเครื่องบิน

Class Aircraft extends Engine{
  var wings;
}

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

คลาสฐานอาจEngineทำให้ mutator เปลี่ยน
คุณสมบัติหรือฉันออกแบบใหม่Aircraftเป็น:

Class Aircraft {
  var wings;
  var engine;
}

ตอนนี้ฉันสามารถเปลี่ยนเครื่องยนต์ได้ทันทีเช่นกัน


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

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


7

เมื่อคุณต้องการ "คัดลอก" / เปิดเผยคลาสพื้นฐาน 'API คุณจะใช้การสืบทอด เมื่อคุณต้องการเพียงแค่ "คัดลอก" ฟังก์ชั่นใช้การมอบหมาย

ตัวอย่างหนึ่งของสิ่งนี้: คุณต้องการสร้างสแต็กออกจากรายการ สแต็คมีเพียงป๊อปดันและแอบดู คุณไม่ควรใช้การสืบทอดเนื่องจากคุณไม่ต้องการ push_back, push_front, removeAt, และฟังก์ชันการทำงานอื่น ๆ ใน Stack


7

สองวิธีนี้สามารถอยู่ด้วยกันได้ดีและสนับสนุนซึ่งกันและกันจริง ๆ

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

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

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

ทำไม?

เพราะมรดกเป็นวิธีที่ไม่ดีในการเคลื่อนย้ายข้อมูล

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

ฉันสามารถนึกถึงสามเหตุผลที่แท้จริงสำหรับการใช้การสืบทอด:

  1. คุณมีหลายคลาสที่มีอินเตอร์เฟสเดียวกันและคุณต้องการประหยัดเวลาในการเขียน
  2. คุณต้องใช้คลาสฐานเดียวกันสำหรับแต่ละวัตถุ
  3. คุณต้องแก้ไขตัวแปรส่วนตัวซึ่งไม่สามารถเปิดเผยได้ในทุกกรณี

หากสิ่งเหล่านี้เป็นจริงแสดงว่าจำเป็นต้องใช้การสืบทอด

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

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

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


7

หากต้องการตอบคำถามนี้จากมุมมองที่ต่างออกไปสำหรับโปรแกรมเมอร์รุ่นใหม่:

การสืบทอดมักจะถูกสอนตั้งแต่เนิ่นๆเมื่อเราเรียนรู้การเขียนโปรแกรมเชิงวัตถุดังนั้นมันจึงถูกมองว่าเป็นวิธีการแก้ปัญหาที่ง่าย

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

มันฟังดูยอดเยี่ยม แต่ในทางปฏิบัติมันแทบไม่เคยทำงานเลยด้วยเหตุผลหลายประการ:

  • เราค้นพบว่ามีฟังก์ชั่นอื่น ๆ ที่เราต้องการให้คลาสของเรามี ถ้าวิธีที่เราเพิ่มฟังก์ชันการทำงานให้กับคลาสนั้นผ่านการสืบทอดเราต้องตัดสินใจ - เราจะเพิ่มมันลงในคลาสฐานที่มีอยู่แม้ว่าจะไม่ใช่ทุกคลาสที่สืบทอดมาจากมันต้องการฟังก์ชันนั้นหรือไม่? เราสร้างคลาสฐานใหม่หรือไม่? แต่แล้วคลาสที่สืบทอดมาจากคลาสฐานอื่นอยู่แล้ว?
  • เราค้นพบว่าสำหรับคลาสใดคลาสหนึ่งที่สืบทอดมาจากคลาสฐานของเราเราต้องการให้คลาสฐานทำงานแตกต่างกันเล็กน้อย ดังนั้นตอนนี้เรากลับไปที่คนจรจัดกับคลาสพื้นฐานของเราอาจเพิ่มวิธีเสมือนบางอย่างหรือแย่กว่านั้นคือบางรหัสที่บอกว่า "ถ้าฉันสืบทอด Type A ทำเช่นนี้ แต่ถ้าฉันสืบทอด Type B ทำเช่นนั้น ." มันไม่ดีด้วยเหตุผลมากมาย หนึ่งคือทุกครั้งที่เราเปลี่ยนคลาสพื้นฐานเราจะเปลี่ยนคลาสที่สืบทอดทั้งหมดอย่างมีประสิทธิภาพ ดังนั้นเราจึงเปลี่ยนคลาส A, B, C และ D เพราะเราต้องการพฤติกรรมที่แตกต่างกันเล็กน้อยในคลาส A ระวังอย่างที่เราคิดว่าเราเป็นเราอาจแบ่งหนึ่งในคลาสเหล่านั้นด้วยเหตุผลที่ไม่มีส่วนเกี่ยวข้องกับสิ่งเหล่านั้น ชั้นเรียน
  • เราอาจรู้ว่าทำไมเราจึงตัดสินใจที่จะทำให้คลาสทั้งหมดเหล่านี้สืบทอดจากกัน แต่มันอาจจะไม่เหมาะสมกับคนอื่นที่ต้องรักษารหัสของเรา เราอาจบังคับให้พวกเขาเลือกยาก - ฉันจะทำสิ่งที่น่าเกลียดและยุ่งเหยิงเพื่อทำการเปลี่ยนแปลงที่ฉันต้องการ (ดูหัวข้อย่อยก่อนหน้านี้) หรือฉันจะเขียนสิ่งนี้อีกครั้ง

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

ทันทีที่มีคนอธิบายว่า "ฉันชอบการแต่งเพลงมากกว่ามรดก" ฉันคิดย้อนกลับไปทุกครั้งที่ฉันพยายามที่จะแบ่งปันฟังก์ชั่นการใช้งานระหว่างชั้นเรียนโดยใช้การสืบทอดและตระหนักว่าส่วนใหญ่มันใช้งานไม่ได้

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

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


คลาสนั้นไม่สามารถใช้คลาสพื้นฐานหลายคลาสไม่ได้เป็นภาพสะท้อนที่ไม่ดีต่อการสืบทอด แต่เป็นการสะท้อนที่แย่เมื่อภาษาขาดความสามารถโดยเฉพาะ
iPherian

ในเวลาตั้งแต่เขียนคำตอบนี้ฉันอ่านโพสต์นี้จาก "ลุงบ๊อบ" ซึ่งอยู่ที่ขาดความสามารถ ฉันไม่เคยใช้ภาษาที่ยอมให้มีการสืบทอดหลายครั้ง แต่เมื่อมองย้อนกลับไปคำถามจะถูกแท็ก "ผู้ไม่เชื่อเรื่องภาษา" และคำตอบของฉันถือว่า C # ฉันต้องขยายขอบเขตอันไกลโพ้นของฉัน
Scott Hannen

6

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


6

เมื่อคุณมีis-aความสัมพันธ์ระหว่างสองชั้น (ตัวอย่างเช่นสุนัขสุนัข) คุณไปเป็นมรดก

ในทางตรงกันข้ามเมื่อคุณมี-หรือความสัมพันธ์คำคุณศัพท์ระหว่างสองชั้น (นักเรียนมีหลักสูตร) ​​หรือ (หลักสูตรการศึกษาของครู) คุณเลือกองค์ประกอบ


คุณบอกว่ามรดกและมรดก คุณไม่ได้หมายถึงมรดกและองค์ประกอบ
trevorKirkby

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

5

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

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


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


4

ฉันเห็นด้วยกับ @Pavel เมื่อเขาพูดว่ามีสถานที่สำหรับการแต่งเพลงและมีสถานที่สำหรับการสืบทอด

ฉันคิดว่าควรใช้การรับมรดกหากคำตอบของคุณตอบคำถามเหล่านี้

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

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


4

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

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

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

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

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

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

จากนั้นฉันเขียนชุดจำนวนเต็มเป็นคลาสย่อยของรายการจำนวนเต็ม:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

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

นี่เป็นตัวอย่างคลาสสิกของการใช้มรดกอย่างไม่เหมาะสม ใช้องค์ประกอบในกรณีนี้

(ส่วนจาก: ใช้การสืบทอดอย่างถูกต้อง )


3

กฎของหัวแม่มือที่ฉันเคยได้ยินคือมรดกควรใช้เมื่อความสัมพันธ์และองค์ประกอบ "is-a" ของมันเมื่อ "a-a" ของมัน แม้ว่าฉันจะรู้สึกว่าคุณควรเอนไปทางองค์ประกอบเสมอเพราะมันขจัดความซับซ้อนได้มาก


2

องค์ประกอบ v / s การสืบทอดเป็นเรื่องที่กว้าง ไม่มีคำตอบที่แท้จริงสำหรับสิ่งที่ดีกว่าเพราะฉันคิดว่ามันทั้งหมดขึ้นอยู่กับการออกแบบของระบบ

โดยทั่วไปประเภทของความสัมพันธ์ระหว่างวัตถุให้ข้อมูลที่ดีกว่าในการเลือกหนึ่งในนั้น

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

มันขึ้นอยู่กับความสัมพันธ์เอนทิตี


2

แม้ว่าองค์ประกอบที่เป็นที่ต้องการผมต้องการจะเน้นข้อดีของมรดกและข้อเสียขององค์ประกอบ

ข้อดีของการสืบทอด:

  1. มันสร้างตรรกะ "ความสัมพันธ์ IS A"หากรถยนต์และรถบรรทุกเป็นยานพาหนะสองประเภท(คลาสพื้นฐาน) คลาสสำหรับเด็กคือคลาสพื้นฐาน

    กล่าวคือ

    รถยนต์เป็นยานพาหนะ

    รถบรรทุกเป็นยานพาหนะ

  2. ด้วยการสืบทอดคุณสามารถกำหนด / แก้ไข / ขยายความสามารถได้

    1. คลาสพื้นฐานไม่ให้มีการใช้งานและคลาสย่อยจะต้องแทนที่เมธอดทั้งหมด (นามธรรม) => คุณสามารถทำสัญญาได้
    2. คลาสพื้นฐานให้การใช้งานเริ่มต้นและคลาสย่อยสามารถเปลี่ยนลักษณะการทำงาน => คุณสามารถกำหนดสัญญาใหม่ได้
    3. คลาสย่อยเพิ่มส่วนขยายให้กับการใช้คลาสพื้นฐานโดยเรียก super.methodName () เป็นคำสั่งแรก => คุณสามารถขยายสัญญา
    4. คลาสพื้นฐานกำหนดโครงสร้างของอัลกอริทึมและคลาสย่อยจะแทนที่ส่วนหนึ่งของอัลกอริทึม => คุณสามารถใช้Template_methodโดยไม่มีการเปลี่ยนแปลงโครงกระดูกของคลาสพื้นฐาน

ข้อเสียขององค์ประกอบ:

  1. ในการสืบทอดคลาสย่อยสามารถเรียกใช้เมธอดคลาสพื้นฐานโดยตรงแม้ว่าจะไม่ได้ใช้เมธอดคลาสคลาสเนื่องจากIS Aความสัมพันธ์หากคุณใช้การประพันธ์คุณต้องเพิ่มวิธีในคลาสคอนเทนเนอร์เพื่อแสดงคลาส API ที่มีอยู่

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

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

ฉันคิดว่ามันไม่ตอบคำถาม
almanegra

คุณสามารถดูคำถาม OP อีกครั้ง ฉันได้กล่าวถึงแล้ว: การแลกเปลี่ยนในแต่ละวิธีมีอะไรบ้าง
Ravindra babu

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

ข้อดีและข้อเสียให้การแลกเปลี่ยนตั้งแต่ข้อดีของการสืบทอดเป็นข้อเสียขององค์ประกอบและข้อเสียขององค์ประกอบคือข้อดีของการสืบทอด
Ravindra babu

1

อย่างที่หลายคนบอกว่าฉันจะเริ่มด้วยการตรวจสอบก่อนว่ามีความสัมพันธ์แบบ "is-a" หรือไม่ หากมีอยู่ฉันมักจะตรวจสอบสิ่งต่อไปนี้:

ไม่ว่าจะเป็นชั้นฐานสามารถยกตัวอย่าง นั่นคือไม่ว่าจะเป็นชั้นฐานสามารถไม่ใช่นามธรรม ถ้าเป็นนามธรรมฉันมักจะชอบองค์ประกอบ

เช่น 1. บัญชีเป็นพนักงาน แต่ฉันจะไม่ใช้การสืบทอดเนื่องจากออบเจกต์พนักงานสามารถสร้างอินสแตนซ์ได้

เช่น 2. หนังสือคือ SellingItem SellingItem ไม่สามารถสร้างอินสแตนซ์ได้ - เป็นแนวคิดที่เป็นนามธรรม ดังนั้นฉันจะใช้ inheritacne SellingItem เป็นคลาสพื้นฐานที่เป็นนามธรรม (หรืออินเตอร์เฟสใน C #)

คุณคิดอย่างไรเกี่ยวกับวิธีการนี้

นอกจากนี้ฉันสนับสนุน @anon คำตอบในเหตุใดจึงใช้การสืบทอดเลย

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

@MatthieuM พูดว่าใน/software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

ปัญหาเกี่ยวกับการสืบทอดคือสามารถใช้สำหรับวัตถุประสงค์มุมฉากสองจุด:

อินเตอร์เฟส (สำหรับ polymorphism)

การใช้งาน (สำหรับการนำโค้ดกลับมาใช้)

REFERENCE

  1. การออกแบบระดับไหนดีกว่ากัน?
  2. มรดกกับการรวม

1
ฉันไม่แน่ใจว่าทำไม 'ชั้นฐานเป็นนามธรรม' ตัวเลขในการอภิปราย .. LSP: ฟังก์ชั่นทั้งหมดที่ทำงานกับสุนัขจะทำงานได้หรือไม่หากมีการส่งผ่านวัตถุพุดเดิ้ล? ถ้าใช่แล้วพุดเดิ้ลสามารถทดแทนสุนัขได้และสามารถสืบทอดมาจากสุนัขได้
Gishu

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

1
ได้อ่าน WCF เมื่อเร็ว ๆ นี้ ตัวอย่างใน. net framework คือ SynchronizationContext (base + สามารถเป็นอินสแตนซ์) ซึ่งคิวทำงานบนเธรด ThreadPool อนุพันธ์ ได้แก่ WinFormsSyncContext (คิวเข้าสู่ UI Thread) และ DispatcherSyncContext (คิวเข้าสู่ WPF Dispatcher)
Gishu

@Gishu ขอบคุณ อย่างไรก็ตามมันจะมีประโยชน์มากขึ้นถ้าคุณสามารถจัดทำสถานการณ์จำลองตามโดเมนธนาคาร, โดเมน HR, โดเมนค้าปลีกหรือโดเมนยอดนิยมอื่น ๆ
LCJ

1
ขอโทษ ฉันไม่คุ้นเคยกับโดเมนเหล่านั้นอีกตัวอย่างหนึ่งถ้าอันก่อนหน้านี้เป็นป้านเกินไปคือคลาสควบคุมใน Winforms / WPF การควบคุมฐาน / ทั่วไปสามารถยกตัวอย่าง การดัดแปลงรวมถึง Listboxes, Textboxes เป็นต้นตอนนี้ฉันคิดแล้วรูปแบบการออกแบบมัณฑนากรเป็นตัวอย่างที่ดีของ IMHO และมีประโยชน์เช่นกัน มัณฑนากรมาจากวัตถุที่ไม่ใช่นามธรรมที่ต้องการห่อ / ตกแต่ง
Gishu

1

ฉันไม่เห็นใครพูดถึงปัญหาเพชรซึ่งอาจเกิดขึ้นกับมรดก

ในภาพรวมหากคลาส B และ C สืบทอด A และทั้งวิธีการแทนที่ X และคลาส D ที่สี่สืบทอดมาจากทั้ง B และ C และไม่แทนที่ X ซึ่งการใช้งาน XD แบบใดที่ควรใช้?

Wikipediaเสนอภาพรวมที่ดีของหัวข้อที่กล่าวถึงในคำถามนี้


1
D สืบทอด B และ C ไม่ใช่ A ถ้าเป็นเช่นนั้นมันจะใช้การดำเนินการของ X ที่อยู่ในคลาส A.
fabricio

1
@fabricio: ขอบคุณฉันแก้ไขข้อความ โดยวิธีการสถานการณ์ดังกล่าวไม่สามารถเกิดขึ้นในภาษาที่ไม่อนุญาตให้มีการสืบทอดหลายคลาสฉันถูกต้องหรือไม่
Veverke

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