เหตุใด Square ที่สืบทอดจากสี่เหลี่ยมผืนผ้าจึงมีปัญหาหากเราแทนที่เมธอด SetWidth และ SetHeight


105

หาก Square เป็นรูปแบบสี่เหลี่ยมผืนผ้ากว่าเหตุใด Square จึงไม่สามารถสืบทอดจากสี่เหลี่ยมผืนผ้าได้ หรือทำไมมันถึงออกแบบไม่ดี?

ฉันเคยได้ยินคนพูดว่า:

หากคุณกำหนดให้ Square ได้รับมาจากสี่เหลี่ยมผืนผ้าดังนั้น Square ควรใช้งานได้ทุกที่ที่คุณคาดว่าสี่เหลี่ยมผืนผ้า

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

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

บางคนสามารถอธิบายข้อโต้แย้งข้างต้นได้หรือไม่ อีกครั้งถ้าเราใช้วิธี SetWidth และ SetHeight มากเกินไปใน Square มันจะไม่แก้ไขปัญหานี้หรือไม่

ฉันเคยได้ยิน / อ่านด้วย:

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

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


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

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

7
ฉันคิดว่าคำถามที่ดีกว่าคือWhy do we even need Squareอะไร? มันเหมือนกับมีปากกาสองด้าม ปากกาสีน้ำเงินหนึ่งอันและปากกาสีแดงสีน้ำเงิน, สีเหลืองหรือสีเขียว ปากกาสีฟ้าซ้ำซ้อน - มากขึ้นดังนั้นในกรณีของสี่เหลี่ยมจัตุรัสเนื่องจากไม่มีประโยชน์ด้านต้นทุน
Gusdor

2
@eBusiness ความเป็นนามธรรมคือสิ่งที่ทำให้มันเป็นคำถามการเรียนรู้ที่ดี สิ่งสำคัญคือต้องสามารถทราบได้ว่าการใช้ชนิดย่อยใดที่ไม่ดีในกรณีการใช้งานเฉพาะ
Doval

5
@Cululhu ไม่จริง การพิมพ์ย่อยนั้นเกี่ยวกับพฤติกรรมและสแควร์ที่ไม่แน่นอนจะไม่ทำงานเหมือนรูปสี่เหลี่ยมผืนผ้าที่ไม่แน่นอน นี่คือเหตุผลที่คำอุปมา "เป็น ... " ไม่ดี
Doval

คำตอบ:


189

โดยพื้นฐานแล้วเราต้องการให้สิ่งต่าง ๆ ประพฤติอย่างสมเหตุสมผล

พิจารณาปัญหาต่อไปนี้:

ฉันได้รับสี่เหลี่ยมกลุ่มหนึ่งและฉันต้องการเพิ่มพื้นที่ของพวกเขา 10% สิ่งที่ฉันทำคือฉันตั้งความยาวของสี่เหลี่ยมผืนผ้าเป็น 1.1 เท่าของที่เคยเป็นมาก่อน

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

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

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

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

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

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

อัลเฟรดมีความสุขกับทักษะการแฮ็ก uber ของเขาและจิลล์ลงนามว่าข้อผิดพลาดได้รับการแก้ไข

เดือนถัดไปไม่มีใครได้รับค่าจ้างเพราะการบัญชีนั้นขึ้นอยู่กับความสามารถในการส่งผ่านกำลังสองไปยังIncreaseRectangleSizeByTenPercentวิธีการและเพิ่มพื้นที่ 21% ทั้ง บริษัท เข้าสู่โหมด "ลำดับความสำคัญ 1 แก้ไขข้อบกพร่อง" เพื่อติดตามแหล่งที่มาของปัญหา พวกเขาติดตามปัญหาเพื่อแก้ไขปัญหาของ Alfred พวกเขารู้ว่าพวกเขาต้องทำให้ทั้งบัญชีและการโฆษณามีความสุข ดังนั้นพวกเขาจึงแก้ไขปัญหาโดยการระบุผู้ใช้ด้วยวิธีการเรียกเช่น:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

และอื่น ๆ และอื่น ๆ.

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

