复合模式-Go

结构模式通过常用的结构和关系帮我们塑造应用,在Go语言中最常用的模式是复合模式,因为在Go语言中没有继承的概念,而鼓励复合的使用

复合模式

复合设计模式倾向于组合(通常定义为具有关系)而不是继承(即关系)。自90年代以来,组合继承方法一直是工程师们讨论的话题。我们将学习如何使用has a方法创建对象结构。总之,Go没有继承权,因为它不需要继承权!

描述

在复合设计模式中,您将创建对象的层次结构和树。对象有不同的对象,其中有自己的字段和方法。这种方法非常强大,解决了许多继承和多重继承的问题。例如,一个典型的继承问题是当一个实体从两个完全不同的类继承时,这两个类之间完全没有关系。想象一下训练的运动员和游泳的游泳运动员。

  • Athlete类有Train()方法
  • Swimmer类有Swim()方法

Swimmer类继承自Athlete,因此也继承了Train方法并且声明了自己的Swim方法。同时也可以有一个自行车运动员Cyclist声明一个Ride方法。但是如果现在有一个会吃东西的动物,像狗会叫一样:

  • CyclistRide方法
  • Animal类有EatBark方法

你也可以有一个鱼,当然可以游泳,但是如何解决呢?鱼不可以像运动员一样训练。你可以创建一个运动员接口,拥有一个swim方法,让游泳运动员和鱼实现它。这应该是最好的实现了,但你必须实现swim方法两次,代码重用性则会受到影响。如果是铁人三项呢?运动员需要游泳跑步和骑车。多重的接口实现可以暂时解决这个问题,但很快就没法维护了。

目标

复合模式就是为了避免这种会导致应用臃肿、代码不清洁的层级地狱。我们可以使用Go实现两种复合模式:直接复合direct composition和嵌入复合embedding composition

示例

复合模式是一种纯纯结构模式,对于结构本身没什么可测试的。因此这次不会写单元测试,在此简单的描述复合模式的创建。

首先从Athlete结构和Train()方法开始

type Athlete struct{}

func (a *Athlete) Train() {
    fmt.Println("Training)
}

上述代码很直接,Train()方法打印字符串,现在再创建一个游泳运动员A,将Athlete组合在里面。

type CompositeSwimmerA struct {
    MyAthlete Athlete
    MySwim func()
}

组合类型CompositeSwimmerA有一个Athlete类型的数据域MyAthlete,同时还有一个func()。在Go中,函数是“一等公民”,他们可以像任何变量一样用作参数、字段。因此CompositeSwimmerA有一个MySwim字段,该字段存储一个闭包,不接受任何参数也不返回任何内容。我们怎么才能为他分配函数呢?我们创建一个与func()类型匹配的函数(没有参数, 没有返回值)。

func Swim(){
    fmt.Println("Swimming!")
}

打完收工。Swim()方法没有参数没有返回值,因此可以用在CompositeSwimmerA结构体的MySwim字段上:

swimmer := CompositeSwimmerA{ 
    MySwim: Swim,
}
swimmer.MyAthlete.Train() 
swimmer.MySwim()

现在有一个叫Swim()的函数,我们将该函数分配到MySwim字段上。注意,Swim类型没有将执行其内容的括号。这样我们把整个函数复制到MySwim方法。此时我们没有传递任何运动员到MyAthlete字段但是已经可以使用使用了。

$ go run main.go
Training
Swimming!

其实这是由于Go语言的自动初始化。如果你不给CompositeSwimmerA传递一个Athlete结构体,编译器会为他自动创建一个,并赋字段初值为0。再看看上面对CompositeSwimmerA的定义,再来解决鱼的问题就不难了。首先我们创建一个Animal结构体:

type Animal struct{}

func (a *Animal) Eat () {
    fmt.Println("Eating")
}

再创建一个Shark对象嵌入一个Animal对象:

type Shark struct {
    Animal
    Swim func()
}

等下,Animal类型字段的名字去哪了?还记得嵌入这个词吗?在Go里,你可以把对象嵌入对象里,看起来就像是继承。也就是说,我们不必显式调用字段名来访问其字段和方法,因为它们将是我们的一部分。这样说的话下面的代码就很ok了:

fish := Shark{
    Swim: Swim,
}
fish.Eat()
fish.Swim()

现在我们有了一个有初始值的、嵌入的Animal类型。这就是为什么可以在不创建或者使用内部字段名的情况下调用Eat()方法。

还有第三个方法使用复合模式。我们可以创建一个Swimmer接口,定义一个Swim()方法和一个SwimmerImpl类型嵌入到游泳运动员中。

type Swimmer interface {
    Swim()
}

type Trainer interface {
    Train()
}

type SwimmerImpl struct {}
func (s *SwimmerImpl) Swim() {
    fmt.Println("Swimming!")
}

type CompositeSwimmerB struct {
    Trainer
    Swimmer
}

用这种方法可以更明确的控制对象的创建。Swimmer字段是嵌入的,但是不会被自动初始化,因为它指向一个接口。正确的使用方法是:

swimmer := CompositeSwimmerB {
    &Athlete{},
    &SwimmerImpl{},
}

swimmer.Train()
swimmer.Swim()

哪种方法更妙?个人来讲,接口方法是最好的。首先,面向接口编程、其次,你不会把代码的一部分留给编译器的零初始化特性。这是一个非常强大的特性,但必须小心使用,因为它会导致运行时问题,在编译时使用接口时会发现这些问题。实际上,在不同的情况下,零初始化将在运行时为您节省时间!但我更喜欢尽可能多地使用接口,所以这实际上不是一个选项。

二叉树复合(Binary Tree Compositions)

另一种常见的符合模式是在模拟二叉树时,需要在字段中存储本身类型(左右孩子):

type Tree struct {
    LeafValue int
    Right *Tree
    left *Tree
}

这是一种递归复合,并且由于递归的特性,我们必须用指针才能让编译器知道需要留多少内存。Tree这个结构体为每个实例存储了LeafValue对象,还有LeftRight中的两个新Tree

我们可以这样创建一个对象:

root := Tree{
    LeafValue: 0,
    Right: &Tree{
        LeafValue: 5,
        Right: &Tree{
            6, nil, nil,
        }
        Left: nil,
    },
    Left: &Tree{
        4, nil, nil,
    }
}
//          0
//        /   \
//       4     5
//      / \   / \
//     n   n n   6
//              / \
//             n   n

复合模式和继承

我们在Go中使用组合设计模式时,必须明确与继承区分开。例如当我们把Parent结构嵌入到Son结构中:

type Parent struct { 
    SomeField int
}
type Son struct { 
    Parent
}

我们不能把Son当做一个Parent,也就是说当一个函数需要一个Parent实例时不能把Son传递给他:

func GetParentField(p *Parent) int {
    fmt.Println(p.SomeField)
}

如果试图传递一个Son实例给GetParentField,就会得到一个错误信息

cannot use son (type Son) as type Parent in argument to GetParentField

如果非要这样的话,可以吧Parent作为一个字段而不是嵌入进Son中去:

type Son struct {
    P Parent
}

现在可以通过P这个字段传递ParentGetParentField了:

son := Son{}
GetParentField(son.P)

源码见 https://github.com/ricardoliu404/go-design-patterns/tree/master/structural/composite

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值