Golang 设计模式:工厂模式

Golang 设计模式:工厂模式

tk_sky 180 2024-02-24

Golang 设计模式:工厂模式

背景

工厂模式也是一种经典的设计模式。特别的,对于 golang 来说,因为没有针对类的构造方法去定义一个统一的规范,如果每次创建一个类的实例时,都需要在业务代码里去执行一遍初始化的细节,就可能导致业务方法与组件类产生非常高的耦合度。如果组件类的定义有改变,那么业务代码都得跟着改。

如果我们能在业务代码和类之间添加一层中间层,使用一种工厂类,就可以避免这样的问题,收获如下好处:

  • 实现类和业务代码不再耦合,如果类的构造改变,可以直接在工厂类处理,为业务代码屏蔽细节
  • 如果多个类都聚拢在工厂类构造,就实现了一种天然的切片,方便放一些公共逻辑

工厂模式一般分为简单工厂模式、工厂方法模式和抽象工厂模式三种。

简单工厂模式

我们定义一种水果接口,有 eat 一种方法:

type Fruit interface {
    Eat()
}

接下来定义 Orange Strawberry Cherry 三种水果实现类,代码省略。

最后,我们定义水果工厂类 FruitFactory:

type FruitFactory struct {
}

func NewFruitFactory() *FruitFactory {
    return &FruitFactory{}
}

func (f *FruitFactory) CreateFruit(typ string) (Fruit, error) {
    src := rand.NewSource(time.Now().UnixNano())
    rander := rand.New(src)
    name := strconv.Itoa(rander.Int())

    switch typ {
    case "orange":
        return NewOrange(name), nil
    case "strawberry":
        return NewStrawberry(name), nil
    case "cherry":
        return NewCherry(name), nil
    default:
        return nil, fmt.Errorf("fruit typ: %s is not supported yet", typ)
    }
}

在工厂类中,CreateFruit 方法是用于生产水果的方法:

  • 三种水果统合到一起,形成了公共切面,于是可以用来给水果取随机的数值来统一命名;
  • 根据使用方传进来的 type 去调用对应水果的构造方法
  • 如果 type 非法,对外抛出错误

可以看出这种工厂模式代码看起来简单明了、方便使用,但是也可以看出这种方法不太利于类的扩展:

  • 如果要加新水果,需要手动对创建水果的方法新增 switch case 分支
  • 如果支持的水果很多,switch case 可能出现复杂度问题

我们可以使用表驱动法稍微解决一下第二个问题:

  • 把水果构造函数定义成一个方法 fruitCreator
  • 在 FruitFactory 增加一个 map,根据水果类型映射到构造函数
  • 在 FruitFactory 的构造函数里初始化这个 map
type fruitCreator func(name string) Fruit

type FruitFactory struct {
    creators map[string]fruitCreator
}

func NewFruitFactory() *FruitFactory {
    return &FruitFactory{
        creators: map[string]fruitCreator{
            "orange":     NewOrange,
            "strawberry": NewStrawberry,
            "cherry":     NewCherry,
        },
    }
}

func (f *FruitFactory) CreateFruit(typ string) (Fruit, error) {
    fruitCreator, ok := f.creators[typ]
    if !ok {
        return nil, fmt.Errorf("fruit typ: %s is not supported yet", typ)
    }

    src := rand.NewSource(time.Now().UnixNano())
    rander := rand.New(src)
    name := strconv.Itoa(rander.Int())
    return fruitCreator(name), nil
}

简单工厂模式的使用方法:

func Test_factory(t *testing.T) {
    // 构造工厂
    fruitFactory := NewFruitFactory()

    orange, _ := fruitFactory.CreateFruit("orange")
    orange.Eat()

    cherry, _ := fruitFactory.CreateFruit("cherry")
    cherry.Eat()

    watermelon, err := fruitFactory.CreateFruit("watermelon")
    if err != nil {
        t.Error(err)
        return
    }
    watermelon.Eat()
}

总结而言,简单工厂模式:

  • 需要对要构造的类根据其共性定义一个公共 interface
  • 工厂类在使用时是个具体的实例
  • 对构造类聚拢、提供公共切面的效果最好
  • 在组件类扩展时需要直接修改工厂的组件构造方法

工厂方法模式

使用工厂方法模式,我们对上面的例子做修改:

  • 将工厂类 FruitFactory 由实现类改成 interface
  • 针对每类水果,提供一个具体的工厂实现类,如 OrangeFactory
  • 水果的定义模式不变,还是一个抽象的水果 interface,多个实现类
type FruitFactory interface {
    CreateFruit() Fruit
}

type OrangeFactory struct {
}

func NewOrangeFactory() FruitFactory {
    return &OrangeFactory{}
}

func (o *OrangeFactory) CreateFruit() Fruit {
    return NewOrange("")
}

// ... 其他水果

在工厂方法模式下,后续如果有频繁的扩展水果的需求,也不用对老代码做修改,只用新增一个水果的实现类和水果工厂实现类即可。

工厂方法模式解决了扩展水果类的问题,但是也有其缺陷,需要为每个水果都实现一个工厂类,冗余度较高,且公共切面没有了,通用逻辑没法一步到位。