มีสองวิธีที่เป็นจริงในการแก้ไขปัญหานี้

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

วิธีที่สองคือการทำลายห่วงโซ่มรดกระหว่างสี่เหลี่ยมและสี่เหลี่ยม หากสี่เหลี่ยมหมายถึงการมีSideLengthคุณสมบัติเดี่ยวและสี่เหลี่ยมมีLengthและWidthคุณสมบัติและไม่มีการสืบทอดมันเป็นไปไม่ได้ที่จะทำลายสิ่งต่าง ๆ โดยไม่ได้ตั้งใจโดยคาดหวังว่ารูปสี่เหลี่ยมผืนผ้าและรับสี่เหลี่ยม ในคำศัพท์ C # คุณสามารถsealเรียนรูปสี่เหลี่ยมผืนผ้าของคุณซึ่งทำให้แน่ใจว่ารูปสี่เหลี่ยมผืนผ้าทั้งหมดที่คุณได้รับนั้นเป็นรูปสี่เหลี่ยมจริง ๆ

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

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


7
Liskov เป็นสิ่งหนึ่งและพื้นที่จัดเก็บก็เป็นอีกปัญหาหนึ่ง ในการนำไปใช้งานส่วนใหญ่อินสแตนซ์แบบสแควร์ที่รับมาจากสี่เหลี่ยมผืนผ้าจะต้องใช้พื้นที่ในการจัดเก็บสองมิติแม้ว่าจำเป็นต้องใช้เพียงมิติเดียว
el.pescado

29
การใช้เรื่องราวที่ยอดเยี่ยมเพื่อแสดงให้เห็นถึงจุด
Rory Hunter

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

33
@ ROYT: ทำไมสี่เหลี่ยมผืนผ้าควรรู้วิธีตั้งค่าพื้นที่ของมัน นั่นคือคุณสมบัติที่ได้มาทั้งหมดจากความยาวและความกว้าง และมากไปยังจุดซึ่งควรเปลี่ยนขนาด - ความยาวความกว้างหรือทั้งสองอย่าง?
cHao

32
@Roy T. มันเป็นเรื่องดีมากที่จะบอกว่าคุณได้แก้ปัญหาต่างกัน แต่ความจริงก็คือนี่เป็นตัวอย่าง - แม้ว่าจะง่ายขึ้น - ในสถานการณ์จริงที่นักพัฒนาต้องเผชิญในชีวิตประจำวันเมื่อรักษาผลิตภัณฑ์ดั้งเดิม และแม้ว่าคุณจะใช้วิธีการนี้ แต่ก็จะไม่หยุดผู้สืบทอดที่ละเมิด LSP และแนะนำบั๊กที่คล้ายกับอันนี้ นี่คือเหตุผลที่ปิดผนึกทุกคลาสใน. NET Framework
Stephen

30

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

ปัญหาเริ่มต้นเมื่อคุณเพิ่มความสามารถในการปรับเปลี่ยนวัตถุ หรือจริงๆ - เมื่อคุณเริ่มส่งผ่านข้อโต้แย้งไปยังวัตถุไม่ใช่แค่อ่าน getters คุณสมบัติ

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

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

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

มันเป็นเรื่องของการแปรปรวนร่วมกับการแปรปรวน

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

  • ส่งคืนเฉพาะค่าที่ซูเปอร์คลาสจะส่งคืนนั่นคือประเภทการส่งคืนจะต้องเป็นประเภทย่อยของประเภทการส่งคืนเมธอดของซูเปอร์คลาส ผลตอบแทนเป็นตัวแปรร่วม
  • ยอมรับค่าทั้งหมดที่ supertype ยอมรับ - นั่นคือประเภทอาร์กิวเมนต์ต้องเป็น supertype ของประเภทอาร์กิวเมนต์ของ superclass อาร์กิวเมนต์เป็นตัวแปรที่ตรงกันข้าม

ดังนั้นกลับไปที่ Rectangle และ Square: การที่ Square สามารถเป็นคลาสย่อยของ Rectangle ได้หรือไม่นั้นขึ้นอยู่กับวิธีการที่ Rectangle มีอยู่

