51

[译] Go 语言中的组合

 5 years ago
source link: https://mp.weixin.qq.com/s/-KaLnZTimnKH7q2H_MdkfQ?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go语言的组合特性构筑在类型嵌入之上。这种范式让我们可以设计更好的API以及从较小的部分构建更大的程序。这一切都从单一目的类型的声明和实现开始。按照组合的思路来构建的程序更易扩展以及适应不断变化的需求,当然也更容易被阅读及理解。

为了证明前面所说这些概念,我们需要回顾下下面的程序:

示例代码(

https://github.com/ardanlabs/gotraining/blob/c081f15e59fbe895c50b25a8a2d2eaf7a5772cbc/topics/composition/example4/example4.go

)

这个代码示例展示了类型嵌入技术,并为我们提供了讨论如何通过组合来设计兼顾灵活性和可读性代码的机会。每个可以从包中导出的标识符都是这个包的API。这些导出的标识符包括常量、变量、类型、方法和函数。注释在每个包的API中是通常被忽略的部分,所以与包的用户的沟通时往往很清晰和简单。

这个例子很长,让我们把它分解成碎片进行分析。

这段程序背后的思想是我们雇佣了承包商去翻修我们的房子。特别的是,房子里有些木板已经腐烂,这些腐烂的板子需要被移除,同时需要钉牢新的板子。承包商负责提供钉子、木板和工具来完成这项工作。

//清单1

//Board represents a surface we can work on.

type Board struct {

NailsNeeded int

NailsDriven int

}

在清单1中,我们声明了Board类型,一个Board 有两个域,木板所需的钉子数量以及目前已钉进木板的钉子数量。现在,让我们看一下声明的接口:

清单2

// NailDriver represents behavior to drive nails into a board.

type NailDriver interface {

DriveNail(nailSupply *int, b *Board)

}

// NailPuller represents behavior to remove nails into a board.

type NailPuller interface {

PullNail(nailSupply *int, b *Board)

}

// NailDrivePuller represents behavior to drive/remove nails into a board.

type NailDrivePuller interface {

NailDriver

NailPuller

}

清单2展示了一个接口,这个接口声明了承包商使用这些工具的行为。第22行NailDriver接口声明了把钉子钉到木板中的行为。这个方法中提供了一些钉子和可以将钉子钉进去的木板。第27行的NailPuller接口声明了相反的行为。这个方法也是给定了一些钉子和木板,但钉子将被从木板中拔出放回到供给中。

NailDriver和 NailPuller这两个接口都实现了单一明确的行为。这就是我们想要的能够将行为分解成独立且简单的行为,正如你所看到的,这样的分解有助于实现程序本身的组合性、灵活性及可读性。

第32行声明了最后的接口,NailDrivePuller接口:

make函数会分配和初始化一个哈希映射数据结构,并返回一个指向该哈希映射的映射变量。相关数据结构细节由运行时实现,并且不因语言本身所规定。本文中我们将关注映射的使用,而不是他们的实现。

//清单3

type NailDrivePuller interface {

NailDriver

NailPuller

}

这个接口由NailDriver和NailPuller两个接口组成。通过组合现有的接口以实现组合行为,你可以发现这种模式在Go语言中是非常普遍的。稍后你将看到它是如何在代码中实现的。现在,任何具体类型的值在实现钉进去和拔出来的行为时,都会实现NailDrivePuller的接口。

有了行为的定义,是时候声明和实现一些工具了:

清单4:

// Mallet is a tool that pounds in nails.

type Mallet struct{}

// DriveNail pounds a nail into the specified board.

func (Mallet) DriveNail(nailSupply *int, b *Board) {

// Take a nail out of the supply.

*nailSupply–

//Pound a nail into the board.

b.NailsDriven++

fmt.Println("Mallet: pounded nail into the board.")

}

在清单4的第40行,我们声明了结构体类型Mallet。这里声明了一个空的结构体,是因为并不需要维持状态,只需要实现这个行为。

Mallet是使用锤子钉钉子的工具。所以在43行,NailDriver接口通过声明DriveNail方法得以实现。这种实现方法是不相关的,从提供的钉子中减去一个,在木板中增加一个。

让我们看一下声明和实现的第二个工具:

// 清单5:

// Crowbar is a tool that removes nails.

type Crowbar struct{}

// PullNail yanks a nail out of the specified board.

