构造函数沉思录
缘起
构造函数,是由C++引入主流程序世界的,其用意是在《C++语言的设计与演化》如是表达:
它建立起其它成员函数进行操作的环境基础。
在很早的一篇blog《对象的声明》中,我曾探讨过构造函数的来龙去脉。对于面向对语言而言,构造函数似乎是标配。
一个语言特性,一旦被扔到真实世界,随之而来的是,其使用往往会超出其设计者的初衷,构造函数亦是如此。
事实上,通过前面C++之父的描述,我们依然很难定位构造函数的准确用法。所以,我们常常看到许多人把诸多操作强塞入构造函数,造成构造函数极为复杂,进而关于导致了一些复杂的语法讨论,比如如何处理构造函数抛出的异常。
这里要讨论的是构造函数的另一个常见问题。
重载构造函数
同样是在《C++语言的设计与演化》里,有这样一段描述:
观察发现,允许定义多个构造函数很有价值,因此这也就成了C++重载机制的一个重要应用方面。
是的,我要说的就是构造函数重载。构造函数可以重载,Java和C#也拿了过来,这似乎成了一种约定俗成。
在实际应用中,有不少人会这么做:给一个类创建多个构造函数,有的初始化了全部的字段/成员变量,有的只初始化诸多字段/成员变量中的几个。下面便是一个例子:
public Image(URL url, Tag tag) {
this.url = url;
this.tag = tag;
}
public Image(URL url) {
this.url = url;
}
这么做的原因通常是,初写这些代码时,这些构造函数要用在不同的场合下。比如,在产品代码中,我们需要的可能是一个完整的对象,而在测试代码中,针对要测试的内容,我们只要设置几个字段即可。
但是,因为它们都是构造函数,名字完全一致,其最初的意图无法体现,后来的人看到这样的函数,图省事,便拿过来用。随后一些初始化不完整的对象就出现在系统中。随着系统的不断演进,残缺的对象在很多情况下就会出问题,于是,为了修补,我们再向代码里添加一些setter。与setter相伴的往往是,可变(mutable)对象的出现。而许多系统的状态不稳定就是由各种可变对象造成(这也是一个值得讨论的话题)。貌似很简单的构造函数蕴含着诸多的问题。
就这个问题而言,可以怎样解决呢?
一种常见的解决方案是,类只提供一个完整的构造函数,至于其它部分,则采用工厂方法完成。比如上面的例子,对Image类,我们只有一个构造函数:
public Image(URL url, Tag tag) {
this.url = url;
this.tag = tag;
}
如果在测试里用到,就为它创建一个工厂方法:
class ImageForTestFactory {
public static Image createImageWithURL(URL url) {
return new Image(url, null);
}
}
Image的第二个参数Tag,这里就简单的设置为null,事实上,我们可以根据需要进行设置。比如,我们需要所有的字段都不能为空,这里就可以提供一个缺省的Tag。这段代码的使用者根本无需顾及Tag究竟是怎样。
工厂方法很大的一个价值,便在于它提供了名字,表明意图。名字到底有多大价值,如果你对整洁代码(Clean Code)有所追求,便就会发现,关于整洁代码的讨论,第一个要讨论的东西便是命名。
事实上,这些做法并不如何特殊,《Effective Java》第二版,开篇讨论的就是这样的问题。条款1就是“考虑以静态工厂方法代替构造函数”。这个条款里面建议的方案更加激进,建议将构造函数设置为private。这样一来,人们就完全没有机会使用该类的构造函数,只能通过其提供的工厂方法构造对象:
class Image {
private Image(URL url, Tag tag) {
this.url = url;
this.tag = tag;
}
public static Image createNewImage(URL url, Tag tag) {
return new Image(url, null);
}
...
}
另外一种值得考虑的做法是,采用builder模式。《Effective Java》第二版的条款2给出了更详细的解释。所以,如果类里有多于一个的构造函数,那么请考虑其它方式代替。
无构造函数的Go
顺着这个思路,再进一步,我们完全可以写出“除本类之外,没有new本类对象的代码”。换句话说,如果不是语言层面有所限制,我们完全可以抛弃构造函数,而事实上,Go语言就这么做了。
在Go语言里,如果我们要构造一个对象可以这么做:
type Person struct {
name string
}
func NewPerson(name string)(*Person) {
p := new(Person)
p.name = name
return p
}
在语法上,Go语言本身并没有类,但从C/C++的年代我们就知道,struct和class本质是一样的。所以,实际上,NewPerson就是一个构造函数,它负责初始化了Person的相关字段。构造一个对象的方法就可以这样:
p := NewPerson(\"dreamhead\")
从本质上说,NewPerson就是一个工厂方法。当然,Go语言之所以可以这么做,因为其struct的所有字段都是public,可以自由访问,在面向对象程序设计语言中,恐怕没那么简单。或许,抛开构造函数的做法,让我们一下子回归到了最初的年代,但同之前模糊的印象不同,如今我们对构造的概念有了全新的理解。我们依然要构造对象,只是不再依赖于构造函数而已。
是时候反思一下构造函数了。C++设计于80年代,那时候,设计模式还不是主流,那时候,编写代码更多强调的是功能,而非整洁。
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:dreamhead 来源: 梦想风暴
- 标签: 构造函数
- 发布时间:2012-05-28 13:21:05
- [55] IOS安全–浅谈关于IOS加固的几种方法
- [55] Oracle MTS模式下 进程地址与会话信
- [54] 如何拿下简短的域名
- [53] android 开发入门
- [52] Go Reflect 性能
- [52] 图书馆的世界纪录
- [49] 读书笔记-壹百度:百度十年千倍的29条法则
- [47] 【社会化设计】自我(self)部分――欢迎区
- [38] 程序员技术练级攻略
- [33] 视觉调整-设计师 vs. 逻辑