为什么一定要弄一个Builder内部类?

2019-06-28 12:06:53 浏览数 (1)

作者:大宽宽

知乎链接:https://www.zhihu.com/question/326142180/answer/697172067

使用Builder大概有两个用途

解决具有大量参数的构造函数不好用的问题

解决让Object始终保持valid状态的问题

下面分别说说两个用途。

Java支持使用constructor初始化一个object,但是如果object的成员非常多,constructor就不好用了。巨长的参数列表很难分清楚哪个是哪个。这时最好用某种方式让哪个字段是哪个更加清晰一点。Builder模式算是可以解决这个问题。此外题主在题目中那种让setter返回this的形式也可以解决。并且,能用"."连接下来配合IDE的自动提示还能够提高编码速度,这算是一个额外的便利。

插播一下这种返回this的setter并不符合java bean的setter规范。但一般来讲,严格遵守这个规范的意义不大。 构造函数不够好用是Java语法的一个根本性问题。有其他语言对此有更好的语法支持,比如kotlin,本来就支持用key=value的形式初始化一个object:

class Foo (name:String, age:Int) // ... val bar = Foo(age = 3, name = "John") 再比如javascript完全可以用object literal这样做。

let foo = new Foo({ key1: "VALUE1“, key2: 2, key3: 3.15, key4: true }); 尽管如此,不使用Builder,直接使用setter可能会得到一个状态不完整的object。这就是第二点作用。也是题主写法解决不了的。

如果追一下oop设计的本源,一个object从出生开始到被销毁,应该始终维护一种“不变量“(invariant),或者叫做始终处于valid状态。

比如,设计了一个Rectangle类,有两个字段width和height。为了让object的状态始终valid,我要求width和height必须都是正数,并且width与height之积不得超过100。假设class的定义是这样的:

public class Rectangle { private int width; private int height; } 如果要用setter初始化的话,那么object就会在完全被set好之前处于“invalid”的状态。

Rectangle r = new Rectange(); // r is invalid, not good r.setWidth(2); // r is invalid, not good r.setHeight(3); // r is valid 为了避免这种invalid状态的发生,就要求使用构造函数一次性初始化好所有的成员。但是如果是一个很多成员的class,构造函数不好用,那么唯一合理的办法就是做一个builder。这样builder就可以分多步初始化所有成员,build的结果出来就是一个处于valid状态的object。

当然,这个事情并不一定要这么解决,比如如果业务允许,你可以给Rectangle的成员设置合理的初始值,然后再用setter改,像这样:

public class Rectangle { private int width = 1; private int height = 1; } Rectangle r = new Rectangle(); r.setWidth(3); r.setHeight(4); 有一类很特别的object就是“不可变object”。不可变是个很好的避免程序出问题的方法。具体“为什么不可变是一件好事情”的原因这里不展开了。为了得到一个不可变的object,是不可能使用任何setter方法的,必须使用构造函数一上来就把所有数据都设置好。因为多参数构造函数不好用,所以这里就得靠builder。

public class Rectangle { private final int width; private final int height; public Rectangle(int w, int h) { if (w <= 0 || h <= 0 || w * h > 100) { throw new RuntimeException("this rectangle is not valid!"); } this.width = w; // 留意因为是final,所以必须在这里就初始化 this.height = h; } }

public class RectangleBuilder { private int width; private int height; public RectangleBuilder setWidth(int w) {this.width = w; return this;) public RectangleBuilder setHeight(int h) {this.height = h; return this;} public Rectangle build() { return new Rectangle(this.width, this.height); } }

RectangleBuilder rb = new RectangleBuilder(); rb.setWidth(3).setHeight(2); Rectangle r = rb.build(); java对于final成员的要求是最晚构造函数得初始化,否则编译报错。这在有些时候不太好用。我们可能希望某个字段在第一次设置后就可以保持不变了。kotlin有个lazyinit的保留字实现了这个特性。 此外,上面这一坨代码就是Builder模式的正规写法,非常的繁琐。好在有Lombok的@Builder帮忙自动生成,不需要手写。 此外,也许你并不在意对象一直处于valid状态,只要在真正使用成员干活之前确保valid就行。那么就直接在干活前加判断是否valid就好。

public class Rectangle { private int width; private int height; // ... public draw() { if (width <= 0 || height <= 0 || width * height > 100) { throw new RuntimeException("this rectangle is not valid!"); } // do the real drawing work } } 如果这样做都不够灵活,你甚至都可以做一个public isValid()的方法让外界在调用关键动作之前,手动先验证Object的状态是valid。

还有一大类Object其实是“Data Object”,即用来做数据结构的。比如函数间传递一些参数,从接口或者数据库读出来的数据要有个存的地方等。这类Object压根就没什么“valid状态”一说,或者说,其是否合法完全是看业务场景的上下文,难以仅通过Object里数据本身就能判定的。对于这类object,直接用java bean规范new一个出来,然后挨个set就好。或者,按照我的想法,setter都是多余的,全部public成员直接赋值就足够了。

注意区分Data Object和面向对象里的那个Object,它们本质上是不同的东西 总结一下,如果你用Java,并且:

你的类里的成员很多

你希望维持object自始至终处于valid状态/不可变

那么你需要一个builder。至于是不是内部类我觉得都可以。也许内部类会让人觉得“XXX.Builder属于XXX“,感觉上好些。

但是做了Builder后,还要做些额外工作告诉类的使用者“你应该用builder来创建object,而不是直接new“,这需要一些沟通、文档之类的工作量。

反之,如果:

你的object字段数量很少,构造函数够用了

你压根就不在意object始终处于valid状态,或者你有别的规范来约束object是不是能用来干活

你的object是“data object”

那八成就不太需要做个builder。题主的写法也许已经足够好了。

对于一些语言,如kotlin,javascript,scala,python等,因为他们的语法本身就能支持builder的功能,基本上也就不需要手工实现builder了。

0 人点赞