หากสี่เหลี่ยมผืนผ้ามีตัวตั้งค่าสำหรับความกว้างและความสูงส่วนบุคคลสี่เหลี่ยมจะไม่ทำให้คลาสย่อยดี

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

หากคุณออกแบบ Square และ Rectangle ของคุณให้ใช้งานร่วมกันได้มันอาจจะทำงานได้ แต่ควรจะพัฒนาร่วมกันมิฉะนั้นฉันจะเดิมพันว่ามันจะไม่ทำงาน


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

11
ฉันพบสิ่งที่น่าสนใจแม้ว่าจะเป็น "ไม่เกี่ยวข้องอย่างเห็นได้ชัด"
Jesvin Jose

14
@gnat ฉันขอเถียงว่ามันเกี่ยวข้องเพราะคุณค่าที่แท้จริงของคำถามคือการรับรู้เมื่อมีความสัมพันธ์ของการพิมพ์ย่อยที่ถูกต้องระหว่างสองประเภท ขึ้นอยู่กับการดำเนินการของ supertype ที่ประกาศดังนั้นมันจึงคุ้มค่าที่จะชี้ให้เห็นว่าปัญหาจะหายไปหากวิธีการกลายพันธุ์หายไป
Doval

1
@gnat ยัง setters เป็นmutatorsดังนั้น LRN เป็นหลักว่า"อย่าทำอย่างนั้นและก็ไม่ได้เป็นปัญหา." ฉันบังเอิญเห็นด้วยกับสิ่งที่ไม่สามารถเปลี่ยนแปลงได้สำหรับประเภทง่าย ๆ แต่คุณได้จุดที่ดี: สำหรับวัตถุที่ซับซ้อนปัญหานั้นไม่ง่ายนัก
Patrick M

1
พิจารณาด้วยวิธีนี้พฤติกรรมที่รับประกันโดยคลาส 'สี่เหลี่ยมผืนผ้า' คืออะไร ที่คุณสามารถเปลี่ยนความกว้างและความสูงอิสระของกันและกัน (เช่น setWidth และ setHeight) วิธีการ ตอนนี้ถ้า Square มาจากสี่เหลี่ยมผืนผ้า Square ต้องรับประกันพฤติกรรมนี้ เนื่องจากสแควร์ไม่สามารถรับประกันพฤติกรรมนี้มันเป็นมรดกที่ไม่ดี อย่างไรก็ตามหากเมธอด setWidth / setHeight ถูกลบออกจากคลาสสี่เหลี่ยมผืนผ้าดังนั้นจึงไม่มีพฤติกรรมดังกล่าวและด้วยเหตุนี้คุณจึงสามารถสืบทอดคลาสสแควร์จากสี่เหลี่ยมผืนผ้าได้
Nitin Bhide

17

มีคำตอบที่ดีมากมายที่นี่; โดยเฉพาะอย่างยิ่งคำตอบของสตีเฟ่นทำหน้าที่ได้ดีในการอธิบายว่าทำไมการละเมิดหลักการทดแทนทำให้เกิดความขัดแย้งในโลกแห่งความเป็นจริงระหว่างทีม

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

มีปัญหาเพิ่มเติมเกี่ยวกับ square-is-a-special-of-สี่เหลี่ยมผืนผ้าซึ่งไม่ค่อยมีใครพูดถึงและนั่นคือ: ทำไมเราถึงหยุดด้วยสี่เหลี่ยมและสี่เหลี่ยม ? หากเรายินดีที่จะพูดว่าสี่เหลี่ยมเป็นสี่เหลี่ยมชนิดพิเศษเราก็ควรพูดเช่นกัน:

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

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


