画圆画方的故事
这个故事最初是来自和发哥的一次聊天,他说了一些面向对象设计方面挺有意思的事情,包括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)。
文章系本人原创,转载请注明出处和作者