func (Crowbar) PullNail(nailSupply *int, b *Board) {

// Yank a nail out of the board.

b.NailsDriven–

// Put that nail back into the supply.

*nailSupply++

fmt.Println("Crowbar: yanked nail out of the board.")

}

在清单5中,在54行声明了我们的第二个工具。这个工具代表了撬棍,我们可以使用它将钉子从木板中拔出。NailPuller接口通过声明PullNail方法得以实现。这个实现同样也是不相关的,但你可以知道钉子是如何从木板中被拔出又放回到供应的钉子中。

这段代码的关键之处在于我们通过接口声明了工具的行为。通过一系列的结构体实现这种行为,结构体中带有明显特征的工具可以被承包商使用。现在让我们创建一个新的类型用来表示承包商如何使用这些工具来工作:

// 清单6:

// Contractor carries out the task of securing boards.

type Contractor struct{}

// Fasten will drive nails into a board.

func (Contractor) Fasten(d NailDriver, nailSupply *int, b *Board) {

for b.NailsDriven < b.NailsNeeded {

d.DriveNail(nailSupply, b)

}

}

在清单6的第70行,我们声明了一个Contractor类型。同样,我们不需要任何状态所以使用一个空的结构体。在第73行,我们看到的Fasten方法,这是针对Contractor类型声明的三种方法之一。

Fasten方法声明了承包商将一些钉子钉到具体的木板上的行为。这个方法需要通过第一个变量实现NailDriver接口。这个值代表了工具,承包商使用这个工具执行这种行为。使用接口类型的变量允许在后续中在API中增加或使用不同的工具,而无需改变API本身。使用者如何使用工具,而Fasten方法提供了何时以及如何使用这些工具的工作流。

请注意,我们没有使用NailDriverPull,而是使用了NailDriver作为变量的类型。这是很重要的,对于Fasten方法只需要一个行为,那就是钉一个钉子。通过声明我们需要的唯一变量,我们更容易理解我们的代码。同时代码由于最小的耦合也更容易使用。现在,让我们看一下Unfasten方法:

// 清单7:

// Unfasten will remove nails from a board.

