设计模式-建造者模式

在前面几篇文章中,已经讲解了单例模式、工厂方法模式、抽象工厂模式,创建型还剩下一个比较重要的模式-建造者模式。在理解该模式之前,我还是希望重申设计模式的初衷,即为解决一些问题而提供的优良方案。学习设计模式遗忘其初衷,注定无法理解其真正的深刻内涵。从创建型模式的名称上来看,这些都是为了解决创建对象相关的问题。单例模式解决了如何创建唯一对象的问题,工厂方法模式解决了对象创建过程的封装问题,抽象工厂模式解决了创建多个相关联对象的问题,那么不知道你之前是否有思考过,建造者模式是要解决什么问题吗?我相信很多人可能没有思考而直接用老一套去学习该模式,最终就是不理解、记不住、用不会!

一、建造者模式概念理解

建造者模式,又称生成器模式,在大部分参考资料中都公认的定义为:

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

我不知道你们能不能理解这个很抽象的定义,起初我是不太能理解。我似乎从“分离”上能看出是要解耦,至于“构建”、“表示”这两个词本身就有模糊的感觉。实际上,这里的“构建”可以理解为“里子”,“表示”可以理解为“面子”,“同样的构建过程可以创建不同的表示”就是里子和多个面子需要解耦,如同一个人可以拥有很多面具以示人。意即,你还是你,但是外人看到的可以有多个。
“构建”指的就是里子。每一个可由外界创建对象的类都会提供一个或多个构造函数,或为有参构造,亦或为无参构造。对象的正规创建最常用的方式通过new关键字及构造方法Constructor。然而实际上,对象的构建过程并不止步于此。对象的本质是类信息(包括属性、方法)+数据,前者是编译后就固定不变的,后者数据是运行时改变的,因此对象的构建过程应从分配内存开始直到对象数据初始化结束。对象的初始化除了调用Constructor方法之外,还有Setter方法也会经常用于初始化对象数据(注,这里Setter方法不仅仅指的是get\set方法)。因此,对象的构建过程本质上是类构造函数-Constructor+Setter方法。我们通常会使用这二者协同初始化对象数据并获得对象,但是这里面会不会存在问题呢?
可实例化类会提供多个构造函数提供给外界用于创建对象。构造函数可能的复杂性包括两个部分,一个是入参,一个是具体逻辑。后者可以通过工厂方法模式进行封装,那前者怎么办?即,若对象的创建需要很多外界输入参数,其中包括必要参数或非必要参数,这种情况下我们在一些源码或业务代码中会看到类中会提供很多个构造方法,不同构造方法通过重载的方式处理参数的差异性,使用方根据需要选择不同的构造方法。那,如果参数再多一些呢?你无法判断使用方想传入哪些参数或者不传入哪些参数来构建对象。还有一种情况即使类的构建过程需要通过对应的内部配置类进行构建。你只提供内部相关复杂对象作为入参的有参构造,我根本不了解这个入参如何办?举例,你提供了Configure内部类作为创建会话对象的有参构造,保证了迪米特原则,但是我这边数据可能是XML类型配置、YAML类型配置文件等,是否我需要解决这些文件解析为Configure内部类对象才能使用你的构造方法呢?这些都是问题,问题的根因就是你的类创建对象过程里子和面子耦合太严重(一个构造对象提供一个面子)。
对于构造方法参数数量问题,有的同学会说那我就把必选参数留在构造方法,可选参数使用Setter方法。似乎是能够解决一部分问题,但是Setter方法可能会使得对象的构建逻辑分散在各处,增加了使用不完整对象的风险。【因此,建造者模式是不建议调用端直接使用对象的Setter方法,必须封装起来】
如何解耦呢?能否将构建对象所需数据(面子)和构造函数中的逻辑(里子)拆分。我们讲过,解决耦合问题的究极好办法就是加一层,即Builder层。让Builder来充当这个面子,Builder负责提供给外界并预处理外界数据,转换为内部Constructor所需要的数据,然后由Builder来调用Constructor来返回对象。这样的解耦使得Constructor仅专注于内部构建逻辑,而外界所需要的面子均由Builder来负责,如此设计既满足业务需要,也满足单一职责原则。
概念总结:

  • 建造者模式主要解决创建对象时对象的创建过程与创建的所需数据表示的严重耦合性问题
  • Constructor构造方法在非必要参数多时无法满足调用方的需求,且在复杂构造参数(内部配置类)时增加调用方创建难度。
  • Setter方法也是对象构建过程的一部分,但是可能会误使用不成熟对象
  • 通过职责拆分的方法解耦对象构建过程及其表示。Builder负责承接调用方可能赋值的数据,并转换为类对象创建的内部参数需要。目标对象的类仅关注自身功能的实现。【即Builder出去跑业务,伺候甲方的。Construstor做好自己本职工作】

