ฉันได้ติดตามคำถามที่ได้รับการโหวตอย่างสูงเกี่ยวกับการละเมิดหลักการทดแทน Liskov ฉันรู้ว่าหลักการชดเชย Liskov คืออะไร แต่สิ่งที่ยังไม่ชัดเจนในใจของฉันคือสิ่งที่อาจผิดพลาดได้หากฉันในฐานะนักพัฒนาไม่ได้คิดถึงหลักการในขณะที่เขียนโค้ดเชิงวัตถุ
ฉันได้ติดตามคำถามที่ได้รับการโหวตอย่างสูงเกี่ยวกับการละเมิดหลักการทดแทน Liskov ฉันรู้ว่าหลักการชดเชย Liskov คืออะไร แต่สิ่งที่ยังไม่ชัดเจนในใจของฉันคือสิ่งที่อาจผิดพลาดได้หากฉันในฐานะนักพัฒนาไม่ได้คิดถึงหลักการในขณะที่เขียนโค้ดเชิงวัตถุ
คำตอบ:
ฉันคิดว่ามันพูดได้ดีมากในคำถามนั้นซึ่งเป็นหนึ่งในเหตุผลที่ได้รับการโหวตอย่างสูง
ตอนนี้เมื่อเรียก Close () บนงานมีโอกาสที่การโทรจะล้มเหลวหากเป็น ProjectTask ที่มีสถานะเริ่มต้นเมื่อไม่เป็นถ้าเป็นภารกิจพื้นฐาน
ลองนึกภาพถ้าคุณจะ:
public void ProcessTaskAndClose(Task taskToProcess)
{
taskToProcess.Execute();
taskToProcess.DateProcessed = DateTime.Now;
taskToProcess.Close();
}
ในวิธีนี้บางครั้งการเรียก. ปิด () จะระเบิดดังนั้นตอนนี้ขึ้นอยู่กับการใช้งานจริงของประเภทที่ได้รับมาคุณต้องเปลี่ยนวิธีการทำงานของวิธีการนี้จากวิธีการเขียนวิธีนี้หากงานไม่มีประเภทย่อยที่อาจเป็น มอบให้กับวิธีการนี้
เนื่องจากการละเมิดการทดแทน liskov รหัสที่ใช้ประเภทของคุณจะต้องมีความรู้ที่ชัดเจนเกี่ยวกับการทำงานภายในของประเภทที่ได้รับมาเพื่อรักษาพวกเขาแตกต่างกัน รหัสคู่นี้แน่นและโดยทั่วไปเพียงทำให้การใช้งานยากขึ้นอย่างต่อเนื่อง
หากคุณไม่ปฏิบัติตามสัญญาที่กำหนดไว้ในคลาสพื้นฐานสิ่งต่าง ๆ อาจล้มเหลวได้อย่างเงียบ ๆ เมื่อคุณได้รับผลลัพธ์ที่ปิด
หากสิ่งเหล่านี้ไม่ถือผู้โทรอาจได้รับผลลัพธ์ที่เขาไม่คาดหวัง
พิจารณากรณีคลาสสิกจากบันทึกคำถามสัมภาษณ์: คุณได้รับ Circle จากวงรี ทำไม? เพราะวงรีคือวงรีแน่นอน!
ยกเว้น ... ellipse มีสองฟังก์ชัน:
Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)
เห็นได้ชัดว่าสิ่งเหล่านี้จะต้องกำหนดใหม่สำหรับ Circle เพราะ Circle มีรัศมีสม่ำเสมอ คุณมีความเป็นไปได้สองอย่าง:
ภาษา OO ส่วนใหญ่ไม่รองรับภาษาที่สองและด้วยเหตุผลที่ดีมันน่าแปลกใจที่พบว่าแวดวงของคุณไม่ได้เป็นแวดวงอีกต่อไป ดังนั้นตัวเลือกแรกนั้นดีที่สุด แต่พิจารณาฟังก์ชั่นต่อไปนี้:
some_function(Ellipse byref e)
ลองจินตนาการว่า some_function เรียก e.set_alpha_radius แต่เนื่องจาก e เป็นวงกลมจริงๆมันจึงมีรัศมีเบต้าที่น่าประหลาดใจเช่นกัน
และในที่นี้คือหลักการการทดแทน: คลาสย่อยต้องถูกแทนที่สำหรับซูเปอร์คลาส สิ่งที่น่าแปลกใจเกิดขึ้น
ในคำพูดของคนธรรมดา:
รหัสของคุณจะมีมากอันยิ่งใหญ่ของCASE / สวิทช์ข้อทั่ว
กรณีของส่วนคำสั่ง CASE / switchเหล่านั้นจะต้องเพิ่มเคสใหม่เป็นระยะ ๆ ซึ่งหมายความว่าฐานของรหัสนั้นไม่สามารถปรับขนาดได้และบำรุงรักษาได้ตามที่ควรจะเป็น
LSP ช่วยให้โค้ดทำงานเหมือนฮาร์ดแวร์มากขึ้น:
คุณไม่จำเป็นต้องดัดแปลง iPod ของคุณเพราะคุณซื้อลำโพงภายนอกคู่ใหม่เนื่องจากทั้งลำโพงเก่าและลำโพงใหม่นั้นใช้อินเตอร์เฟซเดียวกันจึงสามารถใช้แทนกันได้โดยไม่ต้องสูญเสียฟังก์ชันการทำงานที่ต้องการ
typeof(someObject)
เพื่อตัดสินใจว่าคุณ "ได้รับอนุญาตให้ทำ" ต้องแน่ใจ แต่นั่นเป็นรูปแบบการต่อต้านแบบอื่นทั้งหมด
เพื่อยกตัวอย่างชีวิตจริงด้วยUndoManager ของจาวา
มันสืบทอดมาจากAbstractUndoableEdit
สัญญาที่ระบุว่ามันมี 2 สถานะ (ยกเลิกและทำซ้ำ) และสามารถไประหว่างพวกเขาด้วยการโทรเพียงครั้งเดียวไปยังundo()
และredo()
อย่างไรก็ตาม UndoManager มีสถานะมากกว่าและทำหน้าที่เหมือนเลิกทำบัฟเฟอร์ (การเรียกแต่ละครั้งเพื่อundo
ยกเลิกการแก้ไขบางส่วนแต่ไม่ใช่ทั้งหมดการแก้ไขทำให้อ่อนแอลงภายหลัง)
สิ่งนี้นำไปสู่สถานการณ์สมมุติที่คุณเพิ่ม UndoManager ให้กับ CompoundEdit ก่อนโทรend()
แล้วโทรกลับไปที่ CompoundEdit นั้นจะนำไปสู่การโทรหาการundo()
แก้ไขแต่ละครั้งเมื่อปล่อยให้การแก้ไขของคุณถูกยกเลิกไปบางส่วน
ฉันกลิ้งตัวเองUndoManager
เพื่อหลีกเลี่ยงปัญหานั้น (ฉันควรเปลี่ยนชื่อเป็นUndoBuffer
)
ตัวอย่าง: คุณกำลังทำงานกับเฟรมเวิร์ก UI และคุณสร้างการควบคุม UI แบบกำหนดเองของคุณเองโดยการคลาสคลาสControl
ฐานย่อย Control
ชั้นฐานกำหนดวิธีgetSubControls()
ซึ่งควรจะกลับไปคอลเลกชันของการควบคุมที่ซ้อนกัน (ถ้ามี) แต่คุณแทนที่วิธีการที่จะส่งกลับรายการวันเกิดของประธานาธิบดีแห่งสหรัฐอเมริกา
แล้วจะมีอะไรผิดพลาดกับสิ่งนี้? เป็นที่ชัดเจนว่าการเรนเดอร์ของตัวควบคุมจะล้มเหลวเนื่องจากคุณไม่ส่งคืนรายการของตัวควบคุมตามที่คาดไว้ เป็นไปได้ว่า UI จะมีปัญหา คุณกำลังทำลายสัญญาซึ่งคลาสย่อยของการควบคุมคาดว่าจะเป็นไปตาม
คุณยังสามารถดูได้จากมุมมองการสร้างแบบจำลอง เมื่อคุณพูดว่าตัวอย่างของการเรียนA
ยังเป็นตัวอย่างของการเรียนB
คุณบ่งบอกว่า "ติดตามพฤติกรรมของอินสแตนซ์ของคลาสA
ยังสามารถจัดเป็นพฤติกรรมที่สังเกตของอินสแตนซ์ของคลาสB
" (นี้เป็นไปได้เฉพาะในกรณีที่ระดับB
เป็นเฉพาะน้อยกว่า ชั้นเรียนA
.)
ดังนั้นการละเมิด LSP หมายความว่ามีความขัดแย้งในการออกแบบของคุณ: คุณกำลังกำหนดหมวดหมู่บางอย่างสำหรับวัตถุของคุณและจากนั้นคุณจะไม่เคารพพวกเขาในการใช้งานของคุณบางสิ่งต้องผิด
เช่นเดียวกับการทำกล่องที่มีแท็ก: "กล่องนี้มีลูกสีน้ำเงินเท่านั้น" แล้วโยนลูกบอลสีแดงลงไป การใช้แท็กดังกล่าวคืออะไรหากมันแสดงข้อมูลที่ไม่ถูกต้อง
ฉันเพิ่งสืบทอด codebase เมื่อเร็ว ๆ นี้ที่มีผู้ละเมิดหลัก Liskov อยู่ ในชั้นเรียนที่สำคัญ นี่ทำให้ฉันเจ็บปวดอย่างมาก ให้ฉันอธิบายว่าทำไม
ฉันมีที่มาจาก Class A
และแบ่งปันคุณสมบัติมากมายที่แทนที่ด้วยการใช้งานของตนเอง การตั้งค่าหรือได้รับคุณสมบัติที่มีผลกระทบที่แตกต่างกันในการตั้งหรือการที่แน่นอนคุณสมบัติเดียวกันจากClass B
Class A
Class B
Class A
Class A
Class B
public Class A
{
public virtual string Name
{
get; set;
}
}
Class B : A
{
public override string Name
{
get
{
return TranslateName(base.Name);
}
set
{
base.Name = value;
FunctionWithSideEffects();
}
}
}
นอกเหนือจากข้อเท็จจริงที่ว่านี่เป็นวิธีที่น่ากลัวที่สุดในการทำการแปลใน. NET มีปัญหาอื่น ๆ อีกมากมายที่เกี่ยวข้องกับรหัสนี้
ในกรณีName
นี้ใช้เป็นดัชนีและตัวแปรควบคุมการไหลในหลาย ๆ ที่ ชั้นเรียนด้านบนจะเกลื่อนไปทั่ว codebase ทั้งในรูปแบบดิบและรูปแบบที่ได้รับ การละเมิดหลักการทดแทน Liskov ในกรณีนี้หมายความว่าฉันจำเป็นต้องรู้บริบทของการเรียกแต่ละครั้งไปยังแต่ละฟังก์ชันที่ใช้คลาสพื้นฐาน
การใช้รหัสวัตถุของทั้งสองClass A
และClass B
ดังนั้นฉันไม่สามารถเพียงแค่ทำให้นามธรรมที่จะบังคับให้คนที่จะใช้Class A
Class B
มีบางฟังก์ชั่นยูทิลิตี้ที่มีประโยชน์มากที่ทำงานบนมีClass A
และฟังก์ชั่นยูทิลิตี้อื่น ๆ Class B
ที่มีประโยชน์มากที่ทำงานใน นึกคิดฉันต้องการที่จะสามารถที่จะใช้ฟังก์ชั่นยูทิลิตี้ที่สามารถทำงานบนบนClass A
Class B
ฟังก์ชั่นหลายอย่างที่ใช้ a Class B
สามารถทำได้อย่างง่ายดายClass A
ถ้ามันไม่ได้เป็นการละเมิด LSP
สิ่งที่เลวร้ายที่สุดเกี่ยวกับเรื่องนี้คือกรณีนี้ยากที่จะทำการปรับโครงสร้างใหม่เนื่องจากแอปพลิเคชันทั้งหมดบานพับบนคลาสทั้งสองทำงานบนทั้งสองคลาสตลอดเวลาและจะแตกเป็นร้อยถ้าฉันเปลี่ยนสิ่งนี้ (ซึ่งฉันจะทำ อย่างไรก็ตาม).
สิ่งที่ฉันจะต้องทำเพื่อแก้ไขนี่คือการสร้างNameTranslated
คุณสมบัติซึ่งจะเป็นClass B
รุ่นของName
คุณสมบัติและอย่างมากเปลี่ยนทุกการอ้างอิงถึงName
คุณสมบัติที่ได้มาอย่างระมัดระวังเพื่อใช้NameTranslated
คุณสมบัติใหม่ของฉัน อย่างไรก็ตามการได้รับหนึ่งในการอ้างอิงเหล่านี้ผิดแอปพลิเคชันทั้งหมดอาจระเบิด
เนื่องจาก codebase ไม่มีการทดสอบหน่วยรอบ ๆ มันค่อนข้างใกล้เคียงกับสถานการณ์ที่อันตรายที่สุดที่นักพัฒนาสามารถเผชิญได้ หากฉันไม่เปลี่ยนการละเมิดฉันต้องใช้พลังงานจำนวนมากในการติดตามวัตถุประเภทใดที่กำลังดำเนินการในแต่ละวิธีและหากฉันแก้ไขการละเมิดฉันสามารถทำให้ผลิตภัณฑ์ทั้งหมดระเบิดในเวลาที่ไม่เหมาะสม
BaseName
และTranslatedName
เข้าถึงทั้งสไตล์คลาส A Name
และความหมายคลาส B จากนั้นความพยายามในการเข้าถึงName
ตัวแปรชนิดใด ๆB
จะถูกปฏิเสธด้วยข้อผิดพลาดของคอมไพเลอร์ดังนั้นคุณสามารถมั่นใจได้ว่าการอ้างอิงทั้งหมดได้ถูกแปลงเป็นรูปแบบอื่นอย่างใดอย่างหนึ่ง
หากคุณต้องการรู้สึกถึงปัญหาการละเมิด LSP ให้คิดว่าจะเกิดอะไรขึ้นถ้าคุณมี. dll / .jar ระดับพื้นฐานเท่านั้น (ไม่มีซอร์สโค้ด) และคุณต้องสร้างคลาสที่ได้รับมาใหม่ คุณไม่สามารถทำงานนี้ให้สำเร็จได้