การหลีกเลี่ยงอินสแตนซ์ของ Java


102

การมีห่วงโซ่ของการดำเนินการ "instanceof" ถือเป็น "กลิ่นรหัส" คำตอบมาตรฐานคือ "ใช้ความหลากหลาย" ฉันจะทำอย่างไรในกรณีนี้?

มีคลาสย่อยจำนวนหนึ่งของคลาสพื้นฐาน ไม่มีใครอยู่ภายใต้การควบคุมของฉัน สถานการณ์ที่คล้ายคลึงกันจะเป็นกับคลาส Java Integer, Double, BigDecimal เป็นต้น

if (obj instanceof Integer) {NumberStuff.handle((Integer)obj);}
else if (obj instanceof BigDecimal) {BigDecimalStuff.handle((BigDecimal)obj);}
else if (obj instanceof Double) {DoubleStuff.handle((Double)obj);}

ฉันสามารถควบคุม NumberStuff และอื่น ๆ ได้

ฉันไม่ต้องการใช้โค้ดหลายบรรทัดโดยที่ไม่กี่บรรทัดจะทำได้ (บางครั้งฉันทำแผนที่ HashMap Integer.class กับอินสแตนซ์ของ IntegerStuff, BigDecimal.class ไปยังอินสแตนซ์ของ BigDecimalStuff เป็นต้น แต่วันนี้ฉันต้องการสิ่งที่ง่ายกว่านี้)

ฉันต้องการอะไรง่ายๆดังนี้:

public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }

แต่ Java ก็ไม่ได้ผลเช่นนั้น

ฉันต้องการใช้วิธีการคงที่เมื่อจัดรูปแบบ สิ่งที่ฉันกำลังจัดรูปแบบเป็นแบบผสมโดยที่ Thing1 สามารถมีอาร์เรย์ Thing2s และ Thing2 สามารถมีอาร์เรย์ของ Thing1s ฉันมีปัญหาเมื่อฉันใช้รูปแบบของฉันเช่นนี้:

class Thing1Formatter {
  private static Thing2Formatter thing2Formatter = new Thing2Formatter();
  public format(Thing thing) {
      thing2Formatter.format(thing.innerThing2);
  }
}
class Thing2Formatter {
  private static Thing1Formatter thing1Formatter = new Thing1Formatter();
  public format(Thing2 thing) {
      thing1Formatter.format(thing.innerThing1);
  }
}

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

หมายเหตุเพิ่มเมื่อ 5/10/2010:

ปรากฎว่าอาจจะมีการเพิ่มคลาสย่อยใหม่ในอนาคตและรหัสที่มีอยู่ของฉันจะต้องจัดการอย่างสง่างาม HashMap บน Class จะไม่ทำงานในกรณีนั้นเนื่องจากจะไม่พบ Class คำสั่ง chain of if ที่เริ่มต้นด้วยคำที่เฉพาะเจาะจงที่สุดและลงท้ายด้วยคำสั่งทั่วไปน่าจะดีที่สุด:

if (obj instanceof SubClass1) {
    // Handle all the methods and properties of SubClass1
} else if (obj instanceof SubClass2) {
    // Handle all the methods and properties of SubClass2
} else if (obj instanceof Interface3) {
    // Unknown class but it implements Interface3
    // so handle those methods and properties
} else if (obj instanceof Interface4) {
    // likewise.  May want to also handle case of
    // object that implements both interfaces.
} else {
    // New (unknown) subclass; do what I can with the base class
}

4
ฉันขอแนะนำ [รูปแบบผู้เยี่ยมชม] [1] [1]: en.wikipedia.org/wiki/Visitor_pattern
พจนานุกรม

25
รูปแบบผู้เยี่ยมชมต้องการการเพิ่มวิธีการในคลาสเป้าหมาย (เช่นจำนวนเต็ม) - ง่ายใน JavaScript และยากใน Java รูปแบบที่ยอดเยี่ยมเมื่อออกแบบคลาสเป้าหมาย ไม่ใช่เรื่องง่ายเมื่อพยายามสอนเทคนิคใหม่ ๆ ในคลาสเก่า
Mark Lutton

