`

画圆画方的故事

阅读更多

这个故事最初是来自和发哥的一次聊天,他说了一些面向对象设计方面挺有意思的事情,包括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)。

 

文章系本人原创,转载请注明出处和作者

 

8
0
分享到:
评论
4 楼 chairmanMao 2012-12-07  
使用范型就可以了,
3 楼 RayChase 2012-04-15  
xiaodatao 写道
故事很精彩!
小P可以考滤一下,用设计模式中的“策略模式(Strategy)”。

public class Drawer {

    DrawEvent event;
   
    public Drawer(DrawEvent event) {
        this.event = event;
    }
   
    public void draw() {
        event.draw();
    }
}

不,你没有说到点子上去,Event始终只是一个委托,真正draw的是谁(Double Dispatch的“double”一个是你提到的这个event,还有一个呢)?
你调用了event.draw()没有错,但是这个event会把这个draw的行为委托给真正的drawer来完成,那么你这里是通过怎样的机制来解决这个变化点、把这个委托完成的呢?
2 楼 xiaodatao 2012-04-15  
故事很精彩!
小P可以考滤一下,用设计模式中的“策略模式(Strategy)”。

public class Drawer {

    DrawEvent event;
   
    public Drawer(DrawEvent event) {
        this.event = event;
    }
   
    public void draw() {
        event.draw();
    }
}
1 楼 bloodwolf_china 2012-03-12  
嗯,很巧妙,类似于回调函数一样,把抽象类型变为具体类型。
有时候设计模式就像时逢山开路遇水填桥一样,再多想一步。

相关推荐

Global site tag (gtag.js) - Google Analytics