Effective Java 2 遇到多个构造器参数时要考虑使用构建器

2024-06-10 02:04

本文主要是介绍Effective Java 2 遇到多个构造器参数时要考虑使用构建器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第2个经验法则:用遇到多个构造器参数时要考虑使用构建器(consider a builder when faced with many constructor parameters)

上一条讨论了静态工厂相对于构造器来说有五大优势。但静态工厂和构造器有个共同的局限性:它 们都不能很好地扩展到大量的可选参数。

对于需要多参数的类,应该用哪种构造器或者静态工厂来编写呢? 接下来,我将通过Java代码示例来对比分析构造器模式、JavaBeans模式以及建造者模式在处理多参数情况下的应用。

构造器模式

假设我们要创建一个 Car 类,它有颜色、品牌、型号和价格等属性,其中颜色和品牌是必需的, 而型号和价格是可选的。

这其实就是重叠构造器(telescoping constructor)模式。程序员一向习惯采用这种模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依此类推,最后一个构造器包含所有可选的参数。随着参数增多,构造器的重载会变得复杂且难以管理。

简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写并且仍然较 难以阅读。如果读者想知道那些值是什么意思,必须很仔细地数着这些参数来探个究竟。一长串 类型相同的参数会导致一些微妙的错误。如果客户端不小心颠倒了其中两个参数的顺序,编译器 也不会出错,但是程序在运行时会出现错误的行为。

JavaBeans模式

采用JavaBeans模式,我们首先定义一个无参构造器,然后通过setter方法设置属性。

这种模式弥补了重看构造器模式的不足。说得明白一点,就是创建实例很容易,这样产生的代码 读起来也很容易。

遗憾的是,JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中在构造过 程中,JavaBean 可能处于不一致的状态,导致对象在完全配置前处于不一致状态。尤其是在多线程环境下或构建过程较长的情境中。下面通过一个具体例子来进一步说明这一问题:

假设我们有一个 Order 类,用于表示在线商店中的订单信息,包括客户ID、商品列表、总价等属 性。使用JavaBeans模式,类定义如下:

假设在某个服务中,我们打算创建一个订单并填充相关信息,但这个过程是分步进行的,可能涉 及多个操作或方法调用,如下所示:

在这个例子中,如果在设置完商品ID之后,程序因为某些原因(如异常抛出、线程切换)没有机 会执行设置总价的逻辑,那么 Order对象就被留在了一个不一致的状态:它有客户ID和商品列 表,但缺少了总价信息。如果此时对象被其他部分的系统使用,可能会引发逻辑错误或计算问题。

另外,在多线程环境下,如果不加锁或其他同步措施,多个线程同时调用setter方法设置不同属性,还可能引起竞态条件,进一步加剧数据的不一致性。

因此,虽然JavaBeans模式提供了灵活性,但在构建过程中必须谨慎管理对象状态的完整性,特 别是在多步骤或多线程场景中,以避免数据不一致的问题。相比之下,建造者模式在这种情况下 提供了更好的解决方案,因为它能确保对象在构建完成之前是一个完整且一致的状态。

建造者模式 (Builder Pattern)

建造者模式通过引入Builder类来解决上述问题(既能保证像重看构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性),保持了代码的清晰度和对象的完整性。

建造者模式通过链式调用来设置参数,既保证了代码的可读性,也确保了对象的完整性,尤其适 合参数较多且有可选参数的情况。

它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到 一个builder对象。然后客户端在 builder 对象上调用类似于 setter 的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成通常是不可变的对象。

在这个过程中,每次创建Car对象,都会先实例化一个Car.CarBuilder对象,然后通过一系列链 式调用来设置属性,最后调用build()方法生成Car对象。虽然Builder模式提供了清晰的构建 逻辑和良好的可读性,但每个Car对象的创建实际上涉及了两次对象实例化:一次是Builder对 象,一次是最终的Car对象。

想象一个高性能的金融交易系统,每秒需要处理数百万次交易请求。为了优化内存使用和减少GC(垃圾回收)的压力,系统中的每一个环节都需尽可能地高效。在这样的系统中,交易对象的创建频繁且量大,哪怕是最小的性能损失也可能在大规模操作中被放大。

Builder模式不仅可以应用于单个类的复杂对象创建,也非常适合应用于类层次结构,以保持代码的一致性和扩展性。下面通过一个电子产品类层次结构的例子来说明这一点:假设我们有一个基本的Electronics类,以及它的两个子类Smartphone和Laptop,每个类都有其特有的属性。

首先,我们定义一个基础的Electronics类和对应的ElectronicsBuilder。抽象的Electronics类代表了一般的电子产品,包含了一些基础属性,比 如品牌(brand)、型号(model)和价格(price)。同时定义了一个内部抽象类 ElectronicsBuilder,作为构建电子产品实例的模板。这个Builder类定义了一些通用的设置方法(如设置品牌、型号和价格),并且通过泛型参数 T 来确保Builder自身类型的安全返回,即所谓的 fluent interface(流畅接口)设计,让设置过程可以链式调用。

接下来,定义Smartphone类,它继承自Electronics,并新增了特有的属性 storageCapacity。相应的,我们也会创建一个SmartphoneBuilder来构建Smartphone实例。 SmartphoneBuilder继承自E lectronicsBuilder 。 SmartphoneBuilder 除了继承来的通用设置方法外,还添加了一个设置 存储容量的方法。通过覆写 self() 方法返回当前Builder的类型,确保了类型安全和链式调用的延续。

同样地,定义Laptop类,对应的LaptopBuilder负责构建Laptop实例。Laptop类有自己的特性属性——屏幕尺寸 (screenSize )。对应的 LaptopBuilder 同样继承自ElectronicsBuilder的方法,并实现了自己的 self() 和 ElectronicsBuilder ,添加了设置屏幕尺 build() 方法,以适应 Laptop 的构建需求。

现在,可以这样使用Builder模式来创建不同类型的电子产品,并保持代码的清晰和类型安全:

通过这种方式,Builder模式不仅解决了复杂对象的构建问题,而且在类层次结构中保持了良好的扩展性和一致性,使得每个子类都能拥有自己特性的Builder,同时复用了基类Builder的部分逻辑,减少了代码重复,提升了代码的可维护性。

Builder 模式的确也有它自身的不足:

为了创建对象,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况下,可能就成问题了。 

Builder模式还比重看构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个 或者更多个参数。

简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是一 种不错的选择。 特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用Builder 模式的客户端代码将更易干阅读和编写,构建器也比JavaBeans加安全。

这篇关于Effective Java 2 遇到多个构造器参数时要考虑使用构建器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1046924

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数