再谈设计模式系列 - 工厂模式

背景

自己是回头再看设计模式的,发现网络上充斥着千篇一律的什么开宝马,买衣服的例子,根本就无法让人理解为什么要这样做,讲的非常生硬,完全没有说服力。所以自己尝试结合工作中看到的,然后稍微总结一下这种模式。

假设在一个基本的MVC框架中,视图层承担着渲染数据的职责,要实现一个框架的视图层,必须满足

  1. 支持html输出
  2. 支持json格式输出
  3. 支持xml格式输出
    于是,我们可能这样写
go 复制代码
type View struct{}

func (v *View) Render(renderType string, data map[string]interface{}) {
    switch renderType {
        case "json":
        // 将数据编码为 JSON
        jsonData, err := json.Marshal(data)
        if err != nil {
            fmt.Println("Error encoding JSON:", err)
            return
        }
        fmt.Println(string(jsonData))

        case "html":
        // 渲染为 HTML
        fmt.Printf("<div>%v</div>\n", data)

        case "xml":
        // 渲染为 XML
        w := &xml.Writer{}
        w.Header()
        fmt.Println("Using another function.")

    }
}

然后,我们在使用的时候会这样调用:

go 复制代码
func main() {
    view := &View{}
    data := map[string]interface{}{
        "name": "zhangsan",
    }
    view.Render("json", data)
}

问题分析:这样做代码比较简单,但是,会非常不灵活,比如当下面需求出现的时候:

  1. 如果再添加一个excel类型的输出,无法做到灵活地扩展和维护,switch分支将变得老长老长
  2. 如果需要修改一下xml的头信息,只能在原来的基础上修改,这样代码风险进一步提升

问题分析

实际上,这是一种典型的面向过程的编码方式导致的,这种编码方式,不易分工,职责不明确,可以采用面向对象的方式来救场,针对接口编程而不是针对实现编程。

面向对象:大家设想一下我们出门旅游找"司机",我们的目的是找一个"司机",而不是找一个具体的"人",只要对方持有驾照,我们就能知道他是"司机",他就具备开车技能。这里"司机"是个抽象,某一个"人"才是具体。

对照上面的例子,我们应该重新梳理出关系:

司机 = View

某一个人 = HtmlView

技能 = Render()

如果你的思路是这样的,其实你用没用简单工厂模式没那么重要了,只不过我们的前辈把这种问题更加优雅地解决掉了,留给了我们后人一些很经典的解决问题的设计模式。接下来,使用设计模式中的简单工厂模式解决这个问题

最后我们再描述一下实际生活中的问题:我有一个驾车旅行计划,我找到携程(工厂)为我推荐一个能开山路的司机(具体产品)来帮我在318国道危险路段上驾车(抽象产品功能)

第一版:简单工厂模式

按照上面的思路,我们可以抽象出一个View,它具备Render方法实现具体的渲染,而HtmlView,JsonView等都是具体的产品,使用方(Client)从工厂(Factory)获取具体的产品

定义抽象View和产品功能Render

go 复制代码
// 定义 View 接口
type View interface {
    Render(data map[string]interface{})
}

定义具体的产品实现产品功能Render

go 复制代码
package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
)

// 定义 View 接口
type View interface {
    Render(data map[string]interface{})
}

// JsonView 的具体实现
type JsonView struct{}

func (j JsonView) Render(data map[string]interface{}) {
    jsonData, err := json.Marshal(data)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return
    }
    fmt.Println(string(jsonData))
}

// HtmlView 的具体实现
type HtmlView struct{}

func (h HtmlView) Render(data map[string]interface{}) {
    for key, value := range data {
        fmt.Printf("<div>%s: %v</div>\n", key, value)
    }
}

// XmlView 的具体实现
type XmlView struct{}

func (x XmlView) Render(data map[string]interface{}) {
    //Xml实现复杂一些,这里省略......
}

工厂类的实现:

go 复制代码
// Factory 工厂类
type Factory struct{}

// CreateView 根据类型创建 View 实例
func (f Factory) CreateView(viewType string) View {
	switch viewType {
	case "json":
		return JsonView{}
	case "html":
		return HtmlView{}
	case "xml":
		return XmlView{}
	default:
		return HtmlView{} // 默认返回 HtmlView
	}
}

客户端使用代码:

go 复制代码
func main() {
	// 使用 Factory 创建 JsonView 实例
	factory := Factory{}
	view := factory.CreateView("json")

	// 调用 JsonView 的 Render 方法
	view.Render(map[string]interface{}{
		"name": "zhangsan",
	})
}

其实,简单工厂模式,很多代码到这一层就已经非常不错了,一般的变动是绝对可以应付的,至于网上说的每次新增一个产品要改Factory工厂类,这完全不是问题,Factory里面的逻辑是很清晰的。正式的工厂方法模式一定是在特定的复杂条件下产生的,所以如果要引出正式工厂模式,那么背景一定要复杂。

继续分析问题

我们还是先分析现实生活中的问题:

我有一个驾车旅行计划,我找到携程(工厂)为我推荐一个能开山路的司机(具体产品)来帮我在318国道危险路段上驾车(抽象产品功能)

我们抽象出"司机"和司机的"驾车"功能,是因为对"司机"要求是复杂的,有时是开山路,希望找一个熟悉山路的,有时是开雪路,希望找一个有雪路开车经验的,甚至有的时候在国外开车,就得找国外的司机

