当工厂方法遇到静态方法

标签:Java, Python, 设计模式

项目中遇到个问题,虽然还没和别人沟通过,不过先记录一下自己的想法吧。
在第一阶段的开发中,我们只针对美国和加拿大;现在要开始第二阶段的开发了,需要面向欧洲6国;而第三阶段则要面向亚洲。
问题是每个国家除了语言不一样,连逻辑都不一样,于是调用类的逻辑就得改了。简单来说就是要针对不同的国家,调用不同的类。

最容易想到的方法就是使用工厂方法模式:传递国家名,找到对应的类,再调用该类的方法。
可是被调用的方法不是实例方法,而是静态方法,这就导致了不能通过构造一个子类对象,转换成抽象基类的对象来调用该静态方法,因为调用静态方法时不会调用子类的方法。

关于这点,可能有人没弄懂我说什么,所以我再解释一下(以下皆不涉及内联等优化):
首先是一般的函数调用:在C语言中,调用一个函数需要将参数压栈,然后执行call 函数地址这个操作。此处的函数地址是在编译时就知道的,因此编译器可以将其直接替换成一个数。
接着是实例方法的调用:在Java语言里,调用一个实例的方法也和调用一般的函数差不多,所不同的是还需要将this引用的地址压栈,并且函数地址不是在编译时能确定的。为了计算出这个地址,就需要通过this对象自身,找到它的类(注意这里是运行期确定的),再依次向上(父类)查询被调用方法的地址,直到找到一个(编译期保证了至少有一个类实现了该方法)。这里的对象、类和方法是紧耦合的,而且Java虚拟机也只提供了这一种定位方式,所以只是略慢于一般的函数调用。
再是静态方法的调用:在Java语言里,调用一个静态方法和调用一般的函数完全一样,因为类和静态方法的地址在编译期就决定了。这也就意味着通过对象来调用静态方法时,编译器会按照对象当前的静态类型(类)来寻找该静态方法,这也就是静态方法不能像实例方法一样体现出多态性的原因。

以上的函数调用方式皆是静态语言的实现,而只有实例方法的调用可以体现出多态性。
而在面向对象设计时,多态性是非常重要的特性,因为它使得代码更为通用:在替换一部分的实现时,不需要更改整体的实现。(要达到这一目的,当然还必须保证封装性。)
而由于静态方法不能体现出多态性,所以当我需要动态调用不同的静态方法时,通常的实现就无法满足我的需求了,这就是我遇到的关键问题。

不过Java仍然提供了一种笨拙的解决方法,也就是提供类的class对象、方法名和参数,可以让虚拟机通过自省的办法来调用指定的方法。也就是说,这种行为强制让方法的地址查询延迟到了运行期,通过运行期的class对象来找到正确的方法。
理论上来说,这和一般的实例方法调用开销是差不多的(定位会稍慢,因为需要额外获取class对象,并通过方法名而不是指针来定位),但是告诉虚拟机执行这种操作也需要调用特别的方法,而且较难优化,因此性能会大幅降低。

接着看C#。在最新的4.0版里引入了dynamic类型,这种类型的对象可以调用任意的方法,即使该方法在编译期不存在,也不会报错。
我不清楚C#的实现,但这种特性正是源于动态语言的,所以拿Python来解释。
在访问一个对象的属性(这里将方法也算作属性)时,编译器并不告知该属性的地址。而在运行时,虚拟机会查询对象自身是否有该属性;如果找不到,便接着查找类、父类里是否定义了该属性,是否有__get__等方法,还有元类等。
可以看出,动态语言在调用一个函数时,查询其地址的开销是很大的,因此对性能的影响也是很大的;但它同时也提供了一个方便之处,那就是函数地址是不固定的,所以动态更改函数的实现(准确来说是将函数名绑定到另一个函数对象上)和动态绑定对象的方法便成为了可能。
而在我遇到的问题里,实例方法、类方法和静态方法对于Python都是一样的,都具有多态性,所以便不需要用特殊的方法来调用静态方法。

最后列出示例代码,首先是Java的:
//Dispatcher.java
package cn.keakon;

public class Dispatcher {
    
