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

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

问题概述

随着项目进度不断地深入,类的设计将会越来越复杂,常常遇到这样的情况:设计的类要用到很多属性,有的是必须要有的(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和链式配置呢?
相关文章
|
机器学习/深度学习 监控 算法
yolov8+多算法多目标追踪+实例分割+目标检测+姿态估计(代码+教程)
yolov8+多算法多目标追踪+实例分割+目标检测+姿态估计(代码+教程)
|
机器学习/深度学习 存储 编解码
Open3d系列 | 3. Open3d实现点云上采样、点云聚类、点云分割以及点云重建
Open3d系列 | 3. Open3d实现点云上采样、点云聚类、点云分割以及点云重建
15274 1
Open3d系列 | 3. Open3d实现点云上采样、点云聚类、点云分割以及点云重建
|
人工智能 自然语言处理 Devops
云效 AI 智能代码评审体验指南
云效AI智能代码评审正式上线!在合并请求时自动分析代码,精准识别问题,提升交付效率与质量。支持自定义规则、多语言评审,助力研发效能升级。立即体验AI驱动的代码评审革新,让AI成为你的代码质量伙伴!
883 7
|
12月前
|
人工智能 Java Linux
Go 调度器:一个线程的执行流程
本文详细解析了Go语言运行时调度器的初始化流程,重点介绍了GMP模型的构建过程。内容涵盖调度器初始化函数`runtime·schedinit`、线程与处理器的绑定、P结构体的创建与初始化,以及主Goroutine的启动流程。通过源码分析,帮助读者深入理解Go运行时的底层机制。
271 0
|
机器人 C++ Python
ROS2教程 02 功能包
本文是关于ROS2(机器人操作系统2)中功能包(package)管理的教程,介绍了如何检查功能包的依赖、创建新功能包、列出可执行文件、列出所有功能包、查询功能包的位置和描述信息,以及为C++和Python功能包配置必要的文件。
976 0
|
人工智能 自然语言处理 Java
IDEA中使用DeepSeek满血版的手把手教程来了!
IDEA中使用DeepSeek满血版的手把手教程来了!
|
存储 关系型数据库 MySQL
PolarDB-X V2.3 集中式和分布式一体化开源发布
本文主要介绍PolarDB-X V2.3 集中式和分布式一体化开源。
|
小程序 前端开发 安全
微信小程序|大学生党务学习平台的设计与实现
微信小程序|大学生党务学习平台的设计与实现
382 1
|
JavaScript Java 测试技术
基于小程序的基于微信小程序的外卖点餐系统
基于小程序的基于微信小程序的外卖点餐系统
247 1
|
弹性计算 负载均衡 Cloud Native
阿里云负载均衡SLB版本CLB、ALB和NLB有什么区别如何选择?
最初阿里云只有一种传统的负载均衡SLB,这款SLB实例是四层负载均衡;后来在传统的SLB基础上推出七层负载均衡ALB,原四层SLB改名为CLB;后来又推出基于NFV虚拟化平台,支持弹性伸缩的网络型负载均衡NLB
12040 24
阿里云负载均衡SLB版本CLB、ALB和NLB有什么区别如何选择?