โลกแห่งความจริง - หลักการทดแทน Liskov


14

พื้นหลัง: ฉันกำลังพัฒนากรอบการส่งข้อความ กรอบนี้จะช่วยให้:

  • การส่งข้อความผ่านบัสบริการ
  • สมัครสมาชิกคิวบนบัสข้อความ
  • สมัครสมาชิกหัวข้อบนบัสข้อความ

ขณะนี้เรากำลังใช้ RabbitMQ แต่ฉันรู้ว่าเราจะย้ายไปที่ Microsoft Service Bus (บนสถานที่ตั้ง) ในอนาคตอันใกล้นี้

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

ปัญหาที่นี่คือ RabbitMQ และ ServiceBus ไม่สามารถแปลได้โดยตรง ตัวอย่างเช่น RabbitMQ ขึ้นอยู่กับการแลกเปลี่ยนและชื่อหัวข้อในขณะที่ ServiceBus เป็นข้อมูลเกี่ยวกับ Namespaces และ Queues นอกจากนี้ไม่มีอินเทอร์เฟซทั่วไประหว่างไคลเอนต์ ServiceBus และไคลเอนต์ RabbitMQ (เช่นทั้งสองอาจมีการเชื่อมต่อ ICon แต่อินเทอร์เฟซแตกต่างกัน - ไม่ใช่จากเนมสเปซทั่วไป)

ดังนั้นถึงจุดของฉันฉันสามารถสร้างอินเตอร์เฟซดังต่อไปนี้:

public interface IMessageReceiver{
  void AddSubscription(ISubscription subscriptionDetails)
}

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

public void AddSubscription(ISubscription subscriptionDetails){
  if(!subscriptionDetails is RabbitMqSubscriptionDetails){
    // I have a problem!
  }
}

สำหรับฉันแล้วบรรทัดด้านบนจะแบ่งกฎการทดแทนของ Liskov

ฉันถือว่าการพลิกนี้เพื่อให้การสมัครสมาชิกยอมรับ IMessageConnection แต่อีกครั้งการสมัครสมาชิก RabbitMq จะต้องมีคุณสมบัติเฉพาะของ RabbitMQMessageConnection

ดังนั้นคำถามของฉันคือ:

  • ฉันถูกต้องไหมว่าการแบ่ง LSP นี้
  • เราเห็นด้วยหรือไม่ว่าในบางกรณีมันเป็นสิ่งที่หลีกเลี่ยงไม่ได้

หวังว่านี่จะชัดเจนและในหัวข้อ!


การเพิ่มพารามิเตอร์ประเภทให้กับตัวเลือกสำหรับคุณหรือไม่ ในแง่ของจาวา - ซินแท็คซ์สิ่งที่ชอบinterface TestInterface<T extends ISubscription>จะสื่อสารอย่างชัดเจนว่าเป็นที่ยอมรับประเภทและมีความแตกต่างระหว่างการใช้งาน
Hulk

@Hulk ฉันไม่เชื่ออย่างนั้นเพราะการใช้งานแต่ละอย่างจะต้องมีการติดตั้ง ISubscription ที่แตกต่างกัน
GinjaNinja

1
interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }ขออภัยสิ่งที่ผมอยากจะนำเสนอเป็น การใช้งานนั้นอาจดูเหมือนpublic class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} }(ในจาวา)
Hulk

คำตอบ:


11

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

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

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


ขอบคุณ ฉันไม่สามารถคิดวิธีการ 'พลิก' โดยไม่ต้องย้ายปัญหา เช่นการสมัครสมาชิกจะต้องรู้ IModel สำหรับ RabbitMQ และอย่างอื่นสำหรับ ServiceBus เพื่อรับข้อความ ฉันคิดว่าข้อยกเว้นที่มีคำอธิบายประกอบเป็นวิธีเดียวที่จะส่งต่อ
GinjaNinja

2

ใช่รหัสของคุณแบ่ง LSP ที่นี่ ในกรณีเช่นนี้ฉันจะใช้ Anti-Corruption Layer จาก DDD Design Patterns คุณสามารถเห็นตัวอย่างหนึ่งได้ที่นั่น: http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/

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

หวังว่ามันจะช่วย!

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