二、应用实践

上一章节限于个人水平有限,理解角度与主流理解不完全契合,也会存在让大家误解的地方。因此,这个章节就通过具体的示例来说说我的想法,比较不同方案的优劣达到理解建造者模式的目的。在其他参考资料中,给出的案例和建造者解决方案我认为虽容易看懂,但不易理解且难以投入实际使用,可能还存在一些问题。下面我会通过一个简单的案例,一步步分析为什么常规的建造者模式大家几乎都不会使用到。

2.1 基本案例

案例的背景就以大家熟悉的“电脑”对象创建为例,想象以下,创建一个电脑对象应该具备哪些东西(数据)呢?大概会有CPU(中央处理器)、内存、硬盘、显卡、显示器、键盘、鼠标、声卡、网卡、光驱等。所以,创建一个电脑对象可能会需要很多种数据,但是根据用户的需求不同,需要创建的电脑对象可能也存在差异性。如:
① 我仅需要电脑用于跑程序,那我只需要【CPU、内存、硬盘】去创建电脑对象
② 我要跑深度学习,那我除了以上组件(数据)之外,还需要好的【显卡】
③ 我要打游戏,那我除了以上组件(数据)之外,还需要好的【显示器、键盘、鼠标、声卡、网卡】等
④ 我要看DVD,那我就得需要【光驱】了。

你看看多头疼,根据调用方使用场景不同,电脑对象的所需组件也有不同。难道你要遍历所有场景给出不同的构造方法,很明显这种方案不太现实。但如果你仔细发现就可以看到,对象的创建是可以区分必要组件和非必要组件的。在这个例子中,不论用户的需求是啥,都需要有【CPU、内存、硬盘】才能创建电脑。那是否电脑类仅提供必要组件的构造方法,非必要组件由Setter方法来负责呢?这已经是个很好的方案,大部分的业务开发中可能都会使用这个方案。但设计模式不会止步于此,存在两个疑问:(1)Setter方案存在什么问题?(2)有没有更好的方案解决?
Setter方案存在两个问题,第一个问题前面也提到了,对象数据初始化逻辑分散在各处,增加了使用不完整对象的风险。第二个问题是使用方不清楚Setter方法具体含义,是仅用于对象初始化(类似于Construstor)还是用于对象数据运行时修改(类似于其余普通函数),即责任不明。
为解决这个问题,我们前面提出中间添加builder层,由Builder来负责封装对象构建的多种表示的差异性。示例代码如类图如下:
① “电脑”对象类

public class Computer {private Object cpu;private Object memory;private Object hardDisk;private Object graphicsCard;private Object monitor;private Object keyboard;private Object mouse;private Object soundCard;private Object networkCard;private Object opticalDrive;public Computer(Object cpu, Object memory, Object hardDisk) {this.cpu = cpu;this.memory = memory;this.hardDisk = hardDisk;}public void setGraphicsCard(Object graphicsCard) {this.graphicsCard = graphicsCard;}public void setMemory(Object memory) {this.memory = memory;}// 其他可选属性set方法省略...public void doSomething() {// ...}
}

② Builder接口

public interface ComputerBuilder {void setGraphicsCard(Object graphicsCard);void setMonitor(Object monitor);void setKeyboard(Object keyboard);void setMouse(Object mouse);void setSoundCard(Object soundCard);void setNetworkCard(Object networkCard);void setOpticalDrive(Object opticalDrive);Computer buildComputer(Object cpu, Object memory, Object hardDisk);Computer buildDLComputer(Object cpu, Object memory, Object hardDisk, Object graphicsCard);
}

③ Builder具体实现类

