一文搞懂设计模式之工厂模式

大家好,我是晴天,本周将同大家一起学习设计模式系列的第二篇文章——工厂模式,我们将依次学习简单工厂模式,工厂方法模式和抽象工厂模式。拿好纸和笔,我们现在开始啦~

一文搞懂设计模式之工厂模式.png

前言

我们在进行软件开发的时候,虽然不使用任何设计模式也不耽误搬砖,但是,这样会导致代码可重用性可扩展性可读性可维护性都大打折扣。所以强烈建议使用合适的设计模式进行软件开发。设计模式可以划分成三大类:创建型结构型行为型。本文将先从最基础的创建型模型——工厂模型介绍,工厂模式具体可以划分成简单工厂模式工厂方法模式抽象工厂模式,我们依次来学习一下。

为什么需要工厂模式

我们首先先来看一下下面未使用任何工厂模式的代码,看一下有什么问题?

package mainimport "fmt"// 不使用工厂方法模式
// 实现层
type Car struct {name string
}func (c *Car) Show(name string) {if name == "AUDI" {c.name = "奥迪"} else if name == "BMW" {c.name = "宝马"} else {c.name = "保时捷"}fmt.Println("我是:" + c.name)
}func NewCar(name string) Car {var c Carif name == "BMW" {c.name = "宝马"} else if name == "AUDI" {c.name = "奥迪"} else {c.name = "保时捷"}return c
}// 业务逻辑层
func main() {// 定义Carvar c Car// 创建一个具体的AUDICarc = NewCar("AUDI")c.Show("AUDI") // 我是:奥迪c = NewCar("BMW")c.Show("BMW") // 我是:宝马c = NewCar("CHEVROLET")c.Show("CHEVROLET") // 我是:保时捷
}

代码解释:首先在实现层定义了一个Car产品,且有一个Show方法展示自己的品牌,同时写了一个构造函数,用来根据入参来创建具体的Car。在业务逻辑层,创建了一个具体的Car,并通过给NewCar传入不同的参数,创建不同的Car。

存在的问题

  1. 当我们新增车的品牌时,需要对NewCar进行调整和改造,并且也需要对Show方法进行改造,这就涉及到新增产品类别的时候,需要修改类的源代码,这显然违背了开闭原则;同时还会使得Show这个方法变得越来越庞大,这也违背了类的单一职责原则。(对这两个原则不太清楚的小伙伴,可以参考这篇文章一文搞懂设计模式之七大原则)
  2. 实现层和业务逻辑层是高度耦合的,业务逻辑层既要负责创建对象,又要负责使用对象,如果要修改某些逻辑,不仅要修改实现层的代码,业务逻辑层的代码也需要进行改动,会使得创建的逻辑下沉到了业务逻辑层。这显然也是不符合设计模式最基本的一个思想——高内聚,低耦合

为了解决上述问题,我们使用了工厂模式。

简单工厂模式

类图

简单工厂.drawio.png