3
หากวัตถุรูปทรงจะไม่เปลี่ยนรูปจากนั้นหนึ่งอาจมีIShapeประเภทซึ่งรวมถึงกรอบและสามารถวาด, ปรับขนาดและต่อเนื่องและมีชนิดย่อยที่มีวิธีการที่จะรายงานจำนวนของจุดและวิธีการที่จะกลับมาอีกด้วยIPolygon IEnumerable<Point>หนึ่งในนั้นอาจมีIQuadrilateralชนิดย่อยซึ่งเกิดขึ้นจากIPolygon, IRhombusและIRectangleสืบทอดมาจากที่และISquareเป็นผลมาจากและIRhombus IRectangleความไม่แน่นอนจะทำให้ทุกอย่างออกไปนอกหน้าต่างและการสืบทอดหลายอย่างไม่สามารถใช้งานกับคลาสได้ แต่ฉันคิดว่ามันใช้ได้ดีกับส่วนต่อประสานที่ไม่เปลี่ยนแปลง
supercat

ฉันไม่เห็นด้วยกับ Eric ที่นี่อย่างมีประสิทธิภาพ (ไม่เพียงพอสำหรับ -1 แม้ว่า!) ความสัมพันธ์เหล่านั้นเกี่ยวข้อง (อาจ) เกี่ยวข้องกับ @supercat ที่กล่าวถึง; เป็นเพียงปัญหาของ YAGNI: คุณไม่ได้ใช้งานจนกว่าคุณต้องการ
Mark Hurd

คำตอบที่ดีมาก! ควรจะสูงกว่า
andrew.fox

1
@ MarkHurd - ไม่ใช่ปัญหาของ YAGNI: ลำดับชั้นตามการสืบทอดที่เสนอมีรูปร่างเหมือนอนุกรมวิธานที่อธิบาย แต่ไม่สามารถเขียนเพื่อรับประกันความสัมพันธ์ที่กำหนดไว้ จะIRhombusรับประกันได้อย่างไรว่าผลPointตอบแทนทั้งหมดจากการEnumerable<Point>กำหนดโดยIPolygonสอดคล้องกับขอบของความยาวเท่ากัน? เนื่องจากการใช้IRhombusอินเทอร์เฟซเพียงอย่างเดียวไม่รับประกันว่าวัตถุที่เป็นรูปธรรมจะเป็นรูปสี่เหลี่ยมขนมเปียกปูนการสืบทอดจึงไม่สามารถตอบได้
A. Rager

14

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

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

ปัจจัยที่สำคัญที่นี่คือความไม่แน่นอน รูปร่างสามารถเปลี่ยนแปลงได้หรือไม่เมื่อมันถูกสร้างขึ้น?

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

  • ไม่เปลี่ยนรูป: ถ้ารูปร่างไม่สามารถเปลี่ยนแปลงได้เมื่อสร้างแล้ววัตถุรูปสี่เหลี่ยมจะต้องปฏิบัติตามสัญญาสี่เหลี่ยมผืนผ้าเสมอ สแควร์สามารถมีความสัมพันธ์กับสี่เหลี่ยม

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


1
This is one of those cases where the real world is not able to be modeled in a computer 100%. ทำไมเป็นเช่นนั้น เรายังสามารถมีรูปแบบการทำงานของสี่เหลี่ยมและสี่เหลี่ยมผืนผ้า ผลที่ตามมาคือเราต้องมองหาโครงสร้างที่เรียบง่ายกว่าเพื่อนามธรรมเหนือวัตถุทั้งสองนั้น
Simon Bergot

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

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

ขออภัยที่มาสายและฉันเหนื่อยมากเมื่อฉันเขียนคำตอบ ฉันเขียนมันใหม่เพื่อพูดในสิ่งที่ฉันหมายถึงจริงๆปมของความไม่แน่นอนคืออะไร

13

และทำไม Square ถึงใช้งานได้ทุกที่ที่คุณคาดหวังสี่เหลี่ยมผืนผ้า?

เพราะนั่นเป็นส่วนหนึ่งของความหมายของประเภทย่อย (ดูเพิ่มเติมที่: หลักการทดแทน Liskov) คุณสามารถทำได้ต้องทำสิ่งนี้:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

คุณทำสิ่งนี้ตลอดเวลา (บางครั้งก็ยิ่งมีนัยยะ) เมื่อใช้ OOP

และถ้าเราขี่เมธอด SetWidth และ SetHeight ไปที่ Square มากกว่าเหตุใดจึงมีปัญหา

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


