วิธีการหลีกเลี่ยงการ downcasting?


12

คำถามของฉันเกี่ยวกับกรณีพิเศษของ Animal super class

  1. ฉันAnimalสามารถและ moveForward()eat()
  2. SealAnimalขยาย
  3. DogAnimalขยาย
  4. และมีสิ่งมีชีวิตพิเศษที่ยังขยายที่เรียกว่าAnimalHuman
  5. Humanใช้วิธีการspeak()(ไม่ได้ใช้Animal)

ในการใช้วิธีนามธรรมซึ่งยอมรับAnimalฉันต้องการใช้speak()วิธี ดูเหมือนว่าจะเป็นไปไม่ได้หากไม่มีการลดทอนลง Jeremy Miller เขียนไว้ในบทความของเขาว่ามีกลิ่นเหม็น

อะไรจะเป็นวิธีการแก้ปัญหาเพื่อหลีกเลี่ยง downcasting ในสถานการณ์นี้?


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

2
หากคุณต้องการให้สัตว์พูดความสามารถในการพูดเป็นส่วนหนึ่งของสิ่งที่ทำให้สัตว์: artima.com/interfacedesign/PreferPoly.html
JeffO

8
เดินหน้าต่อไป? แล้วปูล่ะ
Fabio Marcolini

ไอเท็ม # ใดใน Effective Java, BTW
goldilocks

1
บางคนไม่สามารถพูดได้ดังนั้นจะมีข้อยกเว้นเกิดขึ้นถ้าคุณลอง
Tulains Córdova

คำตอบ:


12

หากคุณมีวิธีการที่จำเป็นต้องรู้ว่าคลาสเฉพาะนั้นเป็นประเภทใดที่Humanจะทำอะไรคุณก็ต้องทำตามหลักการโซลิดบางอย่างโดยเฉพาะ:

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

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

บางสิ่งเช่นนี้

public void MakeItSpeak( Human obj );

และไม่ชอบสิ่งนี้:

public void SpeakIfHuman( Animal obj );

เราสามารถสร้างวิธีนามธรรมในการAnimalเรียกcanSpeakและการใช้งานที่เป็นรูปธรรมแต่ละครั้งจะต้องกำหนดว่า "พูด" ได้หรือไม่
แบรนดอน

แต่ฉันชอบคำตอบของคุณดีกว่า ความซับซ้อนมากมายมาจากการพยายามปฏิบัติต่อสิ่งที่ไม่เหมือน
แบรนดอน

@ แบรนดอนฉันเดาว่าในกรณีนั้นถ้ามันไม่สามารถ "พูด" แล้วโยนข้อยกเว้น หรือเพียงแค่ไม่ทำอะไรเลย
BЈовић

ดังนั้นคุณจะได้รับมากไปเช่นนี้public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}และpublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber

1
ไปตลอดทาง - makeItSpeak (สัตว์ประจำตระกูล ISpeakingAnimal) - จากนั้นคุณสามารถมีสัตว์เลี้ยงได้ คุณอาจมี makeItSpeak (สัตว์สัตว์) {พูดถ้าอินสแตนซ์ของ ISpeakingAnimal} แต่นั่นมีกลิ่นจาง ๆ
ptyx

6

ปัญหาไม่ได้อยู่ที่ว่าคุณกำลังลดระดับลง แต่เป็นเรื่องที่คุณกำลังทำHumanอยู่ สร้างอินเตอร์เฟสแทน:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

ด้วยวิธีนี้สภาพไม่ได้เป็นสัตว์Human- เงื่อนไขคือว่ามันสามารถพูดได้ วิธีนี้mysteriousMethodสามารถทำงานร่วมกับคลาสย่อยอื่น ๆ ที่ไม่ใช่มนุษย์ได้Animalตราบใดที่ยังใช้งานCanSpeakอยู่


ในกรณีนี้มันควรจะเป็นตัวอย่างของ CanSpeak แทน Animal
Newtopian

1
@ Newtopian สมมติว่าวิธีนั้นไม่ต้องการอะไรจากAnimalและผู้ใช้ทั้งหมดของวิธีนั้นจะเก็บวัตถุที่พวกเขาต้องการที่จะส่งไปผ่านการCanSpeakอ้างอิงประเภท (หรือแม้กระทั่งHumanการอ้างอิงประเภท) ถ้าเป็นกรณีที่วิธีการที่จะได้ใช้ในสถานที่แรกและเราจะไม่ต้องแนะนำHuman CanSpeak
Idan Arye

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

1
@ Newtopian เหตุผลที่เราแนะนำCanSpeakในตอนแรกไม่ใช่ว่าเรามีบางอย่างที่ใช้มัน ( Human) แต่เรามีบางอย่างที่ใช้มัน (วิธีการ) จุดคือการแยกวิธีการที่จากชั้นคอนกรีตCanSpeak Humanถ้าเราไม่ได้มีวิธีการที่จะรักษาสิ่งที่แตกต่างกันจะมีจุดที่แตกต่างในสิ่งที่ไม่มีCanSpeak CanSpeakเราไม่ได้สร้างอินเทอร์เฟซเพียงเพราะเรามีวิธี ...
Idan Arye

