Go语言设计模式(2)工厂模式

Go语言设计模式(2)工厂模式

工厂模式的定义

工厂模式的定义如下:

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

——《设计模式之禅》

工厂模式在Go语言中还是很常见的。因为Go语言的结构体没有构造器,而工厂模式本质上就是new一个对象的替代品,所以在项目中一般都需要使用简单工厂模式去实例化一个结构体已达到与构造器相同的目的(即我们经常见到的NewXxx()方法,关于什么是简单工厂模式我们马上就会讲到);另外在DDD(领域驱动设计)中也经常可以看到工厂模式的身影。

工厂模式的简单例子

我们使用Go语言重写《设计模式之禅》中给出的女娲造人的例子:

factory/human_factory.go

package factory

import "fmt"

// Human 人类总称
type Human interface {
    GetColor() // 每个人种的皮肤都有相应的颜色
    Talk()     // 人类会说话
}

// BlackHuman 黑色人种
type BlackHuman struct{}

func (b *BlackHuman) GetColor() {
    fmt.Println("黑色人种的皮肤颜色是黑色的!")
}

func (b *BlackHuman) Talk() {
    fmt.Println("黑人会说话,一般人听不懂.")
}

// YellowHuman 黄色人种
type YellowHuman struct{}

func (y *YellowHuman) GetColor() {
    fmt.Println("黄色人种的皮肤颜色是黄色的!")
}

func (y *YellowHuman) Talk() {
    fmt.Println("黄色人种会说话,一般说的都是双字节.")
}

// WhiteHuman 白色人种
type WhiteHuman struct{}

func (w *WhiteHuman) GetColor() {
    fmt.Println("白色人种的皮肤颜色是白色的!")
}

func (w *WhiteHuman) Talk()  {
    fmt.Println("白色人种会说话,一般都是单字节.")
}

// AbstractHumanFactory 抽象人类创建工厂
type AbstractHumanFactory interface {
    CreateHuman(t string) Human
}

// HumanFactory 人类创建工厂
type HumanFactory struct {}

// CreateHuman 产生一个人种
func (h *HumanFactory) CreateHuman(t string) Human {
    switch t {
    case "b":
        return &BlackHuman{}
    case "y":
        return &YellowHuman{}
    case "w":
        return &WhiteHuman{}
    default:
        fmt.Println("人种生成错误!")
        return nil
    }
}

factory/human_factory_test.go

package factory

import (
    "fmt"
    "testing"
)

func TestHumanFactory_CreateHuman(t *testing.T) {
    // 声明阴阳八卦炉
    yinYangLu := &HumanFactory{}
    // 女娲第一次造人,火候不足,于是白人产生了
    fmt.Println("--造出的第一批人是白色人种--")
    wh := yinYangLu.CreateHuman("w")
    wh.GetColor()
    wh.Talk()
    // 女娲第二次造人,火候过足,于是黑人产生了
    fmt.Println("--造出的第二批人是黑色人种--")
    bh := yinYangLu.CreateHuman("b")
    bh.GetColor()
    bh.Talk()
    // 第三次造人,火候刚刚好,于是黄色人种产生了
    fmt.Println("--造出的第三批人是黄色人种--")
    yh := yinYangLu.CreateHuman("y")
    yh.GetColor()
    yh.Talk()
}

运行结果如下:

=== RUN   TestHumanFactory_CreateHuman
--造出的第一批人是白色人种--
白色人种的皮肤颜色是白色的!
白色人种会说话,一般都是单字节.
--造出的第二批人是黑色人种--
黑色人种的皮肤颜色是黑色的!
黑人会说话,一般人听不懂.
--造出的第三批人是黄色人种--
黄色人种的皮肤颜色是黄色的!
黄色人种会说话,一般说的都是双字节.
--- PASS: TestHumanFactory_CreateHuman (0.00s)
PASS

与Java版本的代码类似,Go语言版本的代码也非常简单,有过Go语言项目开发经历的话应该不难理解。

工厂模式的通用代码

factory/factory.go

package factory

import "fmt"

// Product 抽象产品类
type Product interface {
    // Method 抽象方法
    Method()
}

// ConcreteProduct1 具体产品类1
type ConcreteProduct1 struct{}

func (c *ConcreteProduct1) Method() {
    fmt.Println("我是具体产品类1的业务逻辑")
}

// ConcreteProduct2 具体产品类2
type ConcreteProduct2 struct{}

func (c *ConcreteProduct2) Method() {
    fmt.Println("我是具体产品类2的业务逻辑")
}

// Creator 抽象工厂类
// 创建一个产品对象,其输入参数类型可以自行设置,这里用了string
type Creator interface {
    CreateProduct(t string) Product
}

// ConcreteCreator 具体工厂类
type ConcreteCreator struct{}

func (c *ConcreteCreator) CreateProduct(t string) Product {
    switch t {
    case "1":
        return &ConcreteProduct1{}
    case "2":
        return &ConcreteProduct2{}
    default:
        // 异常处理
        return nil
    }
}

实际使用效果如下:

factory/factory_test.go

package factory

import "testing"

func TestConcreteCreator_CreateProduct(t *testing.T) {
    c := &ConcreteCreator{}
    c1 := c.CreateProduct("1")
    c1.Method()
    c2 := c.CreateProduct("2")
    c2.Method()
}

运行结果:

=== RUN   TestConcreteCreator_CreateProduct
我是具体产品类1的业务逻辑
我是具体产品类2的业务逻辑
--- PASS: TestConcreteCreator_CreateProduct (0.00s)
PASS

工厂模式的扩展

除了前面提到的工厂模式之外,工厂模式还有以下扩展:

简单工厂模式

如果只有一个工厂类,就可以将抽象工厂类去掉,只保留实现类,此时我们就可以发现我们没有必要将工厂类实例化,那么我们就将工厂类的方法设置为静态(在Go语言中就是改成不属于任何结构体的函数)。

这就是简单工厂模式,它是工厂模式的一种弱化,虽然不便于扩展但是很常用,例如我们在项目中用来实例化结构体的NewXxx()函数就是简单工厂模式在Go语言中最简单也最常见应用。

还是以女娲造人为例,使用简单工厂模式实现的代码如下:

factory/simple_factory.go

package factory

import "fmt"

func CreateHuman(t string) Human {
    switch t {
    case "b":
        return &BlackHuman{}
    case "y":
        return &YellowHuman{}
    case "w":
        return &WhiteHuman{}
    default:
        fmt.Println("人种生成错误!")
        return nil
    }
}

对工厂类的改动同时也引起了调用方的改动——不需要将工厂类实例化了,直接调用CreateHuman()即可。

factory/simple_factory_test.go

package factory

import (
    "fmt"
    "testing"
)

func TestCreateHuman(t *testing.T) {
    // 女娲第一次造人,火候不足,于是白人产生了
    fmt.Println("--造出的第一批人是白色人种--")
    wh := CreateHuman("w")
    wh.GetColor()
    wh.Talk()
    // 女娲第二次造人,火候过足,于是黑人产生了
    fmt.Println("--造出的第二批人是黑色人种--")
    bh := CreateHuman("b")
    bh.GetColor()
    bh.Talk()
    // 第三次造人,火候刚刚好,于是黄色人种产生了
    fmt.Println("--造出的第三批人是黄色人种--")
    yh := CreateHuman("y")
    yh.GetColor()
    yh.Talk()
}

运行结果:

=== RUN   TestCreateHuman--造出的第一批人是白色人种--白色人种的皮肤颜色是白色的!白色人种会说话,一般都是单字节.--造出的第二批人是黑色人种--黑色人种的皮肤颜色是黑色的!黑人会说话,一般人听不懂.--造出的第三批人是黄色人种--黄色人种的皮肤颜色是黄色的!黄色人种会说话,一般说的都是双字节.--- PASS: TestCreateHuman (0.00s)PASS

多工厂类的工厂模式

随着项目的不断扩展,实例化一个对象可能非常耗费精力,或者工厂类需要实例化很多种不同的对象,这时工厂方法就会变得很长,导致难以阅读。我们可以让工厂类也继承于一个基类,每一个产品类对应一个工厂类,这时工厂类的职责也就变得更加明确了:生产与自己关联的产品。虽然这带来了维护和扩展上的挑战——因为需要同时维护产品类及对应于该产品类的工厂类,但是这样的设计也降低了工厂类的复杂度。