func (Contractor) Unfasten(p NailPuller, nailSupply *int, b *Board) {

for b.NailsDriven > b.NailsNeeded {

p.PullNail(nailSupply, b)

}

`}

在清单7中声明的Unfasten方法给承包商提供了一个与Fasten相反的行为。这个方法将在指定的木板中移除钉子,并将这些移除的钉子放回到钉子供给处。这个方法只接受了一个工具,这个工具实现了NailPull接口。因为在这个方法中只实现了一种行为,所以我们想要什么也是很明确的。

承包商的最后一个行为方法叫做ProcessBoard,允许承包商在同一时间操作多个木板。

// 清单8:

// ProcessBoards works against boards.

func (c Contractor) ProcessBoards(dp NailDrivePuller, nailSupply *int, boards []Board) {

for i := range boards {

b := &boards[i]

fmt.Printf("contractor: examining board #%d: %+v\n", i+1, b)

switch {

case b.NailsDriven < b.NailsNeeded:

c.Fasten(dp, nailSupply, b)

case b.NailsDriven > b.NailsNeeded:

c.Unfasten(dp, nailSupply, b)

}

}

}

在清单8中,我们声明和实现了ProcessBoard方法。这个方法的第一个变量实现了NailDriver和NailPull两个接口。

// 清单9:

func (c Contractor) ProcessBoards(dp NailDrivePuller, nailSupply *int, boards []Board) {

清单9展示的是方法如何声明一个存在的行为,它需要通过NailDrivePull接口声明。我们想要API仅实现需要或使用的指定行为。Fasten和Unfasten的值只需要一个行为的动作,而ProcessBoard的值需要同时实现两个行为。

// 清单10:

switch {

case b.NailsDriven < b.NailsNeeded:

c.Fasten(dp, nailSupply, b)

case b.NailsDriven > b.NailsNeeded:

c.Unfasten(dp, nailSupply, b)

}

清单10展示的是ProcessBoard的一系列实现以及接口类型NailDrivePuller是如何调用Fasten和Unfasten方法的。当调用第59行的Fasten方法时,NailDrivePuller的接口值作为NailDriver接口值被传递到Fasten方法中。让我们再看一下Fasten方法的声明:

// 清单11:

func (Contractor) Fasten(d NailDriver, nailSupply *int, b *Board) {

注意Fasten方法需要一个NailDriver接口类型的值,我们传递一个NailDrivePuller接口类型的值。这种方式是可以的,因为编译器知道任意具体类型值可以被存储到NailDrivePuller接口类型的值中,必须也实现了NailDriver接口。

同样在调用Unfasten函数也是对的:

// 清单12:

func (Contractor) Unfasten(p NailPuller, nailSupply *int, b *Board) {

编译器知道所有具体类型的值,被存储到NailDrivePuller的值也同样实现了NailPuller接口类型。因此一个NailDrivePuller接口类型的值被指定给NailPuller接口。因为两个接口之间是静态关系,编译器可以放弃生成一个运行时类型的断言,用一个接口转换来代替。

有了承包商,现在我们声明一个新的类型表示承包商使用的工具箱:

// 清单13:

// Toolbox can contains any number of tools.

type Toolbox struct {

NailDriver

NailPuller

nails int

}

每一个好的承包商都应该有个工具箱,我们在清单13中声明了一个工具箱。结构体类型的Toolbox在107行嵌入一个NailDriver类型的接口值,在108行嵌入一个NailPuller类型的接口值,然后在110行通过Nails域声明一些钉子。

当把一个类型嵌入到另一个类型中时,值得思考的是新的类型作为外部类型,同时将嵌入类型作为内部类型。这非常重要因为你们可以看到嵌入类型创建的关系。

被嵌入的任何类型总是作为一个内部值存在,在外部类型值中,它永远不会丢失自己的身份,并且永远存在。感谢内部类型的提升,任何内部类型的声明都被提升至外部类型。这意味着基于导出规则,我们可以通过外部类型值访问任何与内部类型相关的域和或方法。

让我们看一个例子:

// 清单14:

package main

import "fmt"

// user defines a user in the program.

type user struct {

name string

email string

}

// notify implements a method that can be called via

// a pointer of type user.

func (u *user) notify() {

fmt.Printf("Sending user email To %s<%s>\n",

u.name,

u.email)

}

在清单14中,我们声明了一个user类型,这个类型包含2个string域和一个notify的方法,现在让我们把这个类型嵌入到另一个类型中:

// 清单15:

// admin represents an admin user with privileges.

type admin struct {

user // Embedded Type

level string

}

现在我们可以从清单15中看出内部类型和外部类型的关系。user类现在是外部类型admin的内部类型。。这意味着由于内部类型的作用,可以直接从admin类型的值中调用notify方法。但由于内部类型存在于自身,我们也可以直接从内部类型值调用notify方法。

// 清单16:

// main is the entry point for the application.

func main() {

// Create an admin user.

ad := admin{

user: user{

name: "john smith",

email: "[email protected]",

},

level: "super",

}

// We can access the inner type’s method directly.

ad.user.notify()

// The inner type’s method is promoted to the outer type.

ad.notify()

}

清单16中在28行中展示了admin的值是如何通过结构体字面量创建的。结构体字面量中的第二个结构体字面量用于创建和初始化一个内部类型user的值。有了admin类型的值,我们可以从37行的内部类型值或从40行的外部类型值中直接调用notify方法。

最后一件事,组合既不是子类型也不是子类。admin类型的值不能被用于user类型的值,admin类型的值只能作为admin类型的值,同样user类型的值只能作为user类型的值。由于内部类型的作用,内部类型的域和方法可以被外部类型的值直接访问。

现在让我们回到toolbox:

// 清单17:

// Toolbox can contains any number of tools.

type Toolbox struct {

NailDriver

NailPuller

nails int

}

我们没有在Toolbox中内嵌一个结构体类型,而是内嵌了两个接口类型。这意味着任何实现NailDriver接口的具体类型值都会以NailDriver内部类型的值内嵌到接口中。同样适用于内嵌到NailPuller接口类型。

一旦指定了具体类型,Toolbox确保实施了这种行为。甚至,由于Toolbox内嵌了NailDriver和NailPuller两个接口类型,这意味着Toolbox也同样实现了NailDriverPuller接口。

// 清单18:

NailDrivePuller interface {

NailDriver

NailPuller

}

在清单18中,我们看到再一次声明了NailDrivePuller接口。内嵌接口类型采用内部类型作用的概念,接口符合下一级。

现在我们准备将一切放到一起,通过main函数实现:

// 清单19:

// main is the entry point for the application.

func main() {

// Inventory of old boards to remove, and the new boards

// that will replace them.

boards := []Board{

// Rotted boards to be removed.

{NailsDriven: 3},

{NailsDriven: 1},

{NailsDriven: 6},

// Fresh boards to be fastened.

{NailsNeeded: 6},

{NailsNeeded: 9},

{NailsNeeded: 4},

}

main函数开始于清单19的第116行。我们创建了一些木板,根据钉子明确每个木板需要什么。接下来我们创建工具箱:

// 清单20:

// Fill a toolbox.

tb := Toolbox{

NailDriver: Mallet{},

NailPuller: Crowbar{},

nails: 10,

}

// Display the current state of our toolbox and boards.

displayState(&tb, boards)

清单20中,我们创建了Toolbox值,并创建了Mallet结构体类型的值并把它分配给内部接口类型NailDriver,然后创建Crowbar结构体类型的值并把它分配给内部接口类型NailPuller。最后,我们添加10个钉子到工具箱中。

现在让我们创建一个承包商和处理一些木板:

// 清单21:

// Hire a contractor and put our contractor to work.

var c Contractor

c.ProcessBoards(&tb, &tb.nails, boards)

// Display the new state of our toolbox and boards.

displayState(&tb, boards)

}

单21中的第142行,我们创建了Contractor类型的值,在143行针对这个值调用了ProcessBoard方法。让我们再看一次ProcessBoards方法的声明:

// 清单22

func (c Contractor) ProcessBoards(dp NailDrivePuller, nailSupply *int, boards []Board) {

再一次我们看到ProcessBoard方法如何将它第一个变量作为任意类型的值,这个值实现了NailDrivePuller接口。由于在Toolbox结构体类型中内嵌了NailDriver 和 NailPuller接口类型,所以Toolbox类型的指针实现了NailDrivePuller接口。

注释:Mallet 和 Crowbar类型通过接收值实现了它们各自的接口,基于方法集的规则(http://www.goinggo.net/2014/05/methods-interfaces-and-embedded-types.html),值和指针都满足接口。这就是为什么我们能创建和赋值给具体类型的值,嵌入到接口类型的值。在清单21中的第143使用了Toolbox值的地址,因为我们想要复制一个Toolbox。但是,Toolbox类的值也满足接口。如果一个具体的类型使用指针接收器实现接口,那么只有指针可以满足接口。

最后,下面实现了displayState功能:

// 清单23

// displayState provide information about all the boards.

func displayState(tb *Toolbox, boards []Board) {

fmt.Printf("Box: %#v\n", tb)

fmt.Println("Boards:")

for _, b := range boards {

fmt.Printf("\t%+v\n", b)

}

fmt.Println()

}

总的来说,这个例子尝试去展示组合是如何被应用的以及写代码时需要考虑的事情。最重要的是如何声明类型、如何让他们一起工作。下面是我们在对上文内容的总结:

  • 首先声明一套行为作为独立的接口类型。然后思考这些接口如何组成更大的行为集合。

  • 确保每个功能或方法对于他们接受的接口类型都是非常明确的。只接受是行为的接口类型,你才可以使用这个功能或方法。这将有助于指定所需更大的接口类型。

  • 想一下嵌入的内部类型和外部类型的关系。记住通过内部类型的促进,在内部类型声明的一切都可以推广到外部类型。然而内部类型的值存在于自身通常是基于导出规则进行访问。

  • 嵌入类型不是子类型也不是子类。具体类型的值代表了单一类型,不能是基于任何嵌入的关系。

  • 编译器可以在相关接口值间进行接口转换。在运行时接口转换不能是具体的类型,类型转换仅知道基于接口类型本身是做什么的,而不是实现他们包含具体的值。

我要感谢 Kevin Gillette https://twitter.com/kevingillette )与我一起写代码和发布帖子。也要感谢 Bill Hathaway(https://twitter.com/billhathaway) 协助校订和编辑帖子。

参考资料

原文链接: https://www.ardanlabs.com/blog/2015/09/composition-with-go.html

原文作者: William Kennedy

Container Day

“2018 Container Day暨Rancher Kubernetes企业用户大会” 将于6月29日在北京昆泰酒店(朝阳区望京启阳路2号)盛大召开,扫描下方二维码 免费报名 吧~

6Jz6zm7.jpg!web

bMVrI3M.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK