设计模式学习笔记 - 设计模式与范式 - 创建型:6.建造者模式:详解构造函数、set方法、建造者三种对象创建方式

news/2024/4/29 3:22:13/文章来源:https://blog.csdn.net/chenjian723122704/article/details/136975761

概述

本章学习一个比较常用的创建型设计模式,Builder 模式,中文翻译为建造者模式构建者模式,也有人叫它生成器模式

建造者模式的原理和代码实现非常简单,掌握起来并不难,难点在于应用场景。比如,你有没有考虑过这样几个问题:

  • 直接使用构造函数或者配合 set 方法就能创建对象,为什么还需要建造者模式来创建呢?
  • 建造者模式和工厂模式都可以创建对象,它们的区别在哪里呢?

为什么还需要建造者模式来创建呢?

在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。那什么情况下这种方式就不适用了,就需要采用建造者模式来创建对象呢?

假如有这样一道设计面试题:我们需要资源池配置类 ResourcePoolConfig。在这个资源池配置类中,有一下几个成员变量,也就是可配置项。现在,请求编写代码实现这个 ResourcePoolConfig 类。

成员变量解释是否必填默认值
name资源名称没有
maxTotal最大总资源数量8
maxIdle最大空闲资源数量8
minIdle最小空闲资源数量0

实现这样一个类并不是件难事。最常见、最容易想到的实现思路的代码如下所示。因为 maxTotalmaxIdleminIdle 不是必填变量,所以在创建 ResourcePoolConfig 对象的时候,我们通过往构造函数中,给这几个参数传递 null 值,来表示使用默认值。

public class ResourcePoolConfig {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_DILE = 8;private static final int DEFAULT_MIN_DILE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_DILE;private int minIdle = DEFAULT_MIN_DILE;public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {if (StringUtils.isEmpty(name)) {throw new IllegalArgumentException("name should not be empty");}this.name = name;if (maxTotal != null) {if (maxTotal < 0) {throw new IllegalArgumentException("maxTotal should not be positive");}this.maxTotal = maxTotal;}if (maxIdle != null) {if (maxIdle < 0) {throw new IllegalArgumentException("maxIdle should not be positive");}this.maxIdle = maxIdle;}if (minIdle != null) {if (minIdle < 0) {throw new IllegalArgumentException("minIdle should not be positive");}this.minIdle = minIdle;}}// 省略getter方法...
}

现在 ResourcePoolConfig 只有 4 个可配置项,对应到构造函数中,也只有 4 个参数,参数的个数不多。但是,如果可配置项主键增多,变成 8 个、10 个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,容易搞错各种参数的顺序,传递进错误的参数值,导致非常隐藏的 bug。

// 参数太多,导致可读性差、参数可能传递错误
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool",16,null,8,null, false , true, 10, 20falsetrue);

解决这个问题的办法你应该已经想到了,那就是用 set() 函数来给成员变量赋值,以替代冗长的构造函数。代码具体如下所示。其中,配置项 name 是必填的,所以我们把它放到构造函数中设置,强制创建类对象的时候就要填写。其他配置项 maxTotalmaxIdleminIdle 都不是必填的,所以我们通过 set() 函数来设置,让使用者自主选择填写或不填写。

public class ResourcePoolConfig {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_DILE = 8;private static final int DEFAULT_MIN_DILE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_DILE;private int minIdle = DEFAULT_MIN_DILE;public ResourcePoolConfig(String name) {if (StringUtils.isEmpty(name)) {throw new IllegalArgumentException("name should not be empty");}this.name = name;}public void setMaxTotal(Integer maxTotal) {if (maxTotal != null) {if (maxTotal < 0) {throw new IllegalArgumentException("maxTotal should not be positive");}this.maxTotal = maxTotal;}}public void setMaxIdle(Integer maxIdle) {if (maxIdle != null) {if (maxIdle < 0) {throw new IllegalArgumentException("maxIdle should not be positive");}this.maxIdle = maxIdle;}}public void setMinIdle(Integer minIdle) {if (minIdle != null) {if (minIdle < 0) {throw new IllegalArgumentException("minIdle should not be positive");}this.minIdle = minIdle;}}// 省略getter方法...
}

接下来,来看新的 ResourcePoolConfig 类该如何使用。我写了一个实例代码,如下所示。没有冗长的函数调用和参数列表,代码在可读性和易用性上提高看很多。

ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);