抽象工厂模式

抽象工厂模式是最复杂的一种模式。要使用这种模式,我们需要先对组件进行拆解:

  • 我们把种类相对固定,把不需要频繁变更的纬度定义为产品等级
  • 对种类需要频繁变更的纬度,我们把他定义为产品族
  • 每次需要扩展产品族时,都需要实现对于产品族的工厂实现类,而不对老的实现方法直接修改
  • 针对不频繁变动的产品等级,每个产品族都会有一个具体的工厂实现类,其中会统一声明对应每种产品等级的构造方法,从而具备实现公共切面的能力。

例如,我们定义水果有两种:apple 和 orange,但这两种水果可以由各种不同的人种出来,可以有 TkApple、YewpoApple等。此时产品等级就为水果的种类,它不需要扩展;水果的种植者就需要灵活扩展,是产品族。

我们为每个产品等级(每种水果)定义一个 interface,包含不同的方法:

type Apple interface {
    MakePie()
}

type Orange interface {
    MakeJuice()
}

定义水果工厂接口,其中声明了生产 apple 和 orange 的方法:

type FruitFactory interface {
    CreateApple() Apple
    CreateOrange() Orange
}

然后再针对每种水果类型(产品等级),进行不同种植者(产品族)下的具体实现:

type TkApple struct{
    farmer string
    Apple
}

func (t *TkApple) MakePie(){
    print("making pie by " + farmer)
}

type TkOrange struct{
    farmer string
    Orange
}

func (t *TkOrange) MakeJuice(){
    print("making juice by " + farmer)
}

type YewpoApple struct{
    farmer string
    Apple
}

func (y *YewpoApple) MakePie(){
    print("making pie by " + farmer)
}
// ...

针对每个种植者,声明一个工厂实现类:

type TkFactory struct{}

func (t *TkFactory) myAspect(){
    print("tk's aspect")
}

func (t *TkFactory) CreateApple() Apple{
    // 每个产品族可以插入自己的切面
    t.myAspect()
    return &TkApple{
        farmer: "tk",
    }
}
func (t *TkFactory) CreateOrange() Orange{
    // 每个产品族可以插入自己的切面
    t.myAspect()
    return &TkOrange{
        farmer: "tk",
    }
}
// ...

这时如果我们需要额外扩展一个新的种植者 lumine,需要新增下面的代码:

type LumineApple struct{
    farmer string
    Apple
}

func (l *LumineApple) MakePie(){
    print("making pie by " + farmer)
}

type LumineOrange struct{
    farmer string
    Orange
}

func (l *LumineOrange) MakeJuice(){
    print("making juice by " + farmer)
}

type LumineFactory struct{}

func (l *LumineFactory) myAspect(){
    print("lumine's aspect")
}

func (l *LumineFactory) CreateApple() Apple{
    // 每个产品族可以插入自己的切面
    l.myAspect()
    return &LumineApple{
        farmer: "lumine",
    }
}
func (l *LumineFactory) CreateOrange() Orange{
    // 每个产品族可以插入自己的切面
    l.myAspect()
    return &LumineOrange{
        farmer: "lumine",
    }
}

这样就完成了对产品族的非侵入式扩展。

总结来说,抽象工厂模式主要是将组件拆分为产品族和产品等级的纬度,根据需要频繁扩展和不需要扩展的纬度进行拆分,使之能够同时具有简单工厂模式和工厂方法模式的优势。

在使用抽象工厂模式时要特别注意明确产品族和产品等级的纬度定义,因为在这个模式里要扩展产品等级是非常困难的。

DI容器(容器工厂)模式

另外一种工厂模式的实现方式是容器工厂,它的思路是把工厂改造为一个组件交易的市场,不再把每个组件具体的构造工作交给工厂完成,而是让组件的提供方通过工厂的提供的入口完成组件的注入,然后使用方通过工厂提供的统一出口去进行组件的获取。

实际上,这种模式就是一种依赖注入的实现方式:

  • factory 对外暴露 inject 和 invoke 方法,作为注入组件的入口方法和出口方法;
  • 实现一个工厂类的单例对象,组件提供方通过 GetFactory 方法获取到工厂单例;
  • 组件提供方自己创建好组件,调用 factory.Inject 方法将其注入到容器工厂(或者注入组件的构造方法);
  • 使用方获取工厂单例,并通过 invoke 方法,拿到对应的组件。

总结

一段话总结:

工厂模式是一种经典的设计模式,通过在组件类和使用方之间添加一个工厂类中间层来实现代码的解耦,同时为组件的构造流程提供了公共切面。简单工厂模式提供了简单直观的切面效果,但扩展时具有侵入性;工厂方法模式通过一个组件类对应一个工厂类,避免了扩展时的侵入性,但存在一定的代码冗余,且削弱了公共切面;抽象工厂模式通过扩展需求将组件拆分为产品族和产品等级两个维度,可以同时保证产品族的灵活扩展和产品等级的切面能力。此外,通过引入依赖注入框架,还可以实现容器工厂模式的依赖注入功能。