如何构建含有大量参数的构造器:浅谈Builder Pattern的使用和链式配置

问题概述

随着项目进度不断地深入,类的设计将会越来越复杂,常常遇到这样的情况:设计的类要用到很多属性,有的是必须要有的(required fields),有些是可有可无的(optional fields),这样就要求向构造器(constructors)传递众多的参数,而如何合理构建这样需要大量参数的构造器就会成为一个棘手的问题。

举个例子,现在需要设计一个消息推送管理类,这个类被设计用来管理终端的推送消息服务,要求传入和消息推送服务器链接的所需要的信息、需要关注的主题列表,能够推送消息的主题列表以及各种配置选项信息。以上提到的这些信息都是正常启动服务所必需的,读者虽然不一定能完全理解这个类要做什么,但是可以感受到如果将这么多的数据都一股脑在构造器里传入,将是个比较糟糕的设计,客户端的代码将是难以编写和维护。

其实这样的问题在工业界很常见,比如在著名的网络通信框架Netty中,因为是基于NIO Socket通信,所以需要较为复杂的配置才能启动网络连接。那Netty框架中是如何处理这个问题的呢?我们看下实例:

      // Configure the bootstrap.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            //***Chain Builder Start***
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new HexDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
             .childOption(ChannelOption.AUTO_READ, false)
             ChannelFuture f = b.bind(LOCAL_PORT).sync();      
             f.channel().closeFuture().sync(); 
             //***Chain Builder End***
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }```

这是一段标准Netty框架中启动服务器端的代码,请注意从注释Chain Builder开始的这段“怪异的”代码,这是针对ServerBootstrap对象的链式配置,在链式配置过程中每次调用方法都会返回这个对象的自引用,以便于后续继续调用配置方法,这样就形成一个配置的“流水线”,在流水线上所有需要的参数都被传入,最后用来生成目标对象(这里的目标对象是ChannelFuture对象)。这样生成实例的模式被称为“***Builder Pattern***”(建造者模式)。

可能有的读者对于这样的设计模式感到奇怪:“为什么要多此一举把代码连成一串,分开一个一个地调用方法不是更清晰吗”?存在即合理, 要说明Builder Pattern优势和应用场景,我们首先要回顾下其他创建对象实例的方法,经过对比和分析之后,Builder Pattern的意义也就自然清晰了。

#传统创建对象实例的方法
最常见的解决方案是使用Telescoping Pattern(折叠模式)或者JavaBean模式。这两种方法的利弊都很明显,关于他们的分析文章很多,因为不是本文主要内容,所以只是简要讨论。用兴趣的读者可以参考[这篇文章](http://blog.csdn.net/dm_vincent/article/details/8517175)深入了解。
1. Telescoping Pattern (TP)
应用TP方法往往需要大量编写的代码,对于开发人员的压力比较大,而且不易扩展,只要新加一个要传入的参数,就需要重新构建一个新的构造器。此外,当参数很多时,由于传入参数的顺序是固定的,所以用户往往需要查阅文档才能找到应该调用那种构造器,用起来很不方便。过长的参数列表也容易造成错误,因参数顺序错误而造成的异常是很不好排查的。当然,这样的代码阅读起来也是很折磨,想一想一个构造器有七八个参数,没有注释是很难区分他们的,错一个都会有问题的。
2. JavaBean Pattern(JBP)
为了更灵活,JBP走了另一个极端,只提供一个无参构造方法,然后通过setters方法来设置必要的域和可选的域。这样代码的编写和阅读都变得更加容易,但是却带来了新的安全问题:用户可能在必要属性还没有配置完全好的情况就开始使用类对象。

如何让代码优雅地保持JBP方法的灵活性同时又有着TP方法的安全性,这边是Builder Pattern要做到工作

#分析Builder Pattern
接下来我们看看Builder Pattern是如何工作的,下面是《Effective Java》中给出的例子:

//Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder {
    // Required parameters
    private final int servingSize;
    private final int servings;
    // Optional parameters - initialized to default values
    private int calories = 0;
    private int fat = 0;
    private int carbohydrate = 0;
    private int sodium = 0;
    public Builder(int servingSize, int servings) {//Required parameters
        this.servingSize = servingSize;
        this.servings = servings;
    }
    public Builder calories(int val){ 
           calories = val;
           return this; // return self reference
    }
    public Builder fat(int val)
        { fat = val; return this; }
    public Builder carbohydrate(int val)
        { carbohydrate = val; return this; }
    public Builder sodium(int val)
        { sodium = val; return this; }
    public NutritionFacts build() { // return desired type object
        return new NutritionFacts(this);
    }
}

private NutritionFacts(Builder builder) {
    servingSize = builder.servingSize;
    servings = builder.servings;
    calories = builder.calories;
    fat = builder.fat;
    sodium = builder.sodium;
    carbohydrate = builder.carbohydrate;
}

}```

Builder Pattern的工作一共分为三大步:

  • 首先在类中创建静态类Builder,其构造函数中的参数为构建类对象所必需的参数;
  • 接下来调用setter-like方法设置其他备选参数,并且这些方法都会返回Builder对象的自引用
  • 最后通过build()方法,将整个Builder类实例传递给目标类的私有构造函数中,创建目标对象。

最后创建对象的代码范例是这样的:

NutritionFactscocaCola = new NutritionFacts.Builder(240, 8).
    calories(100).sodium(35).carbohydrate(27).build();```

也就是我们在上面提到链式配置。这样“流水线”配置可以优雅清楚地设置所需的参数,同时,在通过Builder类的帮助下,只有在所有配置参数都设置完毕之后才会去生成对象,如果这个时候发现有参数没有配置或者其值不符合要求,就可以在build()方法中直接抛出异常,避免生成对象。

所以Builder Pattern集TP和JBP的优点于一身,安全而灵活,读写皆易懂,可扩展能力强。

最后我们在回过头来分析下前面提到的Netty框架中创建服务端Channel的代码

ServerBootstrap b = new ServerBootstrap();
//Chain Builder Start
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new HexDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
.childOption(ChannelOption.AUTO_READ, false)
ChannelFuture f = b.bind(LOCAL_PORT).sync();
f.channel().closeFuture().sync(); ```

ServerBootstrap类就相当于Builder类,用于辅助设置所需的参数(类的名字就可以看出它的作用,只不过独立存在而没有作为目标类的内部类),流水作业配置将引导用户得到最终的目标对象——ChannelFuture实例。

值得注意的是,因为还需要对ChannelFuture对象进行连续操作,所以后面的操作继续采用了Builder Pattern中链式操作的模式,来保证代码编写的流畅性和优雅,直接如下那样“一链到底”也是未尝不可。

b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 .handler(new LoggingHandler(LogLevel.INFO))
 .childHandler(new HexDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
 .childOption(ChannelOption.AUTO_READ, false)
 .bind(LOCAL_PORT).sync();      
 .channel().closeFuture().sync(); ```

这充分体现了Netty框架在设计上良苦用心,以及Builder Pattern内在的精髓。

不过这里要说明的,Netty框架很是复杂,并不是简单的套用了某种设计模式,往往多种模式根据实际情况混合使用,来达到最佳效果。这里的Netty实例代码其实并不是严格的Builder Pattern,但是却将其精髓链式配置的优势完善地体现出来,这也就要阅读优质源码的原因。

如果你手上也有需要大量参数的构造器或是需要连续配置和操作的对象,为什么不试试Builder Patter和链式配置呢?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容