前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为什么一定要弄一个Builder内部类?

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

作者头像
山行AI
发布2019-06-28 12:06:53
7510
发布2019-06-28 12:06:53
举报
文章被收录于专栏:山行AI

作者:大宽宽

知乎链接: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了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档