การนำอินเตอร์เฟสไปใช้เมื่อคุณไม่ต้องการคุณสมบัติใดคุณสมบัติหนึ่ง


31

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

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

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


1
ฉันจำได้ว่ามีบางคลาสที่ใช้กันทั่วไปใน C # ซึ่งใช้อินเทอร์เฟซ แต่ระบุไว้อย่างชัดเจนในเอกสารประกอบว่าไม่มีการใช้วิธีการบางอย่าง ฉันจะลองดูว่าฉันสามารถหาได้
Mage Xy

ฉันสนใจที่จะเห็นอย่างแน่นอนหากคุณสามารถหาได้
Chris Pratt

23
ฉันสามารถชี้ให้เห็นหลายกรณีนี้ในห้องสมุดของ. NET - และพวกเขาทั้งหมดได้รับการยอมรับว่าเป็นความผิดพลาดที่ไม่ดี นี่คือการละเมิดที่เป็นสัญลักษณ์และร่วมกันของหลักการชดเชย Liskov - เหตุผลที่ไม่ละเมิด LSP สามารถพบได้ในคำตอบของฉันที่นี่
จิมมี่ฮอฟฟา

3
คุณต้องการใช้อินเทอร์เฟซเฉพาะนี้หรือคุณสามารถแนะนำหน้าเว็บที่มี superinterface และใช้สิ่งนั้นได้หรือไม่?
Christopher Schultz

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

คำตอบ:


51

นี่เป็นตัวอย่างคลาสสิกของวิธีที่ผู้คนตัดสินใจละเมิดหลักการ Liskov Subtitution ฉันขอแนะนำอย่างยิ่ง แต่อาจกระตุ้นให้มีวิธีแก้ไขปัญหาที่แตกต่างกัน:

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

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


นี่คือบิตสั้น ๆ จากวิกิพีเดีย:

หลักการทดแทน Liskovสามารถกำหนดขึ้นมาได้ง่ายๆว่า "อย่าเพิ่มความแข็งแกร่งให้กับเงื่อนไขก่อนหน้าและอย่าทำให้สถานการณ์หลังอ่อนลง"

หลักการแทนที่ Liskov (LSP) เป็นคำจำกัดความเฉพาะของความสัมพันธ์ย่อยที่เรียกว่า (strong) subtyping พฤติกรรมที่เรียกว่า (strong) subtyping ที่ถูกนำมาใช้ครั้งแรกโดย Barbara Liskov ในการปราศรัยการปราศรัยการประชุมปี 1987 ที่มีชื่อว่า Data นามธรรมและลำดับชั้น มันเป็นความหมายมากกว่าความสัมพันธ์ทางวากยสัมพันธ์เพียงเพราะมันตั้งใจที่จะรับประกันการทำงานร่วมกันความหมายของประเภทในลำดับชั้น [... ]

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


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

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


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

@ChrisPratt มันเป็นความจริงข้อผิดพลาดทั่วไปที่จะทำให้ที่ว่าทำไม formalisms สามารถมีค่า - รหัสการจำแนกกลิ่นจะช่วยให้มากขึ้นได้อย่างรวดเร็วระบุพวกเขาและการแก้ปัญหาการเรียกคืนใช้ก่อนหน้านี้
Jimmy Hoffa

4

ดูดีสำหรับฉันถ้านี่เป็นสถานการณ์ของคุณ

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

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


6
C # ไม่รองรับการสืบทอดคลาสหลายคลาส แต่รองรับอินเทอร์เฟซ
mgw854

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


4

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

เมื่อคุณมีอินเทอร์เฟซที่จัดส่งแล้วซึ่งคุณเก็บไว้เพื่อเหตุผลด้านความเข้ากันได้และ ...

  • อินเทอร์เฟซประกอบด้วยสมาชิกที่ล้าสมัยหรือไม่ต่อเนื่อง ยกตัวอย่างเช่นBlockingCollection<T>.ICollection.SyncRoot(ผู้อื่น) ในขณะที่ICollection.SyncRootไม่ได้ล้าสมัยต่อ se NotSupportedExceptionมันจะโยน

  • อินเทอร์เฟซประกอบด้วยสมาชิกที่มีเอกสารว่าเป็นตัวเลือกและการใช้งานอาจทำให้เกิดข้อยกเว้น ตัวอย่างเช่นใน MSDN เกี่ยวกับIEnumerator.Resetมันพูดว่า:

วิธีรีเซ็ตมีไว้สำหรับการทำงานร่วมกันของ COM ไม่จำเป็นต้องดำเนินการ ผู้ใช้งานสามารถโยน NotSupportedException แทนได้

  • โดยความผิดพลาดของการออกแบบอินเทอร์เฟซมันควรจะมีมากกว่าหนึ่งอินเทอร์เฟซในตอนแรก มันเป็นรูปแบบที่พบบ่อยใน BCL NotSupportedExceptionในการดำเนินการอ่านอย่างเดียวรุ่นของภาชนะ ฉันได้ทำมันด้วยตัวเองมันเป็นสิ่งที่คาดหวังในตอนนี้ ... ฉันICollection<T>.IsReadOnlyกลับมาtrueเพื่อที่คุณจะได้บอกพวกเขาว่า การออกแบบที่ถูกต้องน่าจะเป็นรุ่นที่อ่านได้ของส่วนต่อประสานแล้วส่วนต่อประสานแบบเต็มจะสืบทอดมาจากส่วนนั้น

  • ไม่มีส่วนต่อประสานที่ดีกว่าในการใช้ ตัวอย่างเช่นฉันมีคลาสที่อนุญาตให้คุณเข้าถึงรายการตามดัชนีตรวจสอบว่ามีรายการอยู่หรือไม่และมีดัชนีขนาดใดคุณสามารถคัดลอกไปยังอาร์เรย์ได้ ... ดูเหมือนว่าจะเป็นงานIList<T>แต่ชั้นเรียนของฉันมี ขนาดคงที่และไม่สนับสนุนการเพิ่มหรือลบดังนั้นจึงทำงานเหมือนอาร์เรย์มากกว่ารายการ แต่มีไม่มีIArray<T>ใน BCL

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