ในหลายภาษาคุณไม่จำเป็นต้องมีRect r = s;สายคุณสามารถทำได้doSomethingWith(s)และรันไทม์จะใช้การเรียกใด ๆsเพื่อแก้ไขSquareวิธีการเสมือนใด ๆ
Patrick M

1
@PatrickM คุณไม่จำเป็นต้องใช้มันในภาษาใด ๆ ที่มี subtyping ฉันรวมบรรทัดนั้นไว้เพื่ออธิบายอย่างชัดเจน

ดังนั้นแทนที่setWidthและsetHeightเปลี่ยนทั้งความกว้างและความสูง
ApproachDarknessFish

@ValekHalfHeart นั่นคือตัวเลือกที่ฉันกำลังพิจารณาอย่างแม่นยำ

7
@ValekHalfHeart: นั่นเป็นการละเมิดหลักการแทนที่ Liskov ที่จะตามมาหลอกหลอนคุณและทำให้คุณใช้เวลาหลายคืนที่ไม่หลับพยายามที่จะหาจุดบกพร่องแปลก ๆ อีกสองปีต่อมาเมื่อคุณลืมว่ารหัสควรทำงานอย่างไร
Jan Hudec

9

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

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

สี่เหลี่ยมด้านขนานเป็นรูปสี่เหลี่ยมขนมเปียกปูนที่มีด้านคู่ขนานกันสองคู่ มันมีมุมที่สอดคล้องกันสองคู่ ไม่ยากที่จะจินตนาการถึงวัตถุรูปสี่เหลี่ยมด้านขนานตามเส้นเหล่านี้:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

ทำไมทั้งสองฟังก์ชั่นจึงแนะนำบั๊กในรูปสี่เหลี่ยมผืนผ้า? ปัญหาคือคุณไม่สามารถเปลี่ยนมุมในสี่เหลี่ยมผืนผ้าได้ : พวกมันถูกนิยามว่าเป็น 90 องศาเสมอดังนั้นอินเทอร์เฟซนี้จึงใช้งานไม่ได้กับสี่เหลี่ยมผืนผ้าที่สืบทอดมาจาก Parallelogram ถ้าฉันสลับสี่เหลี่ยมผืนผ้าเป็นรหัสที่คาดว่าจะเป็นสี่เหลี่ยมด้านขนานและรหัสนั้นพยายามที่จะเปลี่ยนมุมมันจะเป็นข้อบกพร่องอย่างแน่นอน เราได้ทำสิ่งที่เขียนได้ในคลาสย่อยและทำให้เป็นแบบอ่านอย่างเดียวและนั่นเป็นการละเมิด Liskov

ทีนี้สิ่งนี้ใช้กับสี่เหลี่ยมและสี่เหลี่ยมได้อย่างไร?

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

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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

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


8

การพิมพ์ย่อยนั้นเกี่ยวกับพฤติกรรม

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

ตัวอย่าง:

พิจารณาชุดของตัวเลขจริง ในภาษาใด ๆ เราสามารถคาดหวังให้พวกเขาให้การสนับสนุนการดำเนินงาน+, -, และ* /ตอนนี้ให้พิจารณาชุดของจำนวนเต็มบวก ({1, 2, 3, ... }) เห็นได้ชัดว่าจำนวนเต็มบวกทุกตัวก็เป็นจำนวนจริงเช่นกัน แต่ชนิดของจำนวนเต็มบวกเป็นชนิดย่อยของชนิดของจำนวนจริงหรือไม่ ลองดูการดำเนินการทั้งสี่และดูว่าจำนวนเต็มบวกมีพฤติกรรมเหมือนกับตัวเลขจริงหรือไม่:

  • +: เราสามารถเพิ่มจำนวนเต็มบวกได้โดยไม่มีปัญหา
  • -: การลบจำนวนเต็มบวกทั้งหมดไม่ส่งผลให้มีจำนวนเต็มบวก 3 - 5เช่น
  • *: เราสามารถคูณจำนวนเต็มบวกได้โดยไม่มีปัญหา
  • /: เราไม่สามารถหารจำนวนเต็มบวกเสมอและรับจำนวนเต็มบวกได้ 5 / 3เช่น

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

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

