前言
有的时候我们学习某一种语言的时候,会禁不住和其他一些自己熟悉的语言针对某些特性做一番比较。不同的语言带来的特性在某些更深的层次有它的设计思想。在解决一些问题的时候也带来一种独特的思路。这里从python的optional arguments特性开始引申讨论在java中对象创建扩展中一系列的问题和解决思路。这些解决的方法里揉和了一些设计模式的套路。到最后,针对这些实现来重新思考设计模式。
optional, named argument
在目前的java语言里是不支持optional, named argument这样的语言特性的。这个特性在python里则很常见。我们来看一些python里常见的代码:
class Node:
def __init__(self, wordList=None, word=""):
if wordList is None:
self.wordList = []
else:
self.wordList = wordList
self.word = word
这里,我们相当于定义了一个类以及它的构造函数。比较有意思的是,这里构造函数的参数定义是可选的,其中wordList, adjacencyList的默认值可以为空。这种特性有一个什么好处呢?在一些定义的构造函数里,我们可以根据需要来提供参数。比如说下面的代码:
node = Node([1, 2, 3])
node = Node(word = "abc")
这里,optional argument说明了一个现象,就是在一些方法或者构造函数里,有的时候我们并不是需要用到所有的成员变量。这个时候,如果我们只是提供一些必须的参数,而其他不需要的参数则设置为某个特定的默认值就好了。在java里头,由于没有这个语法特性的支持,如果我们也要实现类似的效果,则很显然,必须要设置多个构造函数。比如说前面的构造函数,如果我们要用java实现多个可选参数的话,可能会用代码实现如下:
public class Node {
private List wordList;
private String word;
public Node(List wordList, String word) {
this.wordList = wordList;
this.word = word;
}
public Node(List wordList) {
this.wordList = wordList;
}
public Node(String word) {
this.word = word;
}
}
和前面的代码比起来,这里只能算是一个勉强接近的实现。我们从代码里可以发现,为了实现一个同等的可选参数效果,我们可能需要定义多个构造函数。在这里定义的代码里,实际上在调用一些构造函数时并没有给一些参数提供指定的默认值。当然,前面的代码还有如下的一种写法:
public class Node {
private List wordList;
private String word;
public Node(List wordList, String word) {
this.wordList = wordList;
this.word = word;
}
public Node(List wordList) {
this(wordList, "");
}
public Node(String word) {
this(new ArrayList(), word);
}
}
总而言之,如果我们要实现可选参数的方法时,必须要提供一个包含所有参数的一个构造函数。然后再根据需要来一个个的提供必要参数的构造函数。和前面支持optional argument语法的python比起来,显得繁琐多了。试想一下,如果我们这里有3个可选参数,而如果我们需要去考虑他们每个参数需要设置默认参数或者不需要设置的情况,则存在有8种可能性。在面对这种排列组合的可能性时,如果再去每个可能都提供一个构造函数的话则明显的不明智了。
唉,谁让java不支持这个特性呢?那么如果我们面临前面这种情况的时候,该如何去处理呢?
一种通用的解决方法
这里面临的一个困境就是我们这里有若干个参数是可选的。也就是说我们可能会需要设置它们,也可能不需要。这种自由度在这里却成为了一个大麻烦。这里我们可以从另外一个思路去考虑。
如果我们有一个对象,专门用来设置我们自己需要的参数就好了。这样,我们就可以用通过调用一个对象的方法来设置我们必要的参数,而如果我们不需要的话,则不调用就行了。而对于我们需要构造的对象来说,一些他们必须的参数,我们可以在构造对象的参数里默认提供。这里的描述还是有点空泛,可以先看一个典型的示例代码:
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
这里的代码看起来比较长,实际上不过是在NutritionFacts类里头定义了一个内部类Builder,由Builder来代理实现一些参数的赋值。这里就模拟出来了可选参数的效果。
比如说,我们可以使用如下的方法来构造对象:
Builder builder = new Builder(0, 0);
NutritionFacts facts = builder.calories(1).sodium(2).build();
facts = builder.calories(1).carbohydrate(2).build();
这里的一个好处就是我们可以根据需要来调用必要的方法设置可选参数了。仔细查看以上的代码,我们可以发现,对于构造函数必须的参数,我们可以将其作为必须的参数设置到里面负责构造的对象里。而对于每个可选参数,我们通过设置对应方法来实现,每个方法返回一个当前对象。这里灵活性确实是增加了,不过看上去这么做还是有点不合算,为了几个可选参数至于吗?
实际上,这样做还有一个好处,就是对于复杂对象构造的过程分离。在上面的示例代码里,因为每个必选参数或者可选参数都很简单,我们就算是拆分出来一个方法也没什么好处。而在某些情况下,比如说我们要构造的对象比较复杂时,每个构造可选参数的方法可能就牵涉到若干个步骤了。这里相当于将每一个具体参数的准备和构造都分离出来。有利于维护性。
关于这部分,还可以结合另外一个具体的示例来讨论他们之间的联系。
关于对象创建的示例
在最近做的一个项目里,有一个类似的对象构造问题。我们通过调用一些restful webservice访问服务器端,服务器会返回一系列json格式的访问结果。而我们需要从这些数据结果里提取必要的参数来构造对象。因为每个参数的提取都需要若干的流程,而且相对比较复杂,我们首先考虑到的就是需要对每个参数的提取做解析。那么,这些方法该定义到哪里呢?该怎么定义呢?因为他们主要只适用于这个对象本身的创建,如果提取出来作为单独的类来使用意义不大。而且他们是要将结果提供给构造函数用的,我们就不能将他们定义成实例方法,所以我们可以将他们定义成static方法。一种实现的思路如下:
public class ServiceRef {
private String serviceURL;
private Stromg serviceName;
private Map<String, String> serviceMap;
privatestatic String parseServiceURL(String result) {
// parse the data and retrieve serviceURL
}
private static Map<String, String> createServiceMap(String result) {
// parse result data to construct the map.
}
}
这里列了一点点代码实现的思路。好了,既然我们这样实现了对每个参数的解析,如果要构造函数的时候,该怎么办呢?一种比较直接的办法就是定义一个专门的static方法来创建ServiceRef对象。部分伪代码可以定义成这样:
public static ServiceRef createInstance(String result) {
String serviceURL = parseServiceURL(result);
Map<String, String> serviceMap = createServiceMap(result);
return new ServiceRef(serviceURL, serviceMap);
}
// define a private constructor
private ServiceRef(String serviceURL, Map<String, String> map) {
// ... omitted
}
这里的代码提供了另外一种思路来实现创建一个复杂的对象。之所以举这个实例是要和前面一节的builder方式做一个比较。对于一个对象,如果它所有项都是必须的,当前的方法就已经足够了。可是如果我们在这里需要考虑可选参数呢?这个时候我们就会看到这种方法的不足,前面那个部分的方法里只要在每个方法里加入对应参数的解析就可以了,这里则很困难。通过这个方法和前面的比较,我们可以发现前面的方法的灵活性。
进一步思考
前面讲到可选参数对象构造时,书上说这是用到了builder pattern。既然讨论到这里,我们就来好好的学习一下这个pattern的精神吧。builder pattern的官方描述说明如下: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
仔细想想,似乎我们前面的讨论不完全符合这个描述啊。实际上我们前面的情况是一种builder pattern严重退化的情景。因为我们不需要考虑同样构建过程的不同表示,我们这里只有一个表示。比如说我们解析文件示例里,只要解析json格式的文件就搞定了。构造的过程倒比较统一,就是那么几个参数。那么,一个看上去比较完整的builder pattern该是什么样的呢?实际上有了我们前面的讨论,完全可以推导出来。
以我们前面讨论的文件解析对象构造为例,这里我们要构造的若干个参数相对来说是一个固定的过程。可是我们这里的实际实现是解析json格式的内容。这里却是和一个具体的实现绑定在一起了。如果后面项目需求变化了,要求我们能够根据xml格式的内容来构造同样的对象,那我们该怎么办呢?
这个好办,按照面向对象的设计思想抽象不变的地方,封装变化的地方。这里我们可以将构造serviceURL, serviceMap等参数的方法作为一个封装的接口抽象出来。针对这些参数的构造不同实现则实现这个接口。于是原有的类关系则变成如下了:
这里我们定义的每个具体builder对象实现了创建目的对象里部分参数的目标。而如果我们将创建整个对象的职责都放到builder的实现里,然后运用像前面部分参数的手法,我们将得到一个类似于如下的类关系图:
我们做了一点职责分离的调整,这个图就是完整的builder pattern的类结构图了。关于builder pattern的具体讨论在网上有很多,这里就不去详述了。主要是讲述一下它的来由。
总结
这篇文章从optional arguments这个python所拥有的特性讨论起,引申到java里要实现类似特性所需要的手法。再通过这些手法的进一步扩展讨论到了相关的模式和设计思路。总体来说,涵盖的范围比较发散,更多是最近接触到的一些具体问题的思考。通过对这几个问题的具体分析和思考,又唤起我重新思考设计模式的意义。从最初书本上定义的一种对特定场景问题的特定解决方法,我们也可以看到,具体的实现手法实际上是基于一种语言或者一种思考方式的。一些看似巧妙的解决手法换一个角度去看的时候,会发现是一种对目前语言设计的局限性的体现。所以说,一些看似繁琐的模式关系以及特性在另外一种思路里不过是一个普通的语言特性而已。模式也仅仅是一些招式而已。
参考材料
http://www.diveintopython.net/power_of_introspection/optional_arguments.html