ลองพิจารณาเหตุผลว่าทำไมจึงไม่รองรับ

บางครั้งInvalidOperationExceptionก็เป็นตัวเลือกที่ดีกว่า ตัวอย่างเช่นอีกวิธีหนึ่งในการเพิ่ม polymorphism ในคลาสคือการใช้อินเตอร์เฟสภายในและโค้ดของคุณคือการเลือกว่าจะให้อินสแตนซ์ใดขึ้นอยู่กับพารามิเตอร์ที่กำหนดในตัวสร้างคลาส [สิ่งนี้มีประโยชน์โดยเฉพาะถ้าคุณรู้ว่าชุดตัวเลือกได้รับการแก้ไขและคุณไม่ต้องการอนุญาตให้มีการแนะนำคลาสบุคคลที่สามโดยการฉีดพึ่งพา] ฉันได้ทำเช่นนี้กับbackport ThreadLocalเนื่องจากการติดตามและการใช้งานที่ไม่ติดตาม appart ที่มากเกินไปและอะไรที่ทำให้ThreadLocal.Valuesเกิดการติดตั้งที่ไม่ติดตามInvalidOperationExceptionแม้สรรพสินค้ามันไม่ได้ขึ้นอยู่กับสถานะของวัตถุ ในกรณีนี้ฉันแนะนำชั้นเรียนด้วยตัวเองและฉันรู้ว่าวิธีการนี้จะต้องดำเนินการโดยเพียงแค่ส่งข้อยกเว้น

บางครั้งค่าเริ่มต้นสมเหตุสมผล ยกตัวอย่างเช่นที่ICollection<T>.IsReadOnlyกล่าวมาข้างต้นมันสมเหตุสมผลแล้วที่จะส่งคืน´true´ หรือ´false´ แล้วแต่กรณี ดังนั้น ... ความหมายของIFoo.Barคืออะไร อาจมีบางค่าเริ่มต้นที่เหมาะสมที่จะกลับมา


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


0

มีใครเคยเจอสถานการณ์ที่คล้ายกันมาก่อนหรือไม่

ใช่. นักออกแบบห้องสมุด. Net ทำ ชั้นเรียนพจนานุกรมทำสิ่งที่คุณกำลังทำอยู่ มันใช้การใช้งานที่ชัดเจนเพื่อซ่อน * วิธีการ IDictionary บางอย่างได้อย่างมีประสิทธิภาพ มันอธิบายได้ดีกว่าที่นี่แต่ต้องสรุปเพื่อที่จะใช้วิธีการเพิ่ม, คัดลอกหรือลบของพจนานุกรมที่ใช้ KeyValuePairs คุณต้องโยนวัตถุเป็น IDictionary

* มันไม่ได้ "ซ่อนเร้น" วิธีการในความเข้มงวดของคำว่า "ซ่อน" เช่นไมโครซอฟท์จะใช้มัน แต่ฉันไม่รู้คำศัพท์ที่ดีกว่าในกรณีนี้



ทำไมต้องลงคะแนน
user2023861

2
อาจเป็นเพราะคุณไม่ได้ตอบคำถาม อิ เรียงจาก
การแข่งขัน Lightness กับโมนิก้า

@LightnessRacesinOrbit ฉันได้อัปเดตคำตอบเพื่อให้ชัดเจนยิ่งขึ้น ฉันหวังว่ามันจะช่วย OP
user2023861

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

-6

คุณสามารถนำไปใช้และส่งคืนค่าที่ไม่ร้ายแรงหรือค่าเริ่มต้นได้เสมอ หลังจากทั้งหมดมันเป็นเพียงทรัพย์สิน มันสามารถส่งคืน 0 (คุณสมบัติเริ่มต้นของ int) หรือค่าใด ๆ ที่เหมาะสมในการดำเนินการของคุณ (int.MinValue, int.MaxValue, 1, 42, ฯลฯ ... )

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

การโยนข้อยกเว้นดูเหมือนว่าจะเป็นรูปแบบที่ไม่ดี


2
ทำไม? การคืนข้อมูลที่ไม่ถูกต้องจะดีขึ้นอย่างไร
การแข่งขัน Lightness กับโมนิก้า

1
การแบ่ง LSP นี้: ดูคำตอบของ Jimmy Hoffa สำหรับคำอธิบายว่าทำไม

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

ฉันไม่ได้ตอนนี้ค่าจะไม่ถูกต้องเมื่อหนึ่งของคุณใช้อินเตอร์เฟซ! มันเป็นการใช้งานของคุณมันสามารถเป็นอะไรก็ได้ที่คุณต้องการ
Jon Raynor

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