เหตุใดเรื่องนี้

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

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


6

หาก Square เป็นรูปแบบของสี่เหลี่ยมผืนผ้ามากกว่าเหตุใดจึงไม่ให้ Square สืบทอดมาจากรูปสี่เหลี่ยมผืนผ้าได้ หรือทำไมมันถึงออกแบบไม่ดี?

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

แน่นอนคนส่วนใหญ่เรียนรู้ว่าในโรงเรียนประถมศึกษาและดูเหมือนจะชัดเจน สี่เหลี่ยมผืนผ้าเป็นรูปร่าง 4 ด้านที่มีมุม 90 องศาและสี่เหลี่ยมจัตุรัสตอบสนองคุณสมบัติเหล่านั้นทั้งหมด งั้นสี่เหลี่ยมจตุรัสก็ไม่ได้เหรอ?

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

ดังนั้นก่อนที่คุณจะพูดว่า "ตารางเป็นประเภทของรูปสี่เหลี่ยมผืนผ้า" คุณต้องถามตัวเองว่านี่คือเกณฑ์ที่ผมดูแลเกี่ยวกับการตาม

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

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

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

ในกรณีนี้อย่าสร้างโมเดลขึ้นมา ทำไมคุณ มันทำให้คุณไม่มีอะไรนอกจากข้อ จำกัด ที่ไม่จำเป็น

มันจะใช้งานได้เฉพาะเมื่อเราสร้างวัตถุสแควร์และถ้าเราแทนที่วิธี SetWidth และ SetHeight สำหรับสแควร์กว่าทำไมจะมีปัญหาใด ๆ ?

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

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

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

จำนวนมากของคำตอบของคำถามนี้มุ่งเน้นในการย่อยการพิมพ์เป็นเรื่องการจัดกลุ่มพฤติกรรม'ทำให้พวกเขากฎ

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


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

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

หรือใช้วิธีอื่น: อย่าพยายามช้อนงอ นั่นเป็นไปไม่ได้ แทนที่จะพยายามตระหนักถึงความจริงเท่านั้นไม่มีช้อน :-)
Cormac Mulhall

1
การมีรูปSquareแบบที่ไม่เปลี่ยนรูปซึ่งสืบทอดมาจากรูปRectnagleแบบที่ไม่เปลี่ยนรูปอาจมีประโยชน์หากมีการดำเนินการบางประเภทที่สามารถทำได้เฉพาะกับรูปสี่เหลี่ยม เป็นตัวอย่างที่เป็นจริงของแนวคิดพิจารณาReadableMatrixประเภท [ฐานพิมพ์อาร์เรย์สี่เหลี่ยมซึ่งอาจถูกเก็บไว้ในรูปแบบต่าง ๆ รวมทั้งกระจัดกระจาย] และComputeDeterminantวิธีการ มันอาจจะทำให้ความรู้สึกที่มีComputeDeterminantการทำงานเฉพาะกับReadableSquareMatrixชนิดที่ได้มาจากการReadableMatrixที่ผมจะคิดว่าจะเป็นตัวอย่างของหนึ่งอันเกิดจากSquare Rectangle
supercat

5

หาก Square เป็นรูปแบบของสี่เหลี่ยมผืนผ้ามากกว่าเหตุใดจึงไม่ให้ Square สืบทอดมาจากรูปสี่เหลี่ยมผืนผ้าได้

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

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

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

  • กำหนด Square เป็นคลาสพื้นฐานพร้อมwidthพารามิเตอร์และresize(double factor)ปรับขนาดความกว้างตามปัจจัยที่กำหนด
  • กำหนดคลาส Rectangle และคลาสย่อยของ Square เนื่องจากมันเพิ่มแอ็ตทริบิวต์อื่นheightและแทนที่การresizeทำงานของมันซึ่งเรียกใช้super.resizeแล้วปรับขนาดความสูงตามปัจจัยที่กำหนด

