• 什么是程序设计中的Duck Typing 编程风格?
  • 发布于 2个月前
  • 215 热度
    0 评论
在程序设计中有一种编程风格叫 Duck Typing ,这种编程风格实际上是面向对象编程语言中所提倡的多态的表现,通过将某一类对象的行为抽象成为一个集合,当前对象的类型由这些抽象层所决定的,这和前面一篇我博文中提到抽象概念类似。典型的应用就是 Go 语言也提供了接口类型,使得我们可以面向接口编程,将实现和接口分离。在我看来,软件的抽象之美也应该以此来表达,和 Java 语言不同的是 Go 并不是那么 强制 ,它使用了一种鸭子类型的方式让动态类型成为可能。

目前 Go 只是某方面代码写的少有着较明显的特性,但是如果一个超大型项目未必有 Java 做的好,另外 Java 最新的设计草案 Implicit Classes and Enhanced Main Methods (Preview) 开始考虑把一些关键字默认隐式的使用,并且帮助学生和教育工作者能快速的了解 PL 设计。

Duck Typing
在 Go 中没有 implements 和 extends 这种关键字,这对我们而言反倒轻松了一些,它认为 Go 的接口就像鸭子测试里的描述:

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。在使用鸭子类型编程语言中,更应该关注的是对象的行为,对能干什么?对象有什么样的能力,这和上篇文章中介绍的多态和抽象接口是类似的,如果一组对象有相似的行为功能可以抽象的划分成为一类。


在通常编程过程中我们编写一个函数,函数接受某个类型的对象,并在函数内部调用它的行为,那这个函数只能接受某个特定类型的对象。而在提倡使用鸭子类型的语言中,这个函数可能就被修改成接受某个行为集合的对象作为参数,只要某个类型对象具备这个行为功能,那么就能被这个函数准确调用和运行,反之则会出现运行时错误。


抽象鸭子
我们用 Go 语言来实现一个鸭子🦆类型:
type Duck interface {
    Swim()    // 游泳
    Feathers() // 羽毛
}
这里使用 Go 提供的 interface 关键字定义了一个鸭子接口类型,这个接口中提供了鸭子的两种行为:游泳和羽毛是什么样的,但是没有提供实现。

我们见过的鸭子类型可多了去了,下面是你可能见过的:

组合接口
现在我们给鸭子再添加一种嘎嘎叫的能力,一种方式是在原有的接口上添加 “嘎嘎叫” 方法,这样做的话就表示所有的鸭子都应该拥有此能力,假设我们的玩具鸭并不能开口嘎嘎叫,所以它没有这种能力。这时候我们可以将会嘎嘎叫的鸭子单独定义一种类型,在 Go 可以使用组合的方式来实现:
type QuackDuck interface {
    Quack()  // 嘎嘎叫
    Duck     // 嵌入接口
}
这样 QuackDuck 类型就拥有了之前 Duck 提供的两种抽象能力,同时还应该拥有嘎嘎叫的能力。

接口实现
前面我们只给出了鸭子的能力定义,还没有任何实现,由于 Go 中没有继承和实现的关键字,想成为上述接口的实现非常简单,只要实现它们定义的方法就可以了。
// 堆代码 duidaima.com
// RealDuck - 真正的鸭子
type RealDuck struct { }

func (RealDuck) Swim() {
    fmt.Println("用鸭璞向后划水")
}

func (RealDuck) Feathers() {
    fmt.Println("遇到水也不会湿的羽毛")
}

func (RealDuck) Quack() {
    fmt.Println("嘎~ 嘎~ 嘎~")
}

// ToyDuck - 玩具鸭
type ToyDuck struct { }

func (ToyDuck) Swim() {
    fmt.Println("以固定的速度向前移动")
}

func (ToyDuck) Feathers() {
    fmt.Println("白色的固定的塑料羽毛")
}
可以看到我们定义了两种鸭子类型,一种是真正的鸭子,它还多实现了一种嘎嘎叫方法,另一个玩具鸭子只有游泳和羽毛这两种行为。这个编程方式和我们写普通的结构体方法没什么区别,只是对应的方法签名相同,其实这种方式在 Go 语言的标准库中有特别多的应用,比如:io.Reader、io.Writer 和 io.Closer。

接口使用
接下来我们可以使用一下这个类型了:
var duck Duck
duck = ToyDuck{}
duck.Swim()
duck.Feathers()
输出
以固定的速度向前移动
白色的固定的塑料羽毛
由于玩具鸭没有嘎嘎叫的能力,所以如果你这么写编译无法通过
错误实现,我们也可以用一种工厂的方式来进行调用:
func Factory(name string) Duck {
    switch name {
    case "toy":
        return &ToyDuck{}
    case "real":
        return &RealDuck{}
    default:
        panic("No such duck")
    }
}
// 堆代码 duidaima.com
func main() {
    duck := Factory("toy")
    duck.Swim()
    duck.Feathers()
}

总结
其实这就是 Go 中的多态体现,仔细体会其中的味道,在阅读其他源码的时候你会更加熟练,但是也有一个问题当代码工程庞大的时候你很难知道一个接口体到底实现了哪些接口,不过有幸的是我们生在最智能的 IDE 时代,在 GoLand 中可以帮你提示,还有一些大佬可能某些开源项目中看到这样的代码 var _ Person = (*Student)(nil) 这就是通过编译器一些检测来帮助查看某个类型是否实现某个接口巧妙的代码,如果当前类型没有实现这个接口编译器则会提示。
用户评论