到此,我们仍然没有用到建造者模式,通过构造函数设置必填项,通过 set() 函数设置可选配置项,就能实现我们的设计需求。如果把问题难度在放大一点,比如,需要解决下面这三种问题,那现在的设计思路就不能满足了。

  • 刚刚讲到 name 是必填的,所以,把它放到构造函数中,强制创建对象的时候设置。如果必填的配置项很多,把这些必填配置项放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果把必填项也通过 set() 函数设置,那校验这些必填项是否已经填写的逻辑就无处安放了。
  • 此外,假设配置项之间有一定的依赖关系,比如,用户设置了 maxTotalmaxIdleminIdle 其中的一个,就必须显式的设置另外两个;或者配置项之间有一定的约束条件,比如 maxIdleminIdle 要小于等于 maxTotal。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的交易逻辑就无处安放了。
  • 如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,就不能在 ResourcePoolConfig 中暴露 set() 方法。

为了解决这些问题,建造者模式就派上用场了。

我们可以把校验逻辑放到 Builder 类中,先创建建造者,并通过 set() 方法设置建造者的变量值,然后再使用 build() 方法在真正创建对象之前,做集中的校验,校验通过之后才会创建对象。此外,我们吧 ResourcePoolConfig 的构造函数又改为 private 私有权限。这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象了。

使用建造者模式实现上面的需求,代码如下所示:

public class ResourcePoolConfig {private String name;private int maxTotal;private int maxIdle;private int minIdle;public ResourcePoolConfig(Builder builder) {this.name = builder.name;this.maxTotal = builder.maxTotal;this.maxIdle = builder.maxIdle;this.minIdle = builder.minIdle;}// 省略getter方法...// 将Builder类设计成了ResourcePoolConfig的内部类// 也可以将Builder设计成独立的非内部类ResourcePoolConfigBuilderpublic static class Builder {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_DILE = 8;private static final int DEFAULT_MIN_DILE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_DILE;private int minIdle = DEFAULT_MIN_DILE;public ResourcePoolConfig build() {// 将校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等if (StringUtils.isEmpty(name)) {throw new IllegalArgumentException("...");}if (maxIdle > maxTotal) {throw new IllegalArgumentException("...");}if (minIdle > maxTotal || minIdle > maxIdle) {throw new IllegalArgumentException("...");}return new ResourcePoolConfig(this);}public Builder setName(String name) {if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("...");}this.name = name;return this;}public Builder setMaxTotal(int maxTotal) {if (maxTotal < 0) {throw new IllegalArgumentException("...");}this.maxTotal = maxTotal;return this;}public Builder setMaxIdle(int maxIdle) {if (maxIdle < 0) {throw new IllegalArgumentException("...");}this.maxIdle = maxIdle;return this;}public Builder setMinIdle(int minIdle) {if (minIdle < 0) {throw new IllegalArgumentException("...");}this.minIdle = minIdle;return this;}}
}// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new Builder().setName("dbconnectionpool").setMaxTotal(16).setMaxTotal(8).setMinIdle(10).build();

实际上,使用建造者模式创建对象,还能避免对象存在无效状态。再来举个例子解释下。比如我们定义了一个长方形类,如果不适用建造者模式,采用先创建后 set 的方式,那就回导致在第一个 set 之后,对象处于无效状态。具体代码如下所示:

