画圆画方的故事

画圆画方的故事 这个故事最初是来自和发哥的一次聊天,他说了一些面向对象设计方面挺有意思的事情,包括Double Dispatch(下面会提到),我根据我自己的体会和思考,把这些零散的片段重新整理成一个小故事,欢迎感兴趣的同学一起讨论。

有一个苦逼的程序员,叫做小P。

有一天,老板给他传达了这样一个需求,根据用户不同的图像绘制事件,画出一个圆或者是画出一个方块来。

老板传达的图像绘制事件是这样的:

interface DrawEvent {  
}  
  
class RoundDrawEvent implements DrawEvent {  
}  
  
class RectangleDrawEvent implements DrawEvent {  
}  

小P说,这个问题很简单:

public class Drawer {  
    public void draw(DrawEvent event) {  
        if (event instanceof RoundDrawEvent) {  
            // 画圆  
        } else if (event instanceof RectangleDrawEvent) {  
            // 画方  
        } else {  
            System.out.print("error");  
        }  
    }  
}  

这似乎没有任何难度,一下就做出来了,不过,用户心情一好,就要提新的需求,而苦逼的程序员面对用户新的需求,是不能退缩的。用户说,现在我要求系统还要支持三角形。

小P想到,如果我再在代码里面增加一个if-else分支,问题是可以解决,可是分支越来越多,代码越来越丑陋,如果我可以用一个Map来代替if-else完成选择的功能,岂不是可以让我原来的实现优雅一点?

public class Drawer {  
    private static Map<Class, IDrawer> DRAWER_MAP = new HashMap<Class, IDrawer>();  
    static {  
        DRAWER_MAP.put(RoundDrawEvent.class, new RoundDrawer());  
        DRAWER_MAP.put(RectangleDrawEvent.class, new RectangleDrawer());  
    }  
  
    public void draw(DrawEvent event) {  
        DRAWER_MAP.get(event.getClass()).draw();  
    }  
}  
  
interface IDrawer {  
    public void draw();  
}  
  
class RoundDrawer implements IDrawer {  
    public void draw() {  
        // 画圆  
    }  
}  
  
class RectangleDrawer implements IDrawer {  
    public void draw() {  
        // 画方  
    }  
}  

突然,一瞬间的火花,小P觉得如果用方法重载来代替if-else的工作,把变化的点转移到方法重载上,也可以做到:

public class Drawer {  
    public void draw(RoundDrawEvent event) {  
        //画圆  
    }  
      
    public void draw(RectangleDrawEvent event) {  
        //画方  
    }  
      
    public void draw(DrawEvent event) {  
        System.out.print("error");  
    }  
}  

可是,测试这个方法的时候,他傻眼了:

DrawEvent event = new RoundDrawEvent();  
new Drawer().draw(event);  

他发现每次输出的结果都是“error”!

而如果测试代码改成这样,却是正确的:

new Drawer().draw(new RoundDrawEvent()); 

这是怎么回事?

看来小P和很数苦逼程序员还是有点不一样,他喜欢尝试、喜欢思考,而且还特别喜欢研究,一查到底。

原来,在Java中,方法重载都是在编译期间确定的,对于编译期间draw方法的实参event,如果使用了DrawEvent这个接口来引用,那么结果就可想而知,去执行draw(DrawEvent event)这个方法了。

原因清楚了,接下去就不难想出解决办法:

既然方法的重载无法是动态的,那么我在调用这个重载了的方法之前,我就要给它传入一个在编译期就已经确定了具体类型的入参,把变化的点转移到对象的多态上。

可是,DrawEvent接口里并没有提供可供外部因素参与和影响的变化点,如果它能够提供一个供外部注入行为的变化点,不就可以用多态来帮助我们了么:

interface DrawEvent {  
    public void draw(Drawer drawer);  
}  
  
class RoundDrawEvent implements DrawEvent {  
    public void draw(Drawer drawer) {  
        drawer.draw(this);  
    }  
}  
  
class RectangleDrawEvent implements DrawEvent {  
    public void draw(Drawer drawer) {  
        drawer.draw(this);  
    }  
}  

这里我说明一下为什么要传入drawer参数,因为真正要画图的家伙,不是这个event,而是drawer,而这个event只不过是利用多态,起到了寻找那个合适的重载方法的作用

好,接下去再完成Drawer就可以了:

public class Drawer {  
    public void draw(RoundDrawEvent event) {  
        // 画圆  
    }  
  
    public void draw(RectangleDrawEvent event) {  
        // 画方  
    }  
  
    public void draw(DrawEvent event) {  
        event.draw(this);  
    }  
}  

可以看到,其实这里的DrawEvent已经不纯粹了,不仅仅代表了事件本身,还作为一个行为的委托者,甄选具体要执行的行为,再把执行的任务交还给Drawer

这时,我用下面的办法测试这个方法的时候,结果就是正确的了:

DrawEvent event = new RoundDrawEvent();  
new Drawer().draw(event);  

如果我把入参的引用变成具体类型,如:

new Drawer().draw(new RoundDrawEvent());

就直接走到Drawer的draw(RoundDrawEvent event)方法上了,于是结果也是正确的。

类似的实现方式,被称为Double Dispatch,它要根据两个对象的运行时类型来选择具体的执行方法(dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call)。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Preview on Feedage: