背景
自己是回头再看设计模式的,发现网络上充斥着千篇一律的什么开宝马,买衣服的例子,根本就无法让人理解为什么要这样做,讲的非常生硬,完全没有说服力。所以自己尝试结合工作中看到的,然后稍微总结一下这种模式。
假设在一个基本的MVC框架中,视图层承担着渲染数据的职责,要实现一个框架的视图层,必须满足
- 支持html输出
- 支持json格式输出
- 支持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)
}
问题分析:这样做代码比较简单,但是,会非常不灵活,比如当下面需求出现的时候:
- 如果再添加一个excel类型的输出,无法做到灵活地扩展和维护,switch分支将变得老长老长
- 如果需要修改一下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有非常复杂的逻辑,这里面需要应对以下升级功能
- 版本:xml有不同的版本,html也有不同版本,希望能指定版本返回
- 场景:在微信容器内,对json,html的要求不一样,需要一些返回一些特殊字段
- 安全:在一些敏感场景中,需要能加密输出全部的内容
这种情况下我们希望升级下面的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工厂提供服务,这个时候,我们又做了一层抽象,用来约束工厂的功能,这样的好处是工厂可以随时替换。
最后说一点感受,本人是从事公司创新产品研发的,发现很多时候工作就像楼下开一家小超市,不需要考虑这么多问题,或者说比这点设计模式重要的问题多了去了,能不能活下来才是最重要的。如果很幸运,你的超市不断壮大,你需要提供有品质,稳定的服务,这时候再考虑有替换供应商的风险,再做一些设计,是完全来得及,并且是收益最高的时候。