且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-04-29 08:29:38

问题概述

随着项目进度不断地深入,类的设计将会越来越复杂,常常遇到这样的情况:设计的类要用到很多属性,有的是必须要有的(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和链式配置呢?