同样,如果要抽象工厂,那么对工厂的要求一定是变复杂了,比如我们在偏远地区没有"携程"旅游公司,就必须要一个当地的旅游公司,甚至,如果携程没有熟悉山地路司机,那么公路和山地路可能就不同的旅游公司提供服务

这种情况下,工厂变得复杂了,如果你不做抽象,那你还是用面向过程方式编写工厂类的代码,这时候需要进一步抽象,降低复杂度,只要这个工厂具备生产功能即可,它是哪个具体的工厂我们并不关心。

如果你继续这样分析问题,好像用不用正式工厂模式也不那么重要了。

回到我们View的例子中的工厂类别,这16行代码是简单的,假设我们现在CreateView有非常复杂的逻辑,这里面需要应对以下升级功能

  1. 版本:xml有不同的版本,html也有不同版本,希望能指定版本返回
  2. 场景:在微信容器内,对json,html的要求不一样,需要一些返回一些特殊字段
  3. 安全:在一些敏感场景中,需要能加密输出全部的内容

这种情况下我们希望升级下面的Factory工厂类,如果不提前抽象化工厂,那么这种升级是非常困难的

go 复制代码
// Factory 工厂类
type Factory struct{}

// CreateView 根据类型创建 View 实例
func (f Factory) CreateView(viewType string) View {
	switch viewType {
	case "json":
		return JsonView{}
	case "html":
		return HtmlView{}
	case "xml":
		return XmlView{}
	default:
		return HtmlView{} // 默认返回 HtmlView
	}
}

比如,我们想实现一个安全加密的json渲染,这时候你可能这样改动,新增一个safejson

go 复制代码
// Factory 工厂类
type Factory struct{}

// CreateView 根据类型创建 View 实例
func (f Factory) CreateView(viewType string) View {
	switch viewType {
	case "json":
		return JsonView{}
	case "safejson":
		return SafeJsonView{}
    //......
}

如果你这样做,本质还是面向过程,没有抽象,不管是普通的json还是安全加密的json,他们都属于json这个类别的东西。这就好比你把携程和开山地路的司机看成了一个维度的东西,这就出问题了。一定要在一个维度上看待事物,抽象确实是门艺术,如果不能正确抽象,设计模式只能做到生搬硬套。

第二版:工厂模式

按照上面的分析,我们可以找一个Json工厂,但是我们对这个工厂有要求(注意,是对工厂的要求),它生产出来的产品是需要有安全能力的。

所以首先定义工厂接口,把原来的具体实现变成抽象:

go 复制代码
// Factory 抽象类
type Factory interface {
    CreateView() View
}

然后具体实现生产Json的安全和非安全的工厂

go 复制代码
// Factory 抽象类
type Factory interface {
    CreateFactory() View
}

//默认Json实现
type JsonFactory struct{}
func(j *JsonFactory) CreateFactory() View{
    return JsonView{}
}

//安全的Json实现
type SafeJsonFactory struct{}
func(j *SafeJsonFactory) CreateFactory() View{
    return SafeJsonView{}
}

客户端使用:

go 复制代码
func main() {
	// 使用 SafeJsonFactory 创建 Json 工厂
	factory := SafeJsonFactory{}
	view := factory.CreateFactory()

	// 调用 Render 方法
	view.Render(map[string]interface{}{
		"name": "zhangsan",
	})
}

注意,回到我们一开始工厂模式解决的问题,是应该工厂变复杂了。所以这里客户端的使用一般不会这样直接写,会交给一个工厂抉择函数,这个工厂抉择类帮我们选择各种各样的工厂。

这个函数的职责是返回不同的工厂(也可以想象成服务提供商),注意,这个函数的逻辑一定要够复杂,否则就简单工厂模式就足以应对了

go 复制代码
func factoryChoose(req http.Request) View {
    //假设应对监管要求,需要加密,其他时间没有监管的安全要求
    if 监管期间 {
    	factory := SafeJsonFactory{}
    	return factory.CreateFactory()
    else{
        factory := JsonFactory{}
    	return factory.CreateFactory()
    }
    //其他更加复杂条件下的不同工厂返回
    ......
}

总结

其实,抛开设计模式不谈,这些问题在实际生活中各个领域也出现过,不单单是编程领域的问题。再看简单工厂模式,其实解决的的问题是产品很复杂,有各种各样的产品,我们做了一层抽象,用来约束产品的功能,这样的好处是产品可以随时替换。

然后,我们发现某一产品的工厂是多家可以提供的,我们有的场景选择A工厂提供服务,有的场景选择B工厂提供服务,这个时候,我们又做了一层抽象,用来约束工厂的功能,这样的好处是工厂可以随时替换。

最后说一点感受,本人是从事公司创新产品研发的,发现很多时候工作就像楼下开一家小超市,不需要考虑这么多问题,或者说比这点设计模式重要的问题多了去了,能不能活下来才是最重要的。如果很幸运,你的超市不断壮大,你需要提供有品质,稳定的服务,这时候再考虑有替换供应商的风险,再做一些设计,是完全来得及,并且是收益最高的时候。

相关推荐
isolusion2 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder3 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚4 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心4 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴5 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲5 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心5 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
hanglove_lucky6 小时前
本地摄像头视频流在html中打开
前端·后端·html
皓木.8 小时前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端