这个故事最初是来自和发哥的一次聊天,他说了一些面向对象设计方面挺有意思的事情,包括 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)。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》