1.使用场景
Builder模式主要用于复杂对象(构造方法的参数过多)的创建、对象构造参数可选配置、创建不可变对象。实际开发多用于开源框架中配置对象的创建。详见 3.范例演示
下面首先介绍为啥要用Builder模式创建对象?
>>使用共有构造方法的问题:
- 不能灵活控制可选构造参数,使用包含所有参数的构造方法,不需要的参数也必须传,同时容易出现参数错位,不很容易发现。(构造方法重载可解决)
- 通过重载的构造方法可定制不同参数的构造方法,但可读性差,因为构造方法名完全相同,缺乏语义性,很难区别,只能根据参数列表选择。(使用JavaBean方式可解决)
>>使用JavaBean设置参数的问题:
- 通过set方法可灵活设置参数,可读性不错,但参数设置分为几次调用,不能保证对象状态的一致性,需要人为处理额外努力保证线程安全。
- public set方法阻止把类做成不可变类的可能。我们可能希望一个对象创建后不能被修改
>>Builder模式创建对象好处?
- 拥有JavaBean模式的可读性,可选设置对象参数,可以实现链式调用,代码简洁
- 拥有重载构造方法创建对象的安全性
- 构造参数校验和对象创建分离,创建对象前可完成参数校验
2.使用方式
- 不直接生成想要的对象,通过必要参数调用构造器,返回一个构造器对象Builder(此构造器对象拥有和目标对象的相同的成员变量)
- 通过构造器的不同参数的set方法,设置参数给构造器
- 最后通过Builder.build()方法,返回目标对象的实例,完成对象创建。
//设置远程过程调用Rpc请求的参数
RpcRequest request = new RpcRequest.Build()
.interfaceName(method.getDeclaringClass().getName())
.serviceVersion("0.0.1")
.methodName(method.getName())
.parameterTypes(method.getParameterTypes())
.parameters(args).build();
3.范例演示
3.1 RpcRequest
场景描述:RpcRequest对象封装rpc(远程过程调用)的调用请求信息,包含远程服务的接口名、服务版本号、请求调用方法、请求参数类型、请求参数信息。RpcRequest对象的构造参数较多,包含一些可选参数,比如请求超时timeout信息,另外未来可能增加其他请求参数。同时请求对象一旦创建,对象信息不希望被修改。因此通过Builder构建者模式,为其创建对象实例。
package com.myron.netty.rpc;
import java.io.Serializable;
import java.util.UUID;
/**
* 封装 RPC 请求
* @author lin.r.x
*
*/
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 1L;
private String requestId;
private String interfaceName;
private String serviceVersion;
private String methodName;
private Class<?>[] parameterTypes;
private Object[] parameters;
public RpcRequest(Builder build) {
this.requestId = build.requestId;
this.interfaceName = build.interfaceName;
this.serviceVersion = build.serviceVersion;
this.methodName = build.methodName;
this.parameterTypes = build.parameterTypes;
this.parameters = build.parameters;
}
// 目标对象只有get方法无set方法,保证对象创建后,私有成员变量不被修改
public String getRequestId() {
return requestId;
}
public String getInterfaceName() {
return interfaceName;
}
public String getServiceVersion() {
return serviceVersion;
}
public String getMethodName() {
return methodName;
}
public Class<?>[] getParameterTypes() {
return parameterTypes;
}
public Object[] getParameters() {
return parameters;
}
//RpcRequest解决Request参数过多,未来会添加新的参数,引入Build模式
public static class Builder {
private String requestId;
private String interfaceName;
private String serviceVersion;
private String methodName;
private Class<?>[] parameterTypes;
private Object[] parameters;
// 默认Build构造方法,用于初始化默认参数
public Builder() {
this.requestId = UUID.randomUUID().toString().replace("-", "");
}
// 带参数的Build构造方法,用于初始化必选参数
public Builder(String requestId) {
this.requestId = requestId;
}
// 传递构造器对象给目标类的构造方法,返回目标对象实例
public RpcRequest build() {
RpcRequest request = new RpcRequest(this);
return request;
}
// 设置参数,并返回构造器本身,用于链式调用设置可选参数
public Builder interfaceName(String interfaceName) {
this.interfaceName = interfaceName;
return this;
}
public Builder serviceVersion(String serviceVersion) {
this.serviceVersion = serviceVersion;
return this;
}
public Builder methodName(String methodName) {
this.methodName = methodName;
return this;
}
public Builder parameterTypes(Class<?>[] parameterTypes) {
this.parameterTypes = parameterTypes;
return this;
}
public Builder parameters(Object[] parameters) {
this.parameters = parameters;
return this;
}
}
}
使用Builder创建RpcRequest实例
//设置远程过程调用Rpc请求的参数
RpcRequest request = new RpcRequest.Build()
.interfaceName(method.getDeclaringClass().getName())
.serviceVersion("0.0.1")
.methodName(method.getName())
.parameterTypes(method.getParameterTypes())
.parameters(args).build();
说明:以上对象创建的构造器,是通过类的静态内部类来实现,将构造器封装在类内部,这种方式使用Builder,使得代码冗长,在实际开发中也有将构造器独立于外部类,下面通过开源框架netty和strom,两个例子了解Builder设计模式运用。
3.2 netty中运用场景
netty 是提供nio的网络通信框架,这里主要关注Netty服务端启动需要先创建服务器启动辅助类ServerBootstrap,它提供了一系列的方法用于设置服务器端启动相关的参数。然而在创建ServerBootstrap实例时,发现ServerBootstrap只有一个无参的构造函数,事实上它需要与多个其它组件或者类交互。ServerBootstrap构造函数没有参数的原因是因为它的参数太多了,Netty创造者为了解决这个问题,就引入了Builder模式。
package com.myron.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class TimeServer {
public void bind(int port) {
//配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
//绑定I/O事件的处理类主要用于处理网络I/O事件,例如记录日志、对消息进行编解码
.childHandler(new ChildChannelHandler());
try {
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();//方法阻塞等待服务端链路关闭之后main函数才退出
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
// 使用默认值
}
}
new TimeServer().bind(port);
}
}
3.3 storm中运用场景
storm是一个流式实时计算的框架,主要用于大数据领域的实时计算。storm中有个重要的对象,Topology拓扑对象,用于描述实时计算任务中数据流的拓扑节点结构。计算拓扑 根据具体业务需要创建,因此非常适合Builder模式构建对象
演示计算wordcount的拓扑对象创建代码
package com.myron.storm.wordcount.topology;
import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.tuple.Fields;
import com.myron.storm.wordcount.bolt.ReportBolt;
import com.myron.storm.wordcount.bolt.SplitSentenceBolt;
import com.myron.storm.wordcount.bolt.WordCountBolt;
import com.myron.storm.wordcount.spout.SentenceSpout;
/**
* Topology定义计算所需要的spout和bolt
*
* @author Administrator
*
*/
public class WordCountTopology {
private static final String SENTENCE_SPOUT_ID = "sentence-spout";
private static final String SPLIT_BOLT_ID = "split-bolt";
private static final String COUNT_BOLT_ID = "count-bolt";
private static final String REPORT_BOLT_ID = "report-bolt";
private static final String TOPOLOGY_NAME = "topology-bolt";
public static void main(String[] args) throws Exception {
//实例化spout和bolt
SentenceSpout spout = new SentenceSpout();
SplitSentenceBolt splitBolt = new SplitSentenceBolt();
WordCountBolt countBolt = new WordCountBolt();
ReportBolt reportBolt = new ReportBolt();
//TopologyBuilder提供流式接口风格API定义topology组件之间的数据流
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout(SENTENCE_SPOUT_ID, spout);
// SentenceSpout --> SplitSentenceBolt
// BoltDeclarer.shuffleGrouping():告诉Storm,sentenceSpout 发射的tuple随机均匀分发给SplitBolt
builder.setBolt(SPLIT_BOLT_ID, splitBolt).shuffleGrouping(SENTENCE_SPOUT_ID);
// SplitSentenceBolt --> WordCountBolt
// BoltDeclarer.fieldsGrouping():告诉Storm,所有"word"字段值的tuple被路由到同一个WordCountBolt
builder.setBolt(COUNT_BOLT_ID, countBolt).fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));
// WordCountBolt --> ReportBolt
// BoltDeclarer.globalGrouping():告诉Storm,所有的WordCountBolt 路由到唯一ReportBolt任务中
builder.setBolt(REPORT_BOLT_ID, reportBolt).globalGrouping(COUNT_BOLT_ID);
// Config对象代表了对topology所有组件全局生效的配置参数集合
Config config = new Config();
//Storm本地模式模拟一个完整的集群
LocalCluster cluster = new LocalCluster();
//提交拓扑到集群
cluster.submitTopology(TOPOLOGY_NAME, config, builder.createTopology());
Thread.sleep(50000);
cluster.killTopology(TOPOLOGY_NAME);
cluster.shutdown();
}
}
4.原理分析
Builder模式构建复杂对象,同时拥有的公共构造方法和JavaBean方式(成员变量setter方法)优点,其实不难发现,Builder模式本质其实只是把上面两种方式组合起来,通过Builder对象的setter方法可选接受参数,调用Builder.build()方法时,将Builder对象接收到客户端参数,一次性调用目标类的带有全部参数的构造方法,并返回创建对象。这种通过第三方辅助类来协助处理,在其他设计模式也很常见。
最后想说,Builder模式增加Builder对象创建的开销,也增加代码量,因此在创建简单对象时,没必要使用。深刻理解每个设计模式针对的场景,比硬记重要。另外了解在开源产品中使用,可以大大加深理解,看看大咖是怎么用的。