以女娲造人为例,多个工厂类的工厂模式代码如下:

factory/multiple_factory.go

package factoryimport "fmt"// Human1 人类总称type Human1 interface {    GetColor() // 每个人种的皮肤都有相应的颜色    Talk()     // 人类会说话}// BlackHuman1 黑色人种type BlackHuman1 struct{}func (b *BlackHuman1) GetColor() {  fmt.Println("黑色人种的皮肤颜色是黑色的!")}func (b *BlackHuman1) Talk() {    fmt.Println("黑人会说话,一般人听不懂.")}// YellowHuman1 黄色人种type YellowHuman1 struct{}func (y *YellowHuman1) GetColor() {  fmt.Println("黄色人种的皮肤颜色是黄色的!")}func (y *YellowHuman1) Talk() {   fmt.Println("黄色人种会说话,一般说的都是双字节.")}// WhiteHuman 白色人种type WhiteHuman1 struct{}func (w *WhiteHuman1) GetColor() { fmt.Println("白色人种的皮肤颜色是白色的!")}func (w *WhiteHuman1) Talk()  {   fmt.Println("白色人种会说话,一般都是单字节.")}// AbstractHumanFactory1 抽象人类创建工厂type AbstractHumanFactory1 interface { // CreateHuman 产生一个人种   // 注意,由于每个子类都有与其自身关联的产品类,所以这里已经不需要通过传递参数来指明创建哪种产品了  CreateHuman() Human}// BlackHumanFactory 黑色人种创建工厂type BlackHumanFactory struct {}// CreateHuman 产生一个黑色人种func (h *BlackHumanFactory) CreateHuman() Human {   return &BlackHuman{}}// YellowHumanFactory 黄色人种创建工厂type YellowHumanFactory struct {}// CreateHuman 产生一个黄色人种func (h *YellowHumanFactory) CreateHuman() Human {   return &YellowHuman{}}// WhiteHumanFactory 白色人种创建工厂type WhiteHumanFactory struct {}// CreateHuman 产生一个白色人种func (h *WhiteHumanFactory) CreateHuman() Human { return &WhiteHuman{}}

调用方也需要一定的更改——在实例化每种产品之前先实例化对应的工厂:

factory/multiple_factory_test.go

package factoryimport ( "fmt"   "testing")func TestCreateHuman1(t *testing.T) { // 女娲第一次造人,火候不足,于是白人产生了 fmt.Println("--造出的第一批人是白色人种--") whf := WhiteHumanFactory{}  wh := whf.CreateHuman() wh.GetColor()   wh.Talk()   // 女娲第二次造人,火候过足,于是黑人产生了 fmt.Println("--造出的第二批人是黑色人种--") bhf := BlackHumanFactory{}  bh := bhf.CreateHuman() bh.GetColor()   bh.Talk()   // 第三次造人,火候刚刚好,于是黄色人种产生了    fmt.Println("--造出的第三批人是黄色人种--") yhf := YellowHumanFactory{} yh := yhf.CreateHuman() yh.GetColor()   yh.Talk()}

运行结果:

=== RUN   TestCreateHuman1--造出的第一批人是白色人种--白色人种的皮肤颜色是白色的!白色人种会说话,一般都是单字节.--造出的第二批人是黑色人种--黑色人种的皮肤颜色是黑色的!黑人会说话,一般人听不懂.--造出的第三批人是黄色人种--黄色人种的皮肤颜色是黄色的!黄色人种会说话,一般说的都是双字节.--- PASS: TestCreateHuman1 (0.00s)PASS

总结

个人认为工厂模式是在Go语言中应用最广泛的一种设计模式——因为Go语言的结构体没有构造器,所以在大型项目中或遇到创建结构体的过程特别复杂的场景时,需要使用简单工厂模式来帮助我们创建结构体。另外,在领域驱动设计(DDD,Domain Driven Design)中,创建聚合根等场景中也用到了工厂模式,尤其是当创建聚合根的逻辑不适合放在聚合根上时。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容