4
@lexicore: markdown ในความคิดเห็นมีจำนวน จำกัด ใช้[text](link)โพสต์ลิงค์ในความคิดเห็น
BalusC

2
"แต่ Java ไม่ได้ผลเช่นนั้น" บางทีฉันอาจเข้าใจผิด แต่ Java รองรับการโอเวอร์โหลดเมธอด (แม้ในเมธอดแบบคงที่) ก็ใช้ได้ ... เป็นเพียงว่าวิธีการของคุณข้างต้นไม่มีประเภทการส่งคืน
Powerlord

4
ความละเอียด @Powerlord เกินเป็นแบบคงที่รวบรวมเวลา
Aleksandr Dubinsky

คำตอบ:


55

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

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

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


22
ความหลากหลายไม่ได้ล้มเหลว แต่สตีฟ Yegge instanceofล้มเหลวในการคิดค้นรูปแบบการเข้าชมซึ่งเป็นทดแทนที่สมบูรณ์แบบสำหรับ
Rotsor

12
ฉันไม่เห็นว่าผู้เยี่ยมชมช่วยเหลือที่นี่ได้อย่างไร ประเด็นคือการตอบสนองของ OpinionatedElf ต่อ NewMonster ไม่ควรเข้ารหัสใน NewMonster แต่ใน OpinionatedElf
DJClayworth

2
ประเด็นของตัวอย่างคือ OpinionatedElf ไม่สามารถบอกได้จากข้อมูลที่มีอยู่ว่าชอบหรือไม่ชอบ Monster ต้องรู้ว่า Class the Monster เป็นของอะไร สิ่งนี้ต้องใช้อินสแตนซ์หรือ Monster ต้องรู้ในทางใดทางหนึ่งว่า OpinionatedElf ชอบหรือไม่ ผู้เข้าชมไม่ได้รับรอบนั้น
DJClayworth

2
รูปแบบผู้เยี่ยมชม @DJClayworth ใช้วิธีนี้ได้โดยการเพิ่มวิธีการลงในMonsterคลาสความรับผิดชอบซึ่งโดยพื้นฐานแล้วคือการแนะนำวัตถุเช่น "สวัสดีฉันเป็นออร์คคุณคิดอย่างไรกับฉัน" ดื้อดึงแล้วเอลฟ์สามารถตัดสินบนพื้นฐานของมอนสเตอร์เหล่านี้ "ทักทาย" bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }กับรหัสเพื่อที่คล้ายกัน จากนั้นรหัสเฉพาะมอนสเตอร์เพียงตัวเดียวจะclass Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }เพียงพอสำหรับการตรวจสอบมอนสเตอร์ทุกครั้ง
Rotsor

2
ดูคำตอบของ @Chris Knight สำหรับสาเหตุที่ไม่สามารถใช้ผู้เยี่ยมชมได้ในบางกรณี
James P.

20

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

public class IntegerWrapper {
    private Integer integer;
    public IntegerWrapper(Integer anInteger){
        integer = anInteger;
    }
    //Access the integer directly such as
    public Integer getInteger() { return integer; }
    //or method passthrough...
    public int intValue() { return integer.intValue(); }
    //then implement your visitor:
    public void accept(NumericVisitor visitor) {
        visitor.visit(this);
    }
}

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


ใช่ "ฟอร์แมตเตอร์" "คอมโพสิต" "ประเภทต่างๆ" ตะเข็บทั้งหมดจะชี้ไปในทิศทางของผู้เยี่ยมชม
Thomas Ahle

3
คุณจะทราบได้อย่างไรว่าจะใช้กระดาษห่อตัวใด ผ่าน if instanceof branching?
ฟันเร็ว

2
ดังที่ @fasttooth ชี้ให้เห็นว่าโซลูชันนี้เปลี่ยนปัญหาเท่านั้น แทนที่จะใช้instanceofเพื่อเรียกhandle()วิธีการที่ถูกต้องตอนนี้คุณจะต้องใช้มันเรียกตัวXWrapperสร้างที่ถูกต้อง...
Matthias

15

