构造函数沉思录

2012-04-23 21:23

构造函数沉思录

by dreamhead

at 2012-04-23 13:23:00

original http://dreamhead.blogbus.com/logs/207936339.html

缘起

构造函数,是由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年代,那时候,设计模式还不是主流,那时候,编写代码更多强调的是功能,而非整洁。