Rectangle r = new Rectangle(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeignt(3); // r is valid

为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,就需要考虑使用建造者模式,先设置建造者变量,然后再一次性地创建对象,让对象一直处于有效状态。

实际上,如果我们并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那我们直接暴露 set() 方法来设置类的成员变量值是完全没问题的。而且,使用建造者模式来构建对象,代码实际上是有点重复的, ResourcePoolConfig 类中的成员变量,要在 Builder 类中重新再定义一遍。

建造者模式和工厂模式的区别在哪里?

建造者模式是让建造者类来负责对象的创建工作。工厂模式中的工厂类也是负责对象的创建工作。那它们之间有什么区别呢?

实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化” 地创建不同的对象。

网上有一个很经典的例子,很好地解释了两者的区别。
顾客走进一家餐馆点餐,利用工厂模式根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,可以通过建造者模式根据用户选择的不同配料来制作披萨。

实际上,我们没必要非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个设计模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题

回顾

建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度设计。

如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,可以通过构造函数配合 set() 方法来解决。但是,如果存在下面任何一种情况,就要考虑使用建造者模式了。

  • 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表过长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
  • 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或者约束条件的校验逻辑就无处安放了。
  • 如果希望创建不可变对象,即对象创建好之后,就不能再修改其内部的属性值,要实现这个功能,我们就不能暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。

另外,本章还对比了工厂模式和建造者模式的区别。

  • 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定参数决定创建哪种类型的对象。
  • 建造者模式是用来创建一种类型的对象,可以通过设置不同的可选参数,“定制化” 地创建不同的对象。

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

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

相关文章

零基础入门数据挖掘系列之「特征工程」

摘要&#xff1a;对于数据挖掘项目&#xff0c;本文将学习应该从哪些角度做特征工程&#xff1f;从哪些角度做数据清洗&#xff0c;如何对特征进行增删&#xff0c;如何使用PCA降维技术等。 特征工程&#xff08;Feature Engineering&#xff09;对特征进行进一步分析&#xf…

AI+软件工程:10倍提效!用ChatGPT编写系统功能文档

系统功能文档是一种描述软件系统功能和操作方式的文档。它让开发团队、测试人员、项目管理者、客户和最终用户对系统行为有清晰、全面的了解。 通过ChatGPT&#xff0c;我们能让编写系统功能文档的效率提升10倍以上。 ​《Leetcode算法刷题宝典》一位阿里P8大佬总结的刷题笔记…

深入理解PHP+Redis实现分布式锁的相关问题

概念 PHP使用分布式锁&#xff0c;受语言本身的限制&#xff0c;有一些局限性。 通俗理解单机锁问题&#xff1a;自家的锁锁自家的门&#xff0c;只能保证自家的事&#xff0c;管不了别人家不锁门引发的问题&#xff0c;于是有了分布式锁。分布式锁概念&#xff1a;是针对多个…

通过Caliper进行压力测试程序,且汇总压力测试问题解决

环境要求 第一步. 配置基本环境 部署Caliper的计算机需要有外网权限;操作系统版本需要满足以下要求:Ubuntu >= 16.04、CentOS >= 7或MacOS >= 10.14;部署Caliper的计算机需要安装有以下软件:python 2.7、make、g++(gcc-c++)、gcc及git。第二步. 安装NodeJS # …

RegSeg 学习笔记(待完善)

论文阅读 解决的问题 引用别的论文的内容 可以用 controlf 寻找想要的内容 PPM 空间金字塔池化改进 SPP / SPPF / SimSPPF / ASPP / RFB / SPPCSPC / SPPFCSPC / SPPELAN &#xfffc; ASPP STDC&#xff1a;short-term dense concatenate module 和 DDRNet SE-ResNeXt …

初识React(一)从井字棋游戏开始

写在前面&#xff1a; 磨磨唧唧了好久终于下定决心开始学react&#xff0c;刚刚接触感觉有点无从下脚...新的语法新的格式跟vue就像两种物种...倒是很好奇路由和store是怎么实现的了~v~&#xff0c;一点一点来吧&#xff01;&#xff01;&#xff01; (一)创建项目 使用vite…

Reactor设计模式和Reactor模型

Reactor设计模式 翻译过来就是反应堆&#xff0c;所以Reactor设计模式本质是基于事件驱动。 角色 Handle&#xff08;事件&#xff09;EventHandler&#xff08;事件处理器&#xff09;ConcreteEventHandler&#xff08;具体事件处理器&#xff09;Synchronous Event Demult…

QT实现蒙层效果

一.蒙层的作用 1.为了其他窗口不被误操作&#xff0c;禁止对其他窗口操作 二.应用场景 1.一些触摸屏设备上弹出一个dialog窗口&#xff0c;在操作这个窗口的时候不希望后面的窗口被误操作 2.之前做一个医疗设备就曾有过这种需求&#xff0c;因为医疗设备对安全性要求非常高&…

利用 Scapy 库编写 ARP 缓存中毒攻击脚本

一、ARP 协议基础 参考下篇文章学习 二、ARP 缓存中毒原理 ARP&#xff08;Address Resolution Protocol&#xff09;缓存中毒是一种网络攻击&#xff0c;它利用了ARP协议中的漏洞&#xff0c;通过欺骗或篡改网络中的ARP缓存来实施攻击。ARP协议是用于将IP地址映射到物理MAC…

各大pdf转word软件都用的哪家的ocr引擎?

国内一般的PDF软件一般都调用某国际PDF原厂的OCR接口&#xff0c;但这家公司是主要做PDF&#xff0c;在OCR方面并不专注&#xff0c;一些不是很复杂的场景还能应付得过来&#xff0c;复杂一点的效果就强差人意了&#xff0c;推荐用金鸣表格文字识别系统&#xff0c;它主要有以下…

基于树莓派实现 --- 智能家居

最效果展示 演示视频链接&#xff1a;基于树莓派实现的智能家居_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Tr421n7BM/?spm_id_from333.999.0.0 &#xff08;PS&#xff1a;房屋模型的搭建是靠纸板箱和淘宝买的家居模型&#xff0c;户型参考了留学时短租的公寓~&a…

Linux repo基本用法: 搭建自己的repo仓库[服务端]

概述 Repo的使用离不开Git, Git 和 Repo 都是版本控制工具&#xff0c;但它们在使用场景和功能上有明显区别… Git 定义&#xff1a;Git 是一个分布式的版本控制系统&#xff0c;由 Linus Torvalds 为 Linux 内核开发而设计&#xff0c;现已成为世界上最流行的版本控制软件之…

【详细讲解PostCSS如何安装和使用】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d; 希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

Leetcode146. LRU 缓存

Every day a Leetcode 题目来源&#xff1a;146. LRU 缓存 解法1&#xff1a;哈希表 链表 代码&#xff1a; /** lc appleetcode.cn id146 langcpp** [146] LRU 缓存*/// lc codestart class LRUCache { private:unordered_map<int, list<pair<int, int>>:…

图解Kafka架构学习笔记(二)

kafka的存储机制 https://segmentfault.com/a/1190000021824942 https://www.lin2j.tech/md/middleware/kafka/Kafka%E7%B3%BB%E5%88%97%E4%B8%83%E5%AD%98%E5%82%A8%E6%9C%BA%E5%88%B6.html https://tech.meituan.com/2015/01/13/kafka-fs-design-theory.html https://feiz…

华为防火墙配置指引超详细(包含安全配置部分)以USG6320为例

华为防火墙USG6320 华为防火墙USG6320是一款高性能、高可靠的下一代防火墙,适用于中小型企业、分支机构等场景。该防火墙支持多种安全功能,可以有效抵御网络攻击,保护网络安全。 目录 华为防火墙USG6320 1. 初始配置 2. 安全策略配置 3. 防火墙功能配置 4. 高可用性配…

四种常用限流算法、固定窗口限流算法、滑动窗口限流算法、漏桶限流算法和令牌桶限流算法

什么是限流&#xff1f; 限流可以被视为服务降级的一种形式&#xff0c;其核心目标是通过控制输入和输出流量来保护系统。通常&#xff0c;一个系统的处理能力是可以预估的&#xff0c;为了确保系统的稳定运行&#xff0c;当流量达到预定的阈值时&#xff0c;必须采取措施限制进…

在宝塔面板中,为自己的云服务器安装SSL证书,为所搭建的网站启用https(主要部分攻略)

前提条件 My HTTP website is running Nginx on Debian 10&#xff08;或者11&#xff09; 时间&#xff1a;2024-3-28 16:25:52 你的网站部署在Debain 10&#xff08;或者11&#xff09;的 Nginx上 安装单域名证书&#xff08;默认&#xff09;&#xff08;非泛域名&#xf…

数据结构与算法(二)优先队列

数据结构与算法&#xff08;二&#xff09; 优先队列 一、优先队列的基本概念 我们的电脑总是运行着多个程序&#xff0c;电脑会给每个程序分配一个优先级&#xff0c;并首先执行下一个优先级更高的程序。在此情况下&#xff0c;可将其抽象为一个数据结构&#xff0c;该数据结构…

鸿蒙HarmonyOS开发-FA模型访问Stage模型DataShareExtensionAbility

无论FA模型还是Stage模型&#xff0c;数据读写功能都包含客户端和服务端两部分。 FA模型中&#xff0c;客户端是由DataAbilityHelper提供对外接口&#xff0c;服务端是由DataAbility提供数据库的读写服务。 Stage模型中&#xff0c;客户端是由DataShareHelper提供对外接口&…