实践中的构建者模式

我将不深入讨论该模式,因为已经有大量的帖子和书籍对此进行了详细的解释。 相反,我将告诉您为什么以及何时应该考虑使用它。 但是,值得一提的是,这种模式与《 四人帮》一书中介绍的模式有些不同。 虽然原始模式着重于抽象化构造步骤,以便通过更改所使用的构建器实现可以得到不同的结果,但本文中说明的模式用于消除不必要的复杂性,该复杂性是由多个构造函数,多个可选参数以及过度使用二传手。

假设您有一个包含大量属性的类,例如下面的User类。 假设您要使该类不可变(顺便说一句,
除非有充分的理由不这样做,否则您应该始终努力。 但是我们将在另一篇文章中讨论)。

public class User {private final String firstName;    //requiredprivate final String lastName;    //requiredprivate final int age;    //optionalprivate final String phone;    //optionalprivate final String address;    //optional
...
}

现在,想象一下您的类中的某些属性是必需的,而其他属性是可选的。 您将如何构建此类的对象? 所有属性都声明为final,因此您必须在构造函数中全部设置它们,但您也想让此类的客户端有机会忽略可选属性。

第一个有效的选择是拥有一个仅将必需属性作为参数的构造函数,一个将所有必需属性加上第一个可选属性作为参数的构造函数,而另一个将两个可选属性作为参数的构造函数,依此类推。 看起来像什么? 像这样:

public User(String firstName, String lastName) {this(firstName, lastName, 0);}public User(String firstName, String lastName, int age) {this(firstName, lastName, age, '');}public User(String firstName, String lastName, int age, String phone) {this(firstName, lastName, age, phone, '');}public User(String firstName, String lastName, int age, String phone, String address) {this.firstName = firstName;this.lastName = lastName;this.age = age;this.phone = phone;this.address = address;}

用这种方法构造类的对象的好处是它可以工作。 但是,这种方法的问题应该很明显。 当您只有几个属性时,没什么大不了的,但是随着数量的增加,代码变得更难以阅读和维护。 更重要的是,对于客户而言,代码变得越来越难。 我应该作为客户端调用哪个构造函数? 一个带有2个参数? 一个3? 不传递显式值的那些参数的默认值是多少? 如果我想为地址设置一个值而不是年龄和电话该怎么办? 在那种情况下,我将不得不调用接受所有参数的构造函数,并为那些我不在乎的参数传递默认值。 此外,具有相同类型的几个参数可能会造成混淆。 第一个String是电话号码还是地址?

那么对于这些​​情况我们还有什么选择呢? 我们总是可以遵循JavaBeans约定,在该约定中,我们有一个默认的no-arg构造函数,并且每个属性都有设置器和获取器。 就像是:

public class User {private String firstName; // requiredprivate String lastName; // requiredprivate int age; // optionalprivate String phone; // optionalprivate String address;  //optionalpublic String getFirstName() {return firstName;}public void setFirstName(String firstName) {this.firstName = firstName;}public String getLastName() {return lastName;}public void setLastName(String lastName) {this.lastName = lastName;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getPhone() {return phone;}public void setPhone(String phone) {this.phone = phone;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}
}

这种方法似乎更易于阅读和维护。 作为客户端,我可以只创建一个空对象,然后仅设置我感兴趣的属性。那么这有什么问题呢? 该解决方案有两个主要问题。 第一个问题与使此类的实例处于不一致状态有关。 如果要创建一个具有其所有5个属性值的User对象,则在调用所有setX方法之前,该对象将不具有完整状态。 这意味着客户端应用程序的某些部分可能会看到此对象,并假定该对象已经构造,而实际上并非如此。 这种方法的第二个缺点是现在User类是可变的。 您失去了不可变对象的所有好处。

幸运的是,对于这些情况,还有第三种选择,即构建器模式。 解决方案将如下所示。