จากมุมมองการเขียนโปรแกรมไม่มีอะไรใน Square ที่สี่เหลี่ยมผืนผ้าไม่มี ไม่มีความรู้สึกในการสร้าง Square ให้เป็นคลาสย่อยของสี่เหลี่ยมผืนผ้า


+1 เพียงเพราะสแควร์เป็นสี่เหลี่ยมพิเศษในวิชาคณิตศาสตร์ไม่ได้หมายความว่ามันจะเหมือนกันใน OO
Lovis

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

4

เพราะโดย LSP การสร้างความสัมพันธ์การสืบทอดระหว่างสองและการเอาชนะsetWidthและsetHeightเพื่อให้แน่ใจว่าสแควร์มีทั้งการแนะนำเหมือนกันทำให้เกิดความสับสนและพฤติกรรมที่ไม่ใช้งานง่าย ช่วยบอกว่าเรามีรหัส:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

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

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


2

อะไร LSP บอกว่าเป็นอะไรที่สืบทอดจากจะต้องเป็นRectangle Rectangleนั่นคือมันควรทำทุกอย่างที่Rectangleทำได้

อาจเป็นเอกสารสำหรับRectangleเขียนว่าพฤติกรรมของRectangleชื่อrเป็นดังนี้:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Rectangleหากสแควร์ของคุณไม่ได้ว่าพฤติกรรมเดียวกันแล้วมันไม่ประพฤติเช่น ดังนั้น LSP Rectangleบอกว่ามันจะต้องไม่ได้รับมรดกจาก ภาษาไม่สามารถบังคับใช้กฎนี้ได้เพราะไม่สามารถหยุดคุณทำสิ่งที่ผิดในวิธีการแทนที่ แต่นั่นไม่ได้หมายความว่า "ใช้ได้เพราะภาษาให้ฉันแทนที่วิธี" เป็นอาร์กิวเมนต์ที่น่าเชื่อสำหรับการทำมัน!

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


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

@gnat: คุณต้องการให้ฉันแก้ไขคำตอบอื่น ๆ ลงไปประมาณความกะทัดรัดนี้หรือไม่? ;-) ฉันไม่คิดว่าฉันจะทำได้โดยไม่ลบคะแนนที่ผู้ตอบคนอื่นรู้สึกว่าจำเป็นต้องตอบคำถามและฉันรู้สึกว่าไม่ได้
Steve Jessop


1

ชนิดย่อยและโดยการขยายการเขียนโปรแกรม OO มักขึ้นอยู่กับหลักการทดแทน Liskov ว่าค่าใด ๆ ของประเภท A สามารถใช้เมื่อจำเป็นต้องใช้ B ถ้า A <= B นี่เป็นความจริงในสถาปัตยกรรม OO เช่น มันจะถือว่าคลาสย่อยทั้งหมดจะมีคุณสมบัตินี้ (และหากไม่มีประเภทย่อยจะเป็นรถและจำเป็นต้องได้รับการแก้ไข)

อย่างไรก็ตามปรากฎว่าหลักการนี้ไม่เหมือนจริง / ไม่เป็นตัวแทนของรหัสส่วนใหญ่หรือเป็นไปไม่ได้แน่นอนที่จะพึงพอใจ (ในกรณีที่ไม่น่ารำคาญ)! ปัญหานี้รู้จักกันในชื่อปัญหาปัญหารูปสี่เหลี่ยมจัตุรัสหรือปัญหาวงกลม - วงรี ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ) เป็นตัวอย่างหนึ่งที่มีชื่อเสียงว่าเป็นเรื่องยากเพียงใดที่จะทำให้สำเร็จ

โปรดทราบว่าเราสามารถใช้ Squares and Rectangles ที่เทียบเท่ากับการสังเกตการณ์ได้มากขึ้นเรื่อย ๆ แต่โดยการทิ้งฟังก์ชันการทำงานมากขึ้นเรื่อย ๆ จนกระทั่งความแตกต่างนั้นไม่มีประโยชน์

ตัวอย่างเช่นดูhttp://okmij.org/ftp/Computation/Subtyping/

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