แทนที่จะเป็นขนาดใหญ่ ifคุณสามารถใส่อินสแตนซ์ที่คุณจัดการลงในแผนที่ (คีย์: คลาส, ค่า: ตัวจัดการ)

หากการค้นหาตามคีย์ส่งกลับnullให้เรียกใช้เมธอดตัวจัดการพิเศษซึ่งพยายามค้นหาตัวจัดการที่ตรงกัน (ตัวอย่างเช่นโดยการโทรisInstance()ทุกคีย์ในแผนที่)

เมื่อพบตัวจัดการให้ลงทะเบียนภายใต้คีย์ใหม่

ทำให้กรณีทั่วไปรวดเร็วและเรียบง่ายและช่วยให้คุณจัดการมรดกได้


+1 ฉันได้ใช้แนวทางนี้เมื่อจัดการโค้ดที่สร้างจาก XML schemas หรือระบบการส่งข้อความซึ่งมีอ็อบเจ็กต์หลายสิบประเภทส่งมอบให้กับโค้ดของฉันในรูปแบบที่ไม่ปลอดภัยเป็นหลัก
DNA

13

คุณสามารถใช้การสะท้อน:

public final class Handler {
  public static void handle(Object o) {
    try {
      Method handler = Handler.class.getMethod("handle", o.getClass());
      handler.invoke(null, o);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public static void handle(Integer num) { /* ... */ }
  public static void handle(BigDecimal num) { /* ... */ }
  // to handle new types, just add more handle methods...
}

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


34
ฉันจะยืนยันว่าสิ่งนี้มีกลิ่นมากกว่าตัวดำเนินการอินสแตนซ์ ควรทำงานแม้ว่า
Tim Büthe

5
@ Tim Büthe: อย่างน้อยคุณก็ไม่ต้องจัดการกับif then elseห่วงโซ่ที่กำลังเติบโตเพื่อเพิ่มลบหรือแก้ไขตัวจัดการ รหัสมีความเปราะบางน้อยต่อการเปลี่ยนแปลง ดังนั้นฉันจะบอกว่าด้วยเหตุนี้มันจึงเหนือกว่าinstanceofวิธีการ อย่างไรก็ตามฉันแค่อยากให้ทางเลือกที่ถูกต้อง
Jordão

1
นี่เป็นวิธีที่ภาษาไดนามิกจะจัดการกับสถานการณ์โดยการพิมพ์แบบเป็ด
DNA

@DNA: ไม่ว่าจะเป็นMultimethods ?
Jordão

1
ทำไมคุณถึงวนซ้ำทุกวิธีแทนที่จะใช้getMethod(String name, Class<?>... parameterTypes)? มิฉะนั้นฉันจะแทนที่==ด้วยisAssignableFromสำหรับการตรวจสอบประเภทของพารามิเตอร์
Aleksandr Dubinsky

9

คุณสามารถพิจารณาห่วงโซ่ของรูปแบบความรับผิดชอบ สำหรับตัวอย่างแรกของคุณเช่น:

public abstract class StuffHandler {
   private StuffHandler next;

   public final boolean handle(Object o) {
      boolean handled = doHandle(o);
      if (handled) { return true; }
      else if (next == null) { return false; }
      else { return next.handle(o); }
   }

   public void setNext(StuffHandler next) { this.next = next; }

   protected abstract boolean doHandle(Object o);
}

public class IntegerHandler extends StuffHandler {
   @Override
   protected boolean doHandle(Object o) {
      if (!o instanceof Integer) {
         return false;
      }
      NumberHandler.handle((Integer) o);
      return true;
   }
}

แล้วในทำนองเดียวกันสำหรับตัวจัดการอื่น ๆ ของคุณ จากนั้นเป็นกรณีของการรวม StuffHandlers เข้าด้วยกันตามลำดับ (เฉพาะเจาะจงมากที่สุดถึงเฉพาะเจาะจงน้อยที่สุดโดยมีตัวจัดการ 'ทางเลือก' ขั้นสุดท้าย) และรหัสผู้ส่งของคุณเป็นเพียงfirstHandler.handle(o);และรหัสผู้ส่งของคุณเป็นเพียง

(อีกทางเลือกหนึ่งคือแทนที่จะใช้ลูกโซ่เพียงแค่มีList<StuffHandler>ในคลาสดิสแพตเชอร์ของคุณและให้มันวนรอบรายการจนกว่าจะhandle()กลับมาเป็นจริง)


9

ฉันคิดว่าทางออกที่ดีที่สุดคือ HashMap ที่มี Class เป็นคีย์และ Handler เป็นค่า โปรดทราบว่าโซลูชันที่ใช้ HashMap ทำงานในความซับซ้อนของอัลกอริทึมคงที่θ (1) ในขณะที่โซ่การรับกลิ่นของ if-instanceof-else ทำงานในความซับซ้อนของอัลกอริทึมเชิงเส้น O (N) โดยที่ N คือจำนวนลิงก์ในห่วงโซ่ if-instanceof-else (เช่นจำนวนคลาสต่างๆที่ต้องจัดการ) ดังนั้นประสิทธิภาพของโซลูชันที่ใช้ HashMap จึงสูงกว่าประสิทธิภาพของโซลูชันเชน if-instanceof-else อย่างไม่มีอาการ พิจารณาว่าคุณจำเป็นต้องจัดการลูกหลานของคลาส Message ที่แตกต่างกัน: Message1, Message2 เป็นต้น ด้านล่างนี้คือข้อมูลโค้ดสำหรับการจัดการโดยใช้ HashMap

public class YourClass {
    private class Handler {
        public void go(Message message) {
            // the default implementation just notifies that it doesn't handle the message
            System.out.println(
                "Possibly due to a typo, empty handler is set to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        }
    }
    private Map<Class<? extends Message>, Handler> messageHandling = 
        new HashMap<Class<? extends Message>, Handler>();

    // Constructor of your class is a place to initialize the message handling mechanism    
    public YourClass() {
        messageHandling.put(Message1.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message1
        } });
        messageHandling.put(Message2.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message2
        } });
        // etc. for Message3, etc.
    }