public class User {private final String firstName; // requiredprivate final String lastName; // requiredprivate final int age; // optionalprivate final String phone; // optionalprivate final String address; // optionalprivate User(UserBuilder builder) {this.firstName = builder.firstName;this.lastName = builder.lastName;this.age = builder.age;this.phone = builder.phone;this.address = builder.address;}public String getFirstName() {return firstName;}public String getLastName() {return lastName;}public int getAge() {return age;}public String getPhone() {return phone;}public String getAddress() {return address;}public static class UserBuilder {private final String firstName;private final String lastName;private int age;private String phone;private String address;public UserBuilder(String firstName, String lastName) {this.firstName = firstName;this.lastName = lastName;}public UserBuilder age(int age) {this.age = age;return this;}public UserBuilder phone(String phone) {this.phone = phone;return this;}public UserBuilder address(String address) {this.address = address;return this;}public User build() {return new User(this);}}
}

需要注意的几个要点:

  • User构造函数是私有的,这意味着不能从客户端代码直接实例化此类。
  • 该类再次是不可变的。 所有属性都是最终属性,它们是在构造函数上设置的。 此外,我们仅为他们提供吸气剂。
  • 构建器使用Fluent Interface惯用法使客户机代码更具可读性(我们稍后将看到一个示例)。
  • 构建器构造函数仅接收必需的属性,并且此属性是在构建器上唯一定义为“最终”的属性,以确保在构造函数上设置其值。

生成器模式的使用具有我一开始提到的前两种方法的所有优点,并且没有任何缺点。 客户端代码更易于编写,更重要的是易于阅读。 我听到的关于该模式的唯一批评是,您必须在构建器上复制类的属性。 但是,考虑到构建器类通常是它构建的类的静态成员类,因此它们可以很容易地一起进化。

现在,尝试创建新的User对象的客户端代码如何? 让我们来看看:

public User getUser() {return newUser.UserBuilder('Jhon', 'Doe').age(30).phone('1234567').address('Fake address 1234').build();}

很整洁,不是吗? 您可以用1行代码构建一个User对象,最重要的是,它很容易阅读。 而且,您要确保每当获得此类的对象时,都不会处于不完整状态。

这种模式非常灵活。 通过在对“ build”方法的调用之间更改构建器属性,可以使用单个构建器来创建多个对象。 构建器甚至可以自动完成每次调用之间生成的某些字段,例如ID或序列号。

重要的一点是,就像构造器一样,构造器可以对其参数施加不变性。 构建方法可以检查这些不变量,如果它们无效则抛出IllegalStateException
将参数从构建器复制到对象之后,必须检查它们,并在对象字段而不是构建器字段上检查它们,这一点至关重要。 这样做的原因是,由于生成器不是线程安全的,因此,如果我们在实际创建对象之前检查参数,则可以在检查参数与复制参数之间的另一个线程更改它们的值。 该时间段称为“漏洞窗口”。 在我们的用户示例中,这可能类似于以下内容:

