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

概述

本章学习一个比较常用的创建型设计模式,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.mzph.cn/news/768204.shtml

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

相关文章

Google ScreenAI代表了一款先进的视觉语言模型,专为用户界面(UI)和视觉情境下的语言理解而设计

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

华为od真题2023-C卷-三叉搜索树

题目描述&#xff1a; 定义构造三叉搜索树规则如下: 每个节点都存有一个数&#xff0c;当插入一个新的数时&#xff0c;从根节点向下寻找&#xff0c;直到找到一个合适的空节点插入。查找的规则是: 1.如果数小于节点的数减去500&#xff0c;则将数插入节点的左子树2.如果数大于…

政安晨:【深度学习部署】—— TensorFlow Extended(TFX)介绍

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras实战演绎机器学习 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 前言 TensorFlow Extended&#xff08;TFX&a…

深入了解Redis的过期策略和内存淘汰机制

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; ✨✨ 帅哥美女们&#xff0c;我们共同加油&#xff01;一起进步&am…

语言模型的原理、实战与评估

语言模型的原理、实战与评估是一个宽泛的话题,下面是对这三个方面简要概述: 语言模型的原理 语言模型(Language Model, LM)是一种统计模型,用于估计一段文本序列的概率分布。它的核心任务是给定一系列词语,计算出这些词语组合成一个完整句子或段落的概率。典型的语言模型…

mysql基础2多表查询

多表查询 多表关系: 一对多 案例: 部门 与 员工的关系 关系: 一个部门对应多个员工&#xff0c;一个员工对应一个部门 实现: 在多的一方建立外键&#xff0c;指向一的一方的主键 多对多 案例: 学生 与 课程的关系 关系: 一个学生可以选修多门课程&#xff0c;一门课程也可以…

MySQL基础复习

目录 一、简单的命令 二、SQL语句分类 三、简单查询 四、条件查询 五、排序 一、简单的命令 net start 服务名称 net stop 服务名称 mysql -uroot -p123456 显示密码形式 mysql -uroot -p 隐藏密码形式 exit 退出 show databases; 查看MySQL中的数据库有哪些 use test…

RuleApp资源社区,知识付费社区,可对接typecho的小程序APP

强大的文章/社区/自媒体客户端&#xff0c;支持打包为安卓&#xff0c;苹果&#xff0c;小程序。包括文章模块&#xff0c;用户模块&#xff0c;支付模块&#xff0c;聊天模块&#xff0c;商城模块等基础功能&#xff0c;包含VIP会员&#xff0c;付费阅读等收费体系&#xff0c…

AttributeError: ‘_MSDataLoaderIter‘ object has no attribute ‘_put_indices‘

问题描述 复现代码过程中遇到错误&#xff1a;AttributeError: _MSDataLoaderIter object has no attribute _put_indices 解决方案 出错的原因是代码中使用了不存在的属性"_put_indices"。这个错误可能与你使用的版本不兼容有关。在pytorch1.x版本中&#xff0c;&q…

c语言函数大全(I开头)

c语言函数大全(I开头) There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quickly. 函数名…

【生产力】Mac 窗口布局工具 Magnet

Magnet 是一款为Mac操作系统设计的实用工具&#xff0c;旨在帮助用户更加方便地管理和组织他们的窗口布局。通过使用Magnet&#xff0c;用户可以轻松地将应用程序窗口拖放到屏幕的各个部分&#xff0c;从而实现窗口的自动排列和大小调整。这款工具特别适合需要同时处理多个应用…

Django Ajax

【一】Json 【1】介绍 JSON&#xff08;javascript object otaition&#xff09;是一种轻量级的数据交换格式JSON使用了Javascript的一部分语法来定义其数据格式&#xff0c;但Json是独立于语言的Json采用完全独立于语言的文本格式&#xff0c;使得Json成为理想的数据交互语言…

OD_2024_C卷_100分_72、求最多可以派出多少支团队【JAVA】【双指针】

题目描述 用数组代表每个人的能力&#xff0c;一个比赛活动要求参赛团队的最低能力值为N&#xff0c;每个团队可以由1人或者2人组成&#xff0c;且1个人只能参加1个团队&#xff0c;计算出最多可以派出多少只符合要求的团队。 输入描述 第一行代表总人数&#xff0c;范围1-5…

react native 键盘事件

在做修改密码功能是发现他的键盘第一次调起之后然后收起键盘焦点不会消失而且键盘也不会再调起来了 我门线引入需要的组件 import { StyleSheet, View, TextInput, Keyboard, TouchableWithoutFeedback, } from react-native; import React, {useEffect, useState, useRef} fr…

计算机网络原理之四种攻击

目录 一、ARP攻击 二、DNS劫持攻击 三、DOS攻击 四、DDOS攻击 一、ARP攻击 概念&#xff1a; ARP协议的基本功能就是通过目标设备的IP地址&#xff0c;在局域网发送广播包&#xff0c;查询目标设备的MAC地址以保证通信的进行。 原理&#xff1a; 基于ARP协议的这一工作特性&…

[Halcon学习笔记]在Qt上实现Halcon窗口的字体设置颜色设置等功能

1、 Halcon字体大小设置在Qt上的实现 在之前介绍过Halcon窗口显示文字字体的尺寸和样式&#xff0c;具体详细介绍可回看 &#xff08;一&#xff09;Halcon窗口界面上显示文字的字体尺寸、样式修改 当时介绍的设定方法 //Win下QString Font_win "-Arial-10-*-1-*-*-1-&q…

MySQL学习笔记------SQL(2)

ziduanSQL DML 全称为&#xff1a;Data Manipulation Language&#xff0c;用来对数据库中表的数据记录进行增删改操作 插入数据 添加数据&#xff08;INSERT&#xff09; 给指定字段添加数据&#xff1a;INSERT INTO 表名(字段名1&#xff0c;字段名2&#xff0c;......…

【PyQt】19-数据操作

数据表 前言一、显示二维表数据&#xff08;QTableView控件&#xff09;扩展知识---MVC模式1.1 代码1.2 运行结果 二、显示列数据&#xff08;QListView控件&#xff09;2.1 代码2.2 运行结果2.3 扩展---列表控件&#xff08;QListWidget&#xff09;运行结果 总结 前言 一、显…

STM32使用滴答定时器实现delayms

在STM32上使用SysTick实现jiffies&#xff08;时间戳&#xff09;并且实现delay_ms 代码实现&#xff1a; volatile uint32_t jiffies 0; // 用于记录系统运行的jiffies数 void SysTick_Handler(void) {/* 每次SysTick中断&#xff0c;jiffies增加 */jiffies; }uint32_t tick…

网络安全新前沿:利用大模型进行复杂威胁检测与响应

1. 背景介绍# 网络安全新前沿&#xff1a;利用大模型进行复杂威胁检测与响应 1. 背景介绍 随着互联网的普及和信息技术的飞速发展&#xff0c;网络安全问题日益突出。传统的网络安全防御手段&#xff0c;如防火墙、入侵检测系统&#xff08;IDS&#xff09;和防病毒软件&…