2

คุณสามารถเพิ่มการติดต่อสื่อสารกับสัตว์ หมาเห่า, คนพูด, ซีล .. เอ่อ .. ฉันไม่รู้ว่าซีลทำอะไร

แต่ดูเหมือนว่าวิธีการของคุณถูกออกแบบมาเพื่อถ้า (สัตว์เป็นมนุษย์) พูด ();

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


15
แมวน้ำนั้นเป็นหนี้บุญคุณ แต่สุนัขจิ้งจอกไม่ได้กำหนดไว้

2

ในกรณีนี้การใช้งานเริ่มต้นของspeak()ในAbstractAnimalชั้นเรียนจะเป็น:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

ณ จุดนี้คุณได้เริ่มต้นใช้งานในชั้นนามธรรม - และมันทำงานอย่างถูกต้อง

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

ใช่นี่หมายความว่าคุณได้ลองจับรหัสเพื่อจัดการกับทุกอย่างspeakแต่ทางเลือกอื่นคือif(thingy is Human)ห่อคำพูดทั้งหมดไว้

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


1
ฉันไม่คิดว่านี่เป็นเหตุผลที่ดีที่จะใช้ข้อยกเว้น (ยกเว้นว่าเป็นรหัสหลาม)
ไบรอันเฉิน

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

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

2
ทางเลือกในการโยนข้อยกเว้นในกรณีนี้อาจไม่ทำอะไรเลย
ท้ายที่สุด

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

1

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

เวลาที่ downcasting ควรถูกมองว่าน่าสงสัยมากที่สุดคือเมื่อวัตถุที่ถูกร่ายนั้นเป็นที่รู้จักกันว่าเป็นชนิดที่เหมาะสม โดยทั่วไปถ้าวัตถุเป็นที่รู้จักกันCatหนึ่งควรใช้ตัวแปรประเภทCatแทนที่จะเป็นตัวแปรประเภทAnimalเพื่ออ้างถึง อย่างไรก็ตามมีบางครั้งที่สิ่งนี้ไม่ได้ผลเสมอไป ตัวอย่างเช่นZooคอลเลกชันอาจเก็บคู่ของวัตถุในช่องคู่ / คี่อาเรย์ด้วยความคาดหวังว่าวัตถุในแต่ละคู่จะสามารถดำเนินการกับแต่ละอื่น ๆ แม้ว่าพวกเขาจะไม่สามารถกระทำกับวัตถุในคู่อื่น ๆ ในกรณีเช่นนี้วัตถุในแต่ละคู่จะยังคงต้องยอมรับชนิดพารามิเตอร์ที่ไม่เฉพาะเจาะจงเช่นที่พวกเขาสามารถทำได้syntacticallyผ่านวัตถุจากคู่อื่น ๆ ดังนั้นแม้ว่าCat'splayWith(Animal other)วิธีการจะทำงานเฉพาะเมื่อotherเป็นCatที่Zooจะต้องสามารถที่จะผ่านมันองค์ประกอบของการเป็นนักAnimal[]ดังนั้นพารามิเตอร์ชนิดของมันจะต้องมีมากกว่าAnimalCat

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


Object.equalToString(String string)ในกรณีสตริงคุณค่อนข้างจะมีวิธี จากนั้นคุณมีboolean String.equal(Object object) { return object.equalStoString(this); }ดังนั้นไม่จำเป็นต้องลดความเร็ว: คุณสามารถใช้การกระจายแบบไดนามิก
Giorgio

@Giorgio: Dynamic dispatching มีประโยชน์ แต่โดยทั่วไปมันยิ่งแย่กว่า downcasting
supercat

วิธีการจัดส่งแบบไดนามิกโดยทั่วไปยิ่งเลวร้ายยิ่งกว่า downcasting? ฉันคิดว่ามันเป็นวิธีอื่น ๆ
ไบรอันเฉิน

@BryanChen: ขึ้นอยู่กับคำศัพท์ของคุณ ฉันไม่คิดว่าObjectจะมีequalStoStringวิธีเสมือนใด ๆและฉันจะยอมรับว่าฉันไม่รู้ว่าตัวอย่างที่ยกมาจะทำงานใน Java แต่ใน C #, การจัดส่งแบบไดนามิก (แตกต่างจากการจัดส่งเสมือนจริง) จะหมายความว่าคอมไพเลอร์มี to do Reflection-based name lookup ในครั้งแรกที่มีการใช้เมธอดบนคลาสซึ่งแตกต่างจากการจัดส่งเสมือน (ซึ่งทำการโทรผ่านสล็อตในตารางเมธอดเสมือนซึ่งจำเป็นต้องมีเมธอดที่ถูกต้อง)
supercat

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

1

ในการใช้วิธีนามธรรมซึ่งยอมรับสัตว์ฉันต้องการใช้วิธี speak ()

คุณมีตัวเลือกน้อย:

  • ใช้การสะท้อนกลับเพื่อโทรspeakถ้ามีอยู่ ประโยชน์: Humanการพึ่งพาไม่มีใน ข้อเสีย: ขณะนี้มีการพึ่งพาที่ซ่อนอยู่ในชื่อ "พูด"

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

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

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