public User build() {User user = new user(this);if (user.getAge() 120) {throw new IllegalStateException(“Age out of range”); // thread-safe}return user;
}

以前的版本是线程安全的,因为我们首先创建用户,然后检查不可变对象上的不变量。 以下代码在功能上看起来相同,但是它不是线程安全的,因此应避免执行以下操作:

public User build() {if (age 120) {throw new IllegalStateException(“Age out of range”); // bad, not thread-safe}// This is the window of opportunity for a second thread to modify the value of agereturn new User(this);
}

这种模式的最后一个优点是可以将构建器传递给某个方法,以使该方法能够为客户端创建一个或多个对象,而该方法无需了解有关如何创建对象的任何种类的细节。 为此,通常需要一个简单的界面,例如:

public interface Builder {T build();
}

在前面的User示例中, UserBuilder类可以实现Builder <User> 。 然后,我们可能会有类似的内容:

UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}

好吧,那是一篇相当长的第一篇文章。 综上所述,对于具有多个参数的类(不是一门精确的科学,但我通常将4个属性用作使用该模式的一个很好的指标),Builder模式是一个绝佳的选择,尤其是当这些参数中的大多数是可选的。 您将获得易于阅读,编写和维护的客户端代码。 此外,您的类可以保持不变,这使您的代码更安全。

更新 :如果将Eclipse用作IDE,事实证明您有很多插件可以避免该模式随附的大多数样板代码。 我见过的三个是:

  • http://code.google.com/p/bpep/
  • http://code.google.com/a/eclipselabs.org/p/bob-the-builder/
  • http://code.google.com/p/fluent-builders-generator-eclipse-plugin/

我没有亲自尝试过其中任何一个,因此我无法真正做出明智的决定。 我认为其他IDE应该也存在类似的插件。

参考:开发 人员实践中来自JCG合作伙伴 Jose Luis 的构建者模式, 它应该是博客。

翻译自: https://www.javacodegeeks.com/2013/01/the-builder-pattern-in-practice.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/370100.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python计算汽车的平均油耗_用python对汽车油耗进行数据分析

原标题&#xff1a;用python对汽车油耗进行数据分析- 从http://fueleconomy.gov/geg/epadata/vehicles.csv.zip下载汽车油耗数据集并解压- 进入jupyter notebook(ipython notebook)并新建一个New Notebook- 输入命令[python]view plaincopyimportpandas as pdimportnumpy as np…

git常用命令2

##一、git常用命令 ###1、 push文件 * 打开cmd窗口 * 输入f:&#xff0c;进入f:&#xff08;自己随便在自己的电脑上找个位置就行了&#xff0c;这里的f:&#xff0c;表示的是f盘&#xff09; * 然后输入mkdir workSpace&#xff0c;会自动在f盘下生成一个workSpace文件夹 * 然…

android移动应用基础教程源代码,Android移动应用基础教程 【程序活动单元Activity】...

本章目录一、Activity的生命周期1、生命周期状态2 、生命周期方法3、横竖屏切换时的生命周期二、Activity的创建配置和关闭1、Activity的创建2、配置Activity3、开启和关闭Activity三、Intent与IntentFilter1、Intent介绍1.1 意图的概念1.2 显式意图1.3 隐式意图2、IntentFilte…

elasticsearch中cluster和transport知识

elasticsearch cluster 概述 elasticsearch节点间通信的基础transport转载于:https://www.cnblogs.com/wzj4858/p/8126033.html

Python中使用subplot在一张画布上显示多张图

subplot(arg1, arg2, arg3) arg1: 在垂直方向同时画几张图arg2: 在水平方向同时画几张图arg3: 当前命令修改的是第几张图 t np.arange(0,5,0.1) y1 np.sin(2*np.pi*t) y2 np.sin(2*np.pi*t) plt.subplot(211) plt.plot(t,y1,b-.) plt.subplot(212) plt.plot(t,y2,r--) plt.s…

Java 8:从PermGen到元空间

您可能已经知道&#xff0c;现在可以下载JDK 8 Early Access 。 这使Java开发人员可以尝试Java 8的一些新语言和运行时功能。这些功能之一是完全删除自Oracle自JDK 7发行以来就宣布的Permanent Generation&#xff08;PermGen&#xff09;空间。例如&#xff0c;自JDK 7起&…

oracle symonym_ORACLE SYNONYM详解

以下内容整理自Oracle 官方文档一 概念A synonym is an alias for any table, view,materialized view, sequence, procedure, function, package, type, Java classschema object, user-defined object type, or another synonym. Because a synonymis simply an alias, it re…

浏览器缓存问题原理以及解决方案

浏览器缓存问题&#xff1a; 简单来说&#xff0c;浏览器缓存就是把一个已经请求过的Web资源&#xff08;如html页面&#xff0c;图片&#xff0c;js&#xff0c;数据等&#xff09;拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时…

Scikit-Learn机器学习入门

现在最常用的数据分析的编程语言为R和Python。每种语言都有自己的特点&#xff0c;Python因为Scikit-Learn库赢得了优势。Scikit-Learn有完整的文档&#xff0c;并实现很多机器学习算法&#xff0c;而每种算法使用的接口几乎相同&#xff0c;可以非常快的测试其它学习算法。 Pa…

hdu1542 Atlantis(扫描线+线段树+离散)矩形相交面积

题目链接&#xff1a;点击打开链接 题目描写叙述&#xff1a;给定一些矩形&#xff0c;求这些矩形的总面积。假设有重叠。仅仅算一次 解题思路&#xff1a;扫描线线段树离散&#xff08;代码从上往下扫描&#xff09; 代码&#xff1a; #include<cstdio> #include <al…

浏览器滚动条 --- 自定义“衣裳”

由于种种原因&#xff0c;浏览器的默认滚动条“衣裳”实在是 (ˉ▽&#xffe3;&#xff5e;)~~&#xff0c;为了“美”&#xff0c;本人结合万维网各大神给的经验和自己的实践&#xff0c;做了此篇总结。若有错误&#xff0c;请在评论里给出&#xff0c;我会及时更改。 我在电…

电脑调分辨率黑屏了怎么办_调显示器分辨率黑屏怎么办

调显示器分辨率黑屏怎么办调显示器分辨率黑屏解决方法&#xff1a;1&#xff0c;开机&#xff0c;当快要进入系统选项时&#xff0c;立即按f8键进入“高级模式”&#xff0c;因为系统选项界面显示的时间非常短&#xff0c;可以提早按f8键&#xff0c;否则错过时机就得重来。2&a…

什么是JNDI,SPI,CCI,LDAP和JCA?

JNDI代表Java命名和目录接口 。 它是用于提供对目录服务&#xff08;即带有对象的服务映射名称&#xff08;字符串&#xff09;&#xff0c;对远程对象或简单数据的引用&#xff09;的访问的API。 这就是所谓的 约束力 。 绑定集称为上下文 。 应用程序使用JNDI接口访问资源。…

android studio gradle 学习,学习Android Studio里的Gradle

一直听说Gradle很强大&#xff0c;只是偶尔用Android Studio创建Demo的时候看到他一次&#xff0c;今天抽个时间完整记录一下。1.gradle位置Android Studio项目创建好之后&#xff0c;默认有3个gradle文件&#xff0c;分别位于&#xff1a;/settings.gradle/build.gradle/app/b…

接口耗时打印并统计

1.可以利用Tomcat的access-log日志&#xff0c;让其打印出http请求的每次耗时。可以在 config/server.xml里Host标签下配置tomcat访问日志格式 <Valve className"org.apache.catalina.valves.AccessLogValve" directory"logs" prefix&quo…

js内存

js在定义变量时完成了内存的分配 js具有自动垃圾回收机制&#xff0c;垃圾回收器会每隔固定的一段时间就执行一次释放操作&#xff0c;即找出那些不再继续使用的值&#xff0c;释放其占用的内存 js中最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的&#xff0c;因…

halcon 图像差分_Halcon编程-基于纹理的mara检测

表面瑕疵检测是机器视觉领域非常重要的一个应用。机器视觉是集光学、机电和计算机三个领域的一门不算新的技术。但目前表面瑕疵检测在学界主要是计算机专业或者控制专业瞄准图像处理方向在做&#xff0c;而视觉光学系统这一块主要是光学工程专业在做。很少有研究者把这三块都结…

Apache Camel入门

在先前的博文中&#xff0c;我们了解了企业集成模式&#xff08;EIP&#xff09;。 现在&#xff0c;在这篇文章中&#xff0c;我们将研究实现这些模式的Apache Camel框架。 关于骆驼&#xff1a; Apache Camel是一个开放源代码项目&#xff0c;已有将近5年的历史&#xff0c;…

css 写打印样式问题

&#xff08;1&#xff09;背景颜色打印不出来问题解决方法 background样式要加上 !important&#xff1b;color样式要加上 !important&#xff1b;-webkit-print-color-adjust: exact;然后记得浏览器打印设置里面要在“打印背景图形”前面打勾。 -webkit-print-color-adjust:…

android studio smssdk,SMSSDK for Android 配置

1.集成之前先要申请Mob的appkey与appsecret2.在Mob官网下载最新SDK&#xff0c;解压后会看到以下目录结构&#xff1a;SMSSDK下存放的是短信SDK的全部内容。3.在android studio中加入SMS的第三方库AS版本的SMSSDK目录下包含以下内容&#xff1a;MobCommons.jar&#xff1a;Mob …