设计模式-建造者模式

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

一、建造者模式概念理解

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

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

我不知道你们能不能理解这个很抽象的定义,起初我是不太能理解。我似乎从“分离”上能看出是要解耦,至于“构建”、“表示”这两个词本身就有模糊的感觉。实际上,这里的“构建”可以理解为“里子”,“表示”可以理解为“面子”,“同样的构建过程可以创建不同的表示”就是里子和多个面子需要解耦,如同一个人可以拥有很多面具以示人。意即,你还是你,但是外人看到的可以有多个。
“构建”指的就是里子。每一个可由外界创建对象的类都会提供一个或多个构造函数,或为有参构造,亦或为无参构造。对象的正规创建最常用的方式通过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;

    @Override
    public void setGraphicsCard(Object graphicsCard) {
        this.graphicsCard = graphicsCard;
    }

    @Override
    public void setMonitor(Object monitor) {
        this.monitor = monitor;
    }

    @Override
    public void setKeyboard(Object keyboard) {
        this.keyboard = keyboard;
    }

    @Override
    public void setMouse(Object mouse) {
        this.mouse = mouse;
    }

    @Override
    public void setSoundCard(Object soundCard) {
        this.soundCard = soundCard;
    }

    @Override
    public void setNetworkCard(Object networkCard) {
        this.networkCard = networkCard;
    }

    @Override
    public void setOpticalDrive(Object opticalDrive) {
        this.opticalDrive = opticalDrive;
    }
    
    @Override
    public 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;
    }
    
	@Override
    public 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类来负责处理。

建造者模式优点:

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

建造者模式缺点:

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

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

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

相关文章

图文教程:使用 Photoshop、3ds Max 和 After Effects 创建被风暴摧毁的小屋

推荐: NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 1. 在 Photoshop 中设置图像 步骤 1 打开 Photoshop。 打开 Photoshop 步骤 2 我已经将小屋的图像导入到Photoshop中以演示 影响。如果您愿意,可以使用其他图像。 图片导入 步骤 3 由于小…

css实现渐变边框动画

渐变边框动画 1、实现效果2、实现代码 1、实现效果 2、实现代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0">&…

Jenkins搭建最简教程

纠结了一小会儿&#xff0c;到底要不要写这个&#xff0c;最终还是决定简单记录一下&#xff0c;因为Jenkins搭建实在是太简单了&#xff0c;虽然也有坑&#xff0c;但是坑主要在找稳定的版本上。 先学一个简称&#xff0c;LTS (Long Term Support) 属实是长见识了&#xff0c…

【高级数据结构】并查集

目录 修复公路&#xff08;带扩展域的并查集&#xff09; 食物链&#xff08;带边权的并查集&#xff09; 修复公路&#xff08;带扩展域的并查集&#xff09; 洛谷&#xff1a;修复公路https://www.luogu.com.cn/problem/P1111 题目背景 A 地区在地震过后&#xff0c;连接…

Qt —— Vs2017编译hiredis源码并测试调用(附调用hiredis库源码)

下载hiredis源码 编译hiredis源码 1、解压下载的hiredis源码包,如图使用Vs2017打开hiredis_win.sln 2、如下两图,Vs2017打开.sln后点击升级。 分别对两个工程的debug、release进行配置。Debug配置为多线程调试DLL(MDd)、Release配置为多线程DLL(/MD),这样做是为了配合被调用…

GAMES104里渲染等一些剩下的问题

渲染的一些剩下的问题 1. 如何理解渲染中的AO(环境光遮蔽) 环境光遮蔽 我们先从一个简单的效果开始—环境光遮蔽(Ambient Occlusion,以下简称AO)。大家可以看到&#xff0c;下图中的场景没有任何渲染效果&#xff0c;也没有任何着色效果&#xff0c;但场景呈现出了非常清晰的…

Flutter:滑动面板

前言 无意中发现了这个库&#xff0c;发现现在很多app中都有类似的功能。以手机b站为例&#xff0c;当你在看视频时&#xff0c;点击评论&#xff0c;视频会向上偏移&#xff0c;下方划出评论界面。 sliding_up_panel SlidingUpPanel是一个Flutter插件&#xff0c;用于创建滑…

【Python机器学习】实验04(2) 机器学习应用实践--手动调参

文章目录 机器学习应用实践1.1 准备数据此处进行的调整为&#xff1a;要所有数据进行拆分 1.2 定义假设函数Sigmoid 函数 1.3 定义代价函数1.4 定义梯度下降算法gradient descent(梯度下降) 此处进行的调整为&#xff1a;采用train_x, train_y进行训练 1.5 绘制决策边界1.6 计算…

CentOS 7安装PostgreSQL 15版本数据库

目录 一、何为PostgreSQL&#xff1f; 二、PostgreSQL安装 2.1安装依赖 2.2 执行安装 2.3 数据库初始化 2.4 配置环境变量 2.5 创建数据库 2.6 配置远程 2.7 测试远程 三、常用命令 四、用户创建和数据库权限 一、何为PostgreSQL&#xff1f; PostgreSQL是以加州大学…

Python接口自动化测试框架运行原理及流程

这篇文章主要介绍了Python接口自动化测试框架运行原理及流程,文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文总结分享介绍接口测试框架开发&#xff0c;环境使用python3selenium3unittestddtrequests测试框…

【MyBatis-Plus 进阶学习笔记】

MyBatis-Plus 进阶学习笔记记录 一、 MyBatis Plus 七大功能0. 数据准备1. 逻辑删除2. 自动填充2.1 优化1 自动填充 有的类没有更新和创建时间字段2.2 优化2 自己设置时间时填充自己设置的&#xff0c;不设置时自动填充 3. 乐观锁插件 注&#xff1a;wrapper不能服用4. 性能分析…

Docker续集+Docker Compose

目录 Containerd与docker的关系 runCrunC与Containerd的关联 OCI协议Dockerfile多阶段构建&#xff08;解决&#xff1a;如何让一个镜像变得更小 &#xff09;多阶段构建Images瘦身实践.dockerignore Docker Compose快速开始Quick StartCompose 命令常用命令命令说明 Compose 模…

吴师傅教你几招极速清理C盘,高能操作绝不让你失望!

电脑使用久了&#xff0c;C盘堆积的垃圾过多&#xff1b;每天上网会给电脑带来很多临时文件&#xff0c;这些垃圾文件不清理掉时间久了就会影响到电脑的运行速度&#xff1b;也会导致C盘变红&#xff0c;空间不足。那么&#xff0c;电脑C盘满了如何清理呢&#xff1f;教你几招极…

【MMdetection3d】Step1:环境搭建

Step1:环境搭建 1.创建并激活虚拟环境1.1 用官方Pytorch指令安装&#xff01;1.2 用官方mmcv指令安装&#xff01; 2 安装MMDetection3 克隆编译mmdetection3d4 环境测试5 测试demo 在Conda虚拟环境中搭建MMdetection3d环境 1.创建并激活虚拟环境 conda create -n mm3d python…

微信小程序:实现提示窗确定,取消执行不同操作(消息提示确认取消)showModal

效果 代码 wx.showModal({title: 提示,content: 是否确认退出,success: function (res) {if (res.confirm) {console.log(用户点击确定)} else if (res.cancel) {console.log(用户点击取消)}}})

一个开源的文件存储软件Filehub,不限速防和谐

FileHub介绍 一个基于Github开发的文件存储软件&#xff0c;美其名曰&#xff1a;FileHub&#xff0c;可存万物&#xff0c;而且绝不和谐任何文件。类似于百度云盘的功能&#xff0c;但是功能上肯定达不到百度云盘的效果&#xff0c;但是基本功能还是有的&#xff1a;例如登录注…

SpringBoot集成RocketMQ

SpringBoot整合RocketMQ使用非常简单&#xff0c;下面是一个简单的例子&#xff0c;作为备忘&#xff1a; 完整项目代码&#xff1a; https://github.com/dccmmtop/springBootRocketMQ 项目目录结构 依赖 <dependencies><dependency><groupId>org.apache.…

18.Netty源码之ByteBuf 详解

highlight: arduino-light ByteBuf 是 Netty 的数据容器&#xff0c;所有网络通信中字节流的传输都是通过 ByteBuf 完成的。 然而 JDK NIO 包中已经提供了类似的 ByteBuffer 类&#xff0c;为什么 Netty 还要去重复造轮子呢&#xff1f;本节课我会详细地讲解 ByteBuf。 JDK NIO…

VMware搭建Hadoop集群 for Windows(完整详细,实测可用)

目录 一、VMware 虚拟机安装 &#xff08;1&#xff09;虚拟机创建及配置 &#xff08;2&#xff09;创建工作文件夹 二、克隆虚拟机 三、配置虚拟机的网络 &#xff08;1&#xff09;虚拟网络配置 &#xff08;2&#xff09;配置虚拟机 主机名 &#xff08;3&#xf…

【LLM】大语言模型学习之LLAMA 2:Open Foundation and Fine-Tuned Chat Model

大语言模型学习之LLAMA 2:Open Foundation and Fine-Tuned Chat Model 快速了解预训练预训练模型评估微调有监督微调(SFT)人类反馈的强化学习(RLHF)RLHF结果局限性安全性预训练的安全性安全微调上手就干使用登记代码下载获取模型转换模型搭建Text-Generation-WebUI分发模型…