    // The method in which you receive a variable of base class Message, but you need to
    //   handle it in accordance to of what derived type that instance is
    public handleMessage(Message message) {
        Handler handler = messageHandling.get(message.getClass());
        if (handler == null) {
            System.out.println(
                "Don't know how to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        } else {
            handler.go(message);
        }
    }
}

ข้อมูลเพิ่มเติมเกี่ยวกับการใช้ตัวแปรประเภท Class ใน Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


สำหรับกรณีจำนวนเล็กน้อย (อาจสูงกว่าจำนวนคลาสเหล่านั้นสำหรับตัวอย่างจริง) if-else จะทำงานได้ดีกว่าแผนที่นอกจากจะไม่ใช้หน่วยความจำฮีปเลย
idelvall


0

ฉันได้แก้ปัญหานี้โดยใช้reflection(ย้อนหลังไปประมาณ 15 ปีในยุคก่อน Generics)

GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();

ฉันได้กำหนด Generic Class (คลาสฐานนามธรรม) หนึ่งรายการ ฉันได้กำหนดการนำคลาสพื้นฐานไปใช้อย่างเป็นรูปธรรมมากมาย คอนกรีตแต่ละคลาสจะถูกโหลดโดยมี className เป็นพารามิเตอร์ ชื่อคลาสนี้ถูกกำหนดให้เป็นส่วนหนึ่งของการกำหนดค่า

คลาสฐานกำหนดสถานะทั่วไปในคลาสคอนกรีตทั้งหมดและคลาสคอนกรีตจะปรับเปลี่ยนสถานะโดยการลบล้างกฎนามธรรมที่กำหนดไว้ในคลาสพื้นฐาน

reflectionในเวลานั้นผมไม่ทราบว่าชื่อของกลไกนี้ซึ่งได้รับการรู้จักกันในชื่อ

ในบทความนี้มีทางเลือกอื่นอีกไม่กี่ทาง: Mapและenumนอกเหนือจากการสะท้อน


เพียงแค่อยากรู้อยากเห็นไม่ได้คุณทำทำไม? GenericClassinterface
Ztyx

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