	public String getResponse(String country) {
		final String CLASS_PREFIX = "cn.keakon.Screen";
		final String DEFAULT_COUNTRY = "US";
		if (country == null || country.equals("")) {
			country = DEFAULT_COUNTRY;
		}
		
		Class screen = null;
		try {
			screen = Class.forName(CLASS_PREFIX + country); // 获取class对象
		} catch(Exception e){
			//System.out.println(e);
			try{
				screen = Class.forName(CLASS_PREFIX + DEFAULT_COUNTRY);
			} catch(Exception e2){
				//System.out.println(e2);
			}
		}
		
		try{
			if (screen != null) {
				return (String)screen.getMethod("doGet", null).invoke(null, null); // 通过Method对象来invoke实际的方法,这和C#的传统方式一样
			}
		} catch(Exception e){
			//System.out.println(e);
		}
		return "";
	}

	public static void main(String[] args) {
		Dispatcher dispatcher = new Dispatcher();
		final int SIZE = 1000;
		
		boolean failed = false;
		long time = System.currentTimeMillis();
		for (int i = 0; i < SIZE; ++i) {
			failed |= "".equals(dispatcher.getResponse("US"));
			failed |= "".equals(dispatcher.getResponse("CN"));
			failed |= "".equals(dispatcher.getResponse("JP"));
			failed |= "".equals(dispatcher.getResponse(""));
			failed |= "".equals(dispatcher.getResponse(null));
		}
		System.out.println(System.currentTimeMillis() - time);
		System.out.println(failed);
	}
}


//ScreenUS.java
package cn.keakon;

public class ScreenUS {

	public static String doGet() {
		return "Hello, world!";
	}
}


//ScreenCN.java
package cn.keakon;

public class ScreenCN {

	public static String doGet() {
		return "你好,世界!";
	}
}
用Java 1.4.2_19运行,时间为156ms;Java 1.6.0_17的时间为125ms。要知道,这只是5000次调用而已,换作静态调用的话是不到1ms的,可见动态性对Java的影响有多大。

接着看Python实现:
#dispatcher.py
# -*- coding: utf-8 -*-

from time import clock

class Dispatcher(object):
	def getResponse(self, country):
		MODULE_NAME = 'screen'
		CLASS_PREFIX = 'Screen'
		DEFAULT_COUNTRY = 'US'

		if not country:
			country = DEFAULT_COUNTRY

		screen = __import__(MODULE_NAME) # 获取模块

		try:
			screen = getattr(screen, CLASS_PREFIX + country) # 获取类
		except:
			screen = getattr(screen, CLASS_PREFIX + DEFAULT_COUNTRY)

		return screen.doGet() # 直接调用类方法

def test():
	dispatcher = Dispatcher()
	SIZE = 1000

	failed = False
	time = clock()
	for i in xrange(SIZE):
		failed |= not dispatcher.getResponse('US')
		failed |= not dispatcher.getResponse('CN')
		failed |= not dispatcher.getResponse('JP')
		failed |= not dispatcher.getResponse('')
		failed |= not dispatcher.getResponse(None)
	print clock() - time
	print bool(failed)

if __name__ == '__main__':
	test()


#screen.py
#-*- coding: utf-8 -*-

class ScreenUS(object):
	@classmethod
	def doGet(cls):
		return 'Hello, world!'

class ScreenCN(object):
	@classmethod
	def doGet(cls):
		return '你好,世界!'
测试结果是12.5ms,也就是Java的10倍,而且有些部分还可以更为优化。
不过相对于Java,doGet不是直接用字符串来查找的,而是经过了编译,所以可以节省部分时间。但即便将Java的实现改成实例方法,使用((Screen)screen.newInstance()).doGet()来调用,也没有显著地加快,仍然比Python慢1个数量级。

从这个例子可以看出,用静态语言实现动态语言的特性,其性能表现不一定比得过原生的动态语言。所以那些指望将Python提高到与Java相当的运行速度的人可以省省了。
当然,如果Python愿意舍弃一部分动态性,像Cython一样可以将部分对象声明为静态类型,使用编译期属性绑定的话,定会让性能有大幅提升。
而C#的动态性能我并没去测,不知道是否和IronPython的性能差不多。

最后想说的是,由于Java糟糕的动态性能,我可能必须得放弃这种丑陋的写法,而转向更为丑陋的if...else硬编码。

11条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?