public class ConcreteComputerBuilder implements ComputerBuilder{private Object graphicsCard;private Object monitor;private Object keyboard;private Object mouse;private Object soundCard;private Object networkCard;private Object opticalDrive;@Overridepublic void setGraphicsCard(Object graphicsCard) {this.graphicsCard = graphicsCard;}@Overridepublic void setMonitor(Object monitor) {this.monitor = monitor;}@Overridepublic void setKeyboard(Object keyboard) {this.keyboard = keyboard;}@Overridepublic void setMouse(Object mouse) {this.mouse = mouse;}@Overridepublic void setSoundCard(Object soundCard) {this.soundCard = soundCard;}@Overridepublic void setNetworkCard(Object networkCard) {this.networkCard = networkCard;}@Overridepublic void setOpticalDrive(Object opticalDrive) {this.opticalDrive = opticalDrive;}@Overridepublic Computer buildComputer(Object cpu, Object memory, Object hardDisk) {// ...这里省略必要参数的校验逻辑Computer ins = new Computer(cpu, memory, hardDisk);if(this.graphicsCard != null) {ins.setGraphicsCard(this.graphicsCard);}// ... 省略其余可选参数的处理逻辑return ins;}@Overridepublic Computer buildDLComputer(Object cpu, Object memory, Object hardDisk, Object graphicsCard) {// ...这里省略必要参数的校验逻辑Computer ins = new Computer(cpu, memory, hardDisk);if(graphicsCard == null) {throw new RuntimeException("缺少显卡组件,无法创建深度学习机器");}ins.setGraphicsCard(this.graphicsCard);// ... 省略其余可选参数的处理逻辑return ins;}
}

在这里插入图片描述

完整的代码如上,类图中使用SetXXX省略了很多Set方法。这种就属于建造者模式,调用方可通过ComputerBuilder来实现获取Computer对象,而在ComputerBuilder中对于可选参数的处理通过封装在了buildXXX方法中,并且提供了多种类型的builder方法供调用方使用。因此这样就实现了对象的构建与它的表示(代码中给出了2种表示,还可以更多)解耦,表示虽不同但是实际上都是通过同一个Constructor来创建对象的。

在一些参考资料中,还给出了Director类,Director翻译为导演类,意即就是将多种表示(如普通电脑、深度学习电脑、打游戏电脑等)预先封装到Director中供调用方使用,调用方不再感知setXXX设置组件数据的过程了。
我认为这种封装会造成类的急速膨胀,而且效果不好。你根本无法预知调用方会有什么样的场景,Director类也无法彻底解决问题。如上例,在Builder通过不同的方法返回不同对象也有同样效果且没有问题。

目前Setter方法问题通过这种方法已经很好解决了,很多相关参考资料也到此结束了,但我认为建造者模式的理解尚不能止步,因为还有遗漏的地方。之前的思考思路是,对象初始化Constructor构造函数可能会存在很多可选参数,不可能全部提供对应的构造方法。然后,我们分析了使用Setter方法解决可选参数的问题,但是Setter方法的使用增加了使用不成熟对象的风险。之后,我们增加了builder层封装处理了Setter方法,然后创建对象。
可以看出,我们之前只是从构造函数数量问题上思考,但是这很难让我们理解并使用建造者模式。为什么这么说呢?因为我们在平时开发中几乎不会碰到这种情况。在大部分的开发场景下,当函数形参数量大于7时,我们就会单独封装为一个类,因此我们很少会碰见要处理参数数量上的问题,大部分情况下我们是要处理类型问题

2.2 理解“表示”

不同的表示就是指用于初始化对象的数据不确定(由调用方指定),包括数量不确定以及类型不确定两个方面。数量不确定可通过上一小节的方案解决,但需要注意的是当函数形参数量大于7时,我们就会单独封装为一个类,这也会转换为类型不确定。类型不确定是啥意思呢?在第一章节内我们提到大部分的复杂对象由于其所需数据多,一般创建对象都是通过其内部配置类创建的,你不能要求所有的调用方都必须了解这个内部配置类才能创建对象。
说起来有些许抽象,就以前一小节案例来说。创建一个深度学习机器肯定是需要显卡组件(参数),细想这里会存在一个我们经常忽略的问题,就是显卡对于“创建电脑”来说会很复杂。创建电脑的过程你必须考虑显卡的接口、频率、功率等信息,这就意味着显卡的类型不仅仅是个简单Object类,而是一个相对复杂的参数类(定义为类GraphicsCard)。那问题来了,调用端为了意图创建一个电脑还需要先创建一个GraphicsCard对象吗?那其他组件呢?太麻烦啦,调用方能否只传输一个String表示显卡的型号呢。继续分析,电脑对象会提供String类型显卡参数的构造方法吗?明显不会,否则电脑对象内部就得处理String到GraphicsCard对象的创建过程了,电脑类的职责不再单一,这就是构建过程和表示耦合导致的结果。因此将String到GraphicsCard对象的过程就可以由Builder承担了,这样既满足了调用方对于不同表示的需求,也保证了目标对象构建过程的职责清晰。
根据此,响应代码类图如下:(代码简单不再提供具体代码)
在这里插入图片描述
从类图中看出,Builder提供了接受String类型数据来负责处理显卡参数类型问题,Computer还是仅负责自己内部的逻辑即可,外面的一切由Builder这个跑业务的给摆平。这么一看,建造者模式理解起来就豁然开朗,调用方要创建对象但是无法提供创建对象的条件,那就上Builder来处理这其中的GAP即可。这就达到了外界可以使用多种方式(表示)通过Builder创建目标对象,
而实际Computer创建对象的动作可能还是同一套逻辑(Constructor)。

数量不确定 远没有 类型不确定 带来的问题严重。如果仅仅是数量问题,使用Setter方法方案即可,使用Builder封装Setter稍微有点大材小用了。但是类型问题,就必须得使用Builder来解决了。

大部分的 数量问题 也都可能转换为 类型问题。当参数数量很多时,都会考虑将这些参数封装起来,比如Configure类。目标对象的创建仅通过Confugure对象数据来创建,而Builder就负责将外部数据转换为Configure对象。
这是普遍的做法,说到这你是否觉得上文的两个类图有哪里让人不适的地方?对,属性太多啦,目标对象属性一套数据,Builder也跟着一套数据,代码重复且可读性会差。解决办法就是封装,要么使用Configure类,要么使用枚举或其他都行。

大部分情况下我们是要处理类型问题,下面我们简单看下Mybatis中用于创建SqlSessionFactory对象的建造者模式是怎么应用的,下面给出SqlSessionFactoryBuilder的代码截图:
在这里插入图片描述
SqlSessionFactory对象的创建需要内部配置类Configuration,而调用方可能提供多种不同的源数据及配置。这其中的转换、验证逻辑均有Builder类来负责处理。

建造者模式优点:

  • 将对象创建过程与表示解耦,满足单一职责原则
  • 由于相互独立,对象的创建方式十分容易扩展

建造者模式缺点:

  • 建造者模式几乎没有缺点。最好用于创建复杂对象,简单对象使用该模式会增加代码复杂度。

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

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

相关文章

VLAN原理(Virtual LAN 虚拟局域网)

VLAN(Virtual LAN 虚拟局域网) 1、广播/广播域 2、广播的危害:增加网络/终端负担,传播病毒, 3、如何控制广播?? ​ 控制广播隔离广播域 ​ 路由器物理隔离广播 ​ 路由器隔离广播缺点&…

解决在云服务器开放端口号以后telnet还是无法连接的问题

这里用阿里云服务器举例,在安全组开放了对应的TCP端口以后。使用windows的cmd下的telnet命令,还是无法正常连接。 telnet IP地址 端口号解决方法1: 在轻量服务器控制台的防火墙规则中添加放行端口。 阿里云-管理防火墙 如图,开放…

右击不显示TortoiseGit图标处理方法

第一种 右键--》TortoiseGIt--》setting--》Icon Overlays--》Status cache,按照下图设置,然后重启电脑。 第二种 进入注册信息,按照步骤找到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIden…

AI帮你制作海报

介绍 Microsoft Designer是由微软推出的图像处理软件,能够通过套用模板等方式快速完成设计加工,生成能够在社交媒体使用的图片。Designer的使用更为简单便捷,用户能够通过套用模板等方式快速完成设计加工,生成能够在社交媒体使用…

python离散仿真器

文章目录 类图示例 类图 示例

Stability AI推出Stable Diffusion XL 1.0,文本到图像模型

Stability AI宣布推出Stable Diffusion XL 1.0,这是一个文本到图像的模型,该公司将其描述为迄今为止“最先进的”版本。 Stability AI表示,SDXL 1.0能生成更加鲜明准确的色彩,在对比度、光线和阴影方面做了增强,可生成…

MySQL使用xtrabackup备份和恢复教程

1、xtrabackup说明 xtrabackup是percona开源的mysql物理备份工具。 xtrabackup 8.0支持mysql 8.0版本的备份和恢复。 xtrabackup 2.4支持mysql 5.7及以下版本的备份和恢复。 这里我以xtrabackup 8.0为例讲解备份和恢复的具体操作方法。 xtrabackup 2.4版本的使用上和8.0版本相…

【HDFS】Block、BlockInfo、BlockInfoContiguous、BlockInfoStriped的分析记录

本文主要介绍如下内容: 关于几个Block类之间的继承、实现关系;针对文章标题中的每个类,细化到每个成员去注释分析列出、并详细分析BlockInfo抽象类提供的抽象方法、非抽象方法的功能针对几个跟块组织结构的方法再进行分析。moveBlockToHead、listInsert、listRemove等。一、…

【计算机网络】应用层协议 -- HTTP协议

文章目录 1. 认识HTTP协议2. 认识URL3. HTTP协议格式3.1 HTTP请求协议格式3.2 HTTP响应协议格式 4. HTTP的方法5. HTTP的状态码6. HTTP的Header7. Cookie和Session 1. 认识HTTP协议 协议。网络协议的简称,网络协议是通信计算机双方必须共同遵守的一组约定&#xff0…

C# 全局响应Ctrl+Alt+鼠标右键

一、简述 某些应用,我们希望全局自定义热键。按键少了会和别的应用程序冲突,按键多了可定用户操作不变。因此我计划左手用CtrlAlt,右手用鼠标右键呼出我自定义的菜单。 我使用键盘和鼠标事件进行简单测试(Ctrl鼠标右键&#xff…

【Ajax】笔记-jsonp实现原理

JSONP JSONP是什么 JSONP(JSON With Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来的,只支持get请求。JSONP 怎么工作的? 在网页有一些标签天生具有跨域能力,比如:img link iframe script. …

【Python数据分析】Python常用内置函数(一)

🎉欢迎来到Python专栏~Python常用内置函数(一) ☆* o(≧▽≦)o *☆嗨~我是小夏与酒🍹 ✨博客主页:小夏与酒的博客 🎈该系列文章专栏:Python学习专栏 文章作者技术和水平有限,如果文…

Redis实战(3)——缓存模型与缓存更新策略

1 什么是缓存? 缓存就是数据交换的缓冲区, 是存贮数据的临时区,一般读写性能较高 \textcolor{red}{是存贮数据的临时区,一般读写性能较高} 是存贮数据的临时区,一般读写性能较高。缓存可在多个场景下使用 以一次 w e b 请求为例…

计算机网络——学习笔记

付费版:直接在上面的CSDN资源下载 免费版:https://wwsk.lanzouk.com/ijkcj13tqmyb 示例图:

基于MOT数据集的高精度行人检测系统(PyTorch+Pyside6+YOLOv5模型)

摘要:基于MOT数据集的高精度行人检测系统可用于日常生活中检测与定位行人目标,利用深度学习算法可实现图片、视频、摄像头等方式的行人目标检测识别,另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型训练数据集&…

数据可视化(3)

1.饼状图 #饼状图 #pie(x,labels,colors,labeldistance,autopct,startangle,radius,center,textprops) #x,每一块饼状图的比例 #labels:每一块饼形图外侧显示的文字说明 #labeldistance:标记的绘制位置,相对于半径的比例&#xf…

[论文笔记] CLRerNet: Improving Confidence of Lane Detection with LaneIoU

Honda, Hiroto, and Yusuke Uchida. “CLRerNet: Improving Confidence of Lane Detection with LaneIoU.” arXiv preprint arXiv:2305.08366 (2023). 2023.05 出的一篇车道线检测的文章, 效果在CULane, CurveLanes SOTA 文章目录 简介LaneIoULineIoU存在问题为什么使用LaneIo…

阿里Java开发手册~集合处理

1. 【强制】关于 hashCode 和 equals 的处理,遵循如下规则: 1 ) 只要重写 equals ,就必须重写 hashCode 。 2 ) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断&#xff…

【雕爷学编程】MicroPython动手做(02)——尝试搭建K210开发板的IDE环境3

4、下载MaixPy IDE,MaixPy 使用Micropython 脚本语法,所以不像 C语言 一样需要编译,要使用MaixPy IDE , 开发板固件必须是V0.3.1 版本以上(这里使用V0.5.0), 否则MaixPy IDE上会连接不上, 使用前尽量检查固…

基于fpga_EP4CE6F17C8实现的呼吸灯

文章目录 前言实验手册(EP4CE6F17C8)一、实验目的二、实验原理理论原理 三、系统架构设计四、模块说明1.模块端口信号列表2.状态转移图3.时序图 五、仿真波形图六、引脚分配七、代码实现八、仿真代码九、板级验证效果 …