先来看一下简单工厂的类图,主要分为四个部分:抽象产品类、具体产品类、具体工厂类和main(业务逻辑)。
抽象产品类定义了一个Show方法,需要具体产品类去实现这个Show方法;具体产品类实现Show方法,打印出自己属于哪个具体的产品类型;具体工厂类有一个具体的生产产品的方法CreateComputer,且返回值为抽象产品类Computer(这里一定要返回抽象产品类,方法内部生产具体的产品对象,使用父类指针指向子类指针实现多态

tips:类图中抽象产品类和具体产品类之间的箭头表示继承的意思,具体产品类需要继承抽象产品类的方法,即需要完成该方法的具体实现。工厂类和具体产品类之间的箭头表示依赖的意思,表示CreateComputer方法内部需要用到具体产品类的对象。

代码示例:

package mainimport "fmt"// 练习题:
// 实现简单工厂模式
// 产品类是电脑// 抽象层
// 抽象电脑,有一个Show方法
type Computer interface {Show()
}// 实现层
// 具体产品类
type Macbook struct {Name string
}func (m *Macbook) Show() {fmt.Println(m.Name)
}type Lenovo struct {Name string
}func (l *Lenovo) Show() {fmt.Println(l.Name)
}// 工厂类
type EasyFactory struct {
}// 根据入参,生产具体的产品
func (e *EasyFactory) CreateComputer(name string) Computer {var c Computerif name == "Macbook" {c = &Macbook{Name: name}} else if name == "Lenovo" {c = &Lenovo{Name: name}}return c
}// 对产品类符合开闭原则
// 对于工厂类来说,不符合开闭原则,每新增一个产品种类,都需要修改工厂类的方法
// 业务逻辑
func main() {// 1.创建一个抽象产品var c Computer// 2.创建一个具体工厂对象var ef = EasyFactory{}// 3.调用工厂方法,生产具体产品c = ef.CreateComputer("Macbook")// 4.调用具体产品的方法c.Show()// 5.再次调用工厂方法,生产具体产品 (实现多态)c = ef.CreateComputer("Lenovo")c.Show()
}

代码解释:定义一个Computer抽象产品,定义两个品牌的具体产品类Macbook和Lenovo,定义一个具体工厂类,并且定义一个根据入参创建具体电脑品牌的产品。main函数内部的注释就是按照步骤创建抽象产品、创建工厂、创建具体产品并调用产品方法的过程。

优点

  1. 把具体产品对象的创建和使用进行了解耦合,由工厂实现对具体产品对象的创建,业务逻辑层只需要跟工厂交互,然后使用工厂生产出来的具体产品即可,不用关心对象的创建过程如何。
  2. 将具体产品的初始化工作放到了工厂方法里面,做到了对产品的面向接口编程。

缺点

  1. 对工厂来说,不符合开闭原则,每新增一个具体产品,都需要修改工厂方法。
  2. 工厂的创建方法业务逻辑过于繁重,如果该方法不能顺利执行,将会发生非常严重的问题,整个程序将无法正常运行。

工厂方法模式

工厂方法模式可以理解成:简单工厂模式+开闭原则

类图

工厂方法模式.drawio (3).png

来看一下工厂方法模式的类图,分为五个部分:抽象产品类、具体产品类、抽象工厂类、具体工厂类和main(业务逻辑)。抽象产品类定义了一个Brand方法,需要具体产品类去实现这个Brand方法;具体产品类实现Brand方法,打印出自己属于哪个具体的产品品牌;抽象工厂类定义了ProduceMilk方法,具体工厂类继承抽象工厂类,需要实现ProduceMilk方法,且返回值为抽象产品类Milk(这里一定要返回抽象产品类,方法内部生产具体的产品对象,使用父类指针指向子类指针实现多态

代码示例:

package mainimport "fmt"// 工厂方法就是:简单工厂+开闭原则// 抽象层
// 抽象产品类
type Milk interface {Brand()
}// 抽象工厂类
type AbstractMilkFactory interface {ProduceMilk() Milk
}// 实现层
// 牛奶类的具体产品类
type MengNiu struct {brand string
}func (m *MengNiu) Brand() {fmt.Println("品牌:" + m.brand)
}// 牛奶类的具体产品类
type YiLi struct {brand string
}func (y *YiLi) Brand() {fmt.Println("品牌:" + y.brand)
}// 工厂类的具体对象
// 蒙牛工厂
type MengNiuFactory struct {
}func (m *MengNiuFactory) ProduceMilk() Milk {var mengNiu MengNiumengNiu.brand = "蒙牛"return &mengNiu
}// 伊利工厂
type YiLiFactory struct {
}func (y *YiLiFactory) ProduceMilk() Milk {var yili YiLiyili.brand = "伊利"return &yili
}// 业务逻辑层
func main() {// 1.创建蒙牛抽象牛奶工厂var mnFacroty AbstractMilkFactory// 2.实例化成蒙牛工厂mnFacroty = new(MengNiuFactory)// 3.生产蒙牛牛奶mnMilk := mnFacroty.ProduceMilk()// 4.显示牛奶品牌mnMilk.Brand()// 5.创建伊利抽象牛奶工厂var ylFactory AbstractMilkFactory// 6.实例化成伊利工厂ylFactory = new(YiLiFactory)// 7.生产伊利牛奶ylMilk := ylFactory.ProduceMilk()// 8.显示牛奶品牌ylMilk.Brand()
}

代码解释:定义抽象牛奶产品和抽象牛奶工厂,定义蒙牛和伊利两种具体牛奶产品和具体牛奶工厂。main函数(业务逻辑层)只需要跟抽象牛奶产品和抽象牛奶工厂进行交互,符合依赖倒转原则(对这个原则不太清楚的小伙伴,可以参考这篇文章一文搞懂设计模式之七大原则)

优点

  1. 新增一个具体产品时,无需修改产品和工厂的源代码,符合开闭原则
  2. 每个工厂只负责创建一个产品,不需要多个if…else判断创建什么产品,符合单一职责原则
  3. 业务逻辑层只需要跟抽象的产品和抽象的工厂进行交互,符合依赖倒转原则

缺点

  1. 每新增一个具体产品类,都需要新增一个对应的工厂类,代码新增程度是1:1的,增加了程序的复杂程度和代码工作量。
  2. 很难对抽象产品类或者抽象工厂类进行扩展,一旦扩展,所有子类都需要进行修改。

抽象工厂模式

在介绍抽象工厂模式之前先介绍两个概念:产品等级结构产品族

抽象工厂模式.drawio (1).png

产品等级结构:产品等级结构即产品的继承结构。通俗理解:具有相同功能但是来自于不同生产厂商的产品零部件,它们的一个完备集合叫做产品等级结构。

产品族:指由同一个工厂生产的,位于不同产品等级结构中的一组产品。

类图

抽象工厂UML.drawio.png
来看一下抽象工厂的类图,首先明确有哪些抽象类和哪些具体类,抽象类是一个完整的产品等级结构,抽象衣服类、抽象裤子类、抽象鞋类,抽象的工厂类;具体类有中国衣服类、中国裤子类、中国鞋类、日本衣服裤子鞋类,中国工厂类和日本工厂类。中国的衣服裤子鞋组成了中国的产品族,日本衣服裤子鞋组成了日本产品族。中国工厂只依赖于(生产)中国的衣服裤子鞋,日本工厂只依赖于(生产)日本的衣服裤子鞋。

代码示例:

package mainimport "fmt"// 抽象工厂的作用在于不用每新建一个品类,都创建一个工厂,每个工厂生产一整套产品等级结构
// 练习:产品等级结构为
// clothes trousers shoes  衣服 裤子 鞋// 抽象层
// 产品等级结构为Clothes,trousers,shoes
type Clothes interface {PutOn()
}
type Trousers interface {PutOn()
}
type Shoes interface {TakeOn()
}// 产品族:生产完整的产品等级结构的工厂
// 一个工厂能生产出完整的产品
type AbsFactory interface {CreateClothes() ClothesCreateTrousers() TrousersCreateShoes() Shoes
}// 实现层
// 不同产品族的产品等级结构全部创建完成
type ChinaClothes struct {
}func (c *ChinaClothes) PutOn() {fmt.Println("穿上中国衣服")
}type ChinaTrousers struct {
}func (c *ChinaTrousers) PutOn() {fmt.Println("穿上中国裤子")
}type ChinaShoes struct {
}func (c *ChinaShoes) TakeOn() {fmt.Println("穿上中国鞋")
}type JapanClothes struct {
}func (j *JapanClothes) PutOn() {fmt.Println("穿上日本衣服")
}type JapanTrousers struct {
}func (j *JapanTrousers) PutOn() {fmt.Println("穿上日本裤子")
}type JapanShoes struct {
}func (j *JapanShoes) TakeOn() {fmt.Println("穿上日本鞋")
}// 创建中国和日本工厂
type ChinaFactory struct {
}func (c *ChinaFactory) CreateClothes() Clothes {return &ChinaClothes{}
}
func (c *ChinaFactory) CreateTrousers() Trousers {return &ChinaTrousers{}
}
func (c *ChinaFactory) CreateShoes() Shoes {return &ChinaShoes{}
}type JapanFactory struct {
}func (j *JapanFactory) CreateClothes() Clothes {return &JapanClothes{}
}
func (j *JapanFactory) CreateTrousers() Trousers {return &JapanTrousers{}
}
func (j *JapanFactory) CreateShoes() Shoes {return &JapanShoes{}
}// 业务逻辑层
func main() {// 1.定义抽象中国工厂var chinaFac AbsFactory// 2.实例化中国工厂chinaFac = new(ChinaFactory)// 3.中国工厂生产中国衣服chinaClothes := chinaFac.CreateClothes()// 4.中国工厂生产中国裤子chinaTrousers := chinaFac.CreateTrousers()// 5.中国工厂生产中国鞋chinaShoes := chinaFac.CreateShoes()// 6.调用中国产品的方法chinaClothes.PutOn()chinaTrousers.PutOn()chinaShoes.TakeOn()// 日本工厂同理var japanFac AbsFactoryjapanFac = new(JapanFactory)japanClothes := japanFac.CreateClothes()japanTrousers := japanFac.CreateTrousers()japanShoes := japanFac.CreateShoes()japanClothes.PutOn()japanTrousers.PutOn()japanShoes.TakeOn()
}

优点

  1. 拥有工厂模式的优点
  2. 新增一个产品族的时候,只需要新增一个具体的工厂即可,符合开闭原则

缺点

  1. 如果产品等级结构发生变化,对于工厂类来说,抽象工厂类需要新增方法,所有产品族都需要进行修改,不符合开闭原则。
  2. 抽象程度比较高,代码理解起来有一定的困难

总结:

  1. 简单工厂模式:让一个工厂类创建所有的产品,不利于扩展和维护,违背了开闭原则
  2. 工厂方法模式:就是在简单工厂模式的基础上,增加了开闭原则,但是每个工厂类只能创建一种产品,使得代码中类的数量过于庞大
  3. 抽象工厂模式:拥有工厂方法模式的优点,但是对于扩充产品等级结构不友好,违反开闭原则。

写在最后:

感谢大家的阅读,晴天将继续努力,分享更多有趣且实用的主题,如有错误和纰漏,欢迎给予指正。 更多文章敬请关注作者个人公众号 晴天码字。 我们下期不见不散,to be continued…

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

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

相关文章

driver.find_element()用法

driver.find_element()用于在Web页面中定位单个元素。它是Selenium WebDriver库中的 一种方法。该方法接受一个定位器(locator)和一个值作为参数,用于指定要查找的元素 位置。下面是具体的用法和一些例子: 通过ID定位元素&#x…

虚拟机vmware使用桥接方式联网设置

步骤:虚拟机设置----》网络适配器---->桥接模式 这样设置好;只是这样设置是无法联网的 现在进入到虚拟机内部----->电机右上角的”网络连接“(wired connection)(没错就是wired connection 虽然是连接WiFi热点但…

【实战Flask API项目指南】之二 Flask基础知识

实战Flask API项目指南之 Flask基础知识 本系列文章将带你深入探索实战Flask API项目指南,通过跟随小菜的学习之旅,你将逐步掌握Flask 在实际项目中的应用。让我们一起踏上这个精彩的学习之旅吧! 前言 当小菜踏入Flask后端开发的世界&…

零日漏洞预防

零日漏洞,是软件应用程序或操作系统(OS)中的意外安全漏洞,负责修复该漏洞的一方或供应商不知道该漏洞,它们仍然未被披露和修补,为攻击者留下了漏洞,而公众仍然没有意识到风险。 零日攻击是如何…

【css3】涟漪动画

效果展示 dom代码 <div class"mapSelfTitle66"><div></div> </div> 样式代码 .mapSelfTitle66{width:120px;height:60px;position: relative;&>div{width:100%;height:100%;background: url("~/assets/images/video_show/err…

手写数字识别--神经网络实验

实验源码自取&#xff1a; 我自己搞的代码&#xff0c;预测精度才94% 神经网络实验报告源码.zip - 蓝奏云 老师给的实验源码答案和资料&#xff0c;预测精度高达99% 深度学习实验报告.zip - 蓝奏云 上深度学习的课程&#xff0c;老师布置了一个经典的实验报告&#xff0c;我做…

利用移动互联、物联网、智能算法、地理信息系统、大数据分析等信息技术开发的智慧工地云平台源码

智慧工地是指利用移动互联、物联网、智能算法、地理信息系统、大数据挖掘分析等信息技术&#xff0c;提高项目现场的“人•机•料•法•环•安”等施工要素信息化管理水平&#xff0c;实现工程施工可视化智能管理&#xff0c;并逐步实现绿色生态建造。 技术架构&#xff1a;微…

数据分析实战 - 2 订单销售数据分析(pandas 进阶)

题目来源&#xff1a;和鲸社区的题目推荐&#xff1a; 刷题源链接&#xff08;用于直接fork运行 https://www.heywhale.com/mw/project/6527b5560259478972ea87ed 刷题准备 请依次运行这部分的代码&#xff08;下方4个代码块&#xff09;&#xff0c;完成刷题前的数据准备 …

PYTHON学习

元组不可修改&#xff1a; 元组支持下标索引。 字符串也是容器&#xff0c;不支持修改。

python加上ffmpeg实现音频分割

前言: 这是一个系列的文章,主要是使用python加上ffmpeg来对音视频文件进行处理,包括音频播放、音频格式转换、音频文件分割、视频播放等。 系列文章链接: 链接1: python使用ffmpeg来制作音频格式转换工具(优化版) 链接2:<Python>PyQt5+ffmpeg,简单视频播放器的编写(…

Arduino设置SoftwareSerial缓冲区大小

SoftwareSerial的缓冲区大小设置 概述修改缓冲区的大小实验 概述 新的Arduino的ESP8266软串口的缓冲区原来老的库中有宏定义可以用来修改接收和发送缓冲区的大小。在现在新的库中已经没有这个设置了&#xff0c;那怎么才能修改缓冲区的大小哪&#xff1f; 修改缓冲区的大小 …

【借力打力】记一次由于堆栈信息不详细的错误排查方法,利用访问日志进行定位问题

【借力打力】记一次由于堆栈信息不详细的错误排查方法&#xff0c;利用访问日志进行定位问题 1&#xff0c;背景2&#xff0c;排查步骤2.1 调用方问题2.2 Nginx手段2.3 运维工具辅助2.4 嵌入tomcat日志记录 3&#xff0c;结果 1&#xff0c;背景 异常信息每隔50分钟显示一次&a…

Libevent网络库原理及使用方法

目录 1. Libevent简介2. Libevent事件处理流程3. Libevent常用API接口3.1 地基——event_base3.2 事件——event3.3 循环等待事件3.4 自带 buffer 的事件——bufferevent3.5 链接监听器——evconnlistener3.6 基于event的服务器程序3.7 基于 bufferevent 的服务器和客户端实现 …

Spring cloud负载均衡 @LoadBalanced注解原理

接上一篇文章&#xff0c;案例代码也在上一篇文章的基础上。 在上一篇文章的案例中&#xff0c;我们创建了作为Eureka server的Eureka注册中心服务、作为Eureka client的userservice、orderservice。 orderservice引入RestTemplate&#xff0c;加入了LoadBalanced注解&#x…

大数据(十):数据可视化(二)

专栏介绍 结合自身经验和内部资料总结的Python教程&#xff0c;每天3-5章&#xff0c;最短1个月就能全方位的完成Python的学习并进行实战开发&#xff0c;学完了定能成为大佬&#xff01;加油吧&#xff01;卷起来&#xff01; 全部文章请访问专栏&#xff1a;《Python全栈教…

物理机安装黑群晖

物理机安装黑群晖 黑群晖系统: DSM 6.2.0–6.2.3 引导版本: 1.04b (首选推荐版本) 针对机型: DS918 引导方式: 引导方式&#xff1a;传统BIOS和UEFI&#xff08;可选&#xff09; 点击此处链接&#xff0c;引导下载地址 点此处为&#xff1a;DS918 DSM 6.2.3-25426 Update2 系…

Leetcode实战

我们今天来利用这段时间的学习实操下我们的oj题。 int removeElement(int* nums, int numsSize, int val){int dst0;int src0;while(src<numsSize){if(nums[src]!val){nums[dst]nums[src];}elsesrc;}return dst;}我们这里用用两个下标&#xff0c;src来移动&#xff0c;如果…

linux的shell script判断用户输入的字符串,判断主机端口开通情况

判断输入的字符串是否是hello 图一运行报错 检查发下&#xff0c;elif 判断里面少个引号&#xff0c;哎&#xff0c;现在小白到了&#xff0c;一看就会&#xff0c;一写就错的时候了&#xff0c;好像现在案例比较简单&#xff0c;行数较少。 案例二 if 结合test 判断主机端…

小程序如何设置用户同意服务协议并上传头像和昵称

为了保护用户权益和提供更好的用户体验&#xff0c;设置一些必填项和必读协议是非常必要的。首先&#xff0c;用户必须阅读服务协议。服务协议是明确规定用户和商家之间权益和义务的文件。通过要求用户在下单前必须同意协议&#xff0c;可以确保用户在使用服务之前了解并同意相…

第11章_数据处理之增删改

文章目录 1 插入数据1.1 实际问题1.2 方式 1&#xff1a;VALUES的方式添加1.3 方式2&#xff1a;将查询结果插入到表中演示代码 2 更新数据演示代码 3 删除数据演示代码 4 MySQL8新特性&#xff1a;计算列演示代码 5 综合案例课后练习 1 插入数据 1.1 实际问题 解决方式&#…