面向对象思考与 golang cobra 库实现原理
golang 不是面向对象的语言 ,在golang中函数是一类成员(First-class function)/知乎解释 。本文不打算纠结 golang 有哪些面向对象特性,仅关注面向对象的思想如何在 golang 中应用,让我们轻松一些写出类似 cobra 中 comamnd.go 这样易于使用、可扩展的程序。
本文要点:
- 面向对象设计与编程基本概念
- Golang 的与面向对象支持相关的知识
- 用设计模式设计 command.go
前提条件:
- go tour 练习完成
- 使用 flag 包处理简单的 cli
- 会用 cobra
- 熟悉 C++ 或 Java
环境准备
使用 cobra 创建一个 golang 应用,文件结构
main.go
/cmd
root.go
register.go
delete.go
其中: register 和 delete 命令支持 -u --user=name
参数
1、面向对象设计与编程基本概念
什么是面向对象?
Everything is an Object.
--- Bruce Eckel 《Thinking Java》
对于普通人,面向对象设计与编程是最常见的选择。多年产业实践证明,面向对象具有具有易于理解、易于复用(reuse)和可扩展(extend)的优势。如果我们把世界的一切用函数来理解,这需要你具备更加优秀的抽象思维能力,特别是数学思维能力。Lisp 等语言的成功,证明了以 λ 演算为基础语言的重要性,它更容易高效编写高品质的程序。同样,用顺序、分支与循环这样结构化方法理解计算,则相对机械一些。
面向对象的语言?
以 对象 作为基本 程序结构单位 的程序设计语言。纯面向对象语言:Smalltalk, Eiffel(埃菲尔),…,Java。 其中 Java 是最成功的程序设计语言,长期排在 TIOBE 编程语言指数排行榜前二位。随着互联网发展,尽管出现了许多新的语言 Golang, Clojure, Scala,Dart 等更挣钱的语言的竞争,Java 目前稳居排行榜第一位, “存在即真理” 的背后,一定有它的道理。
什么是对象
An object is the simulation of an entity in a computer.
An object is an encapsulation of attributes, behaviors and constraints.
面向对象语言的特点
- 包装 Encapsulation
- 信息隐藏 Information Hiding
- 数据抽象 Data Abstraction
- 继承 Inheritance
- 多态 Polymorphism
通过上述技术,实现了 Reference System -> Design & Programming
面向对象的设计
设计对象、接口、对象之间的关系,用于解决问题。
设计模式
常见应用场景的涉及的对象、对象外部特征、及其之间关系,以及典型代码。例如:
- 单例(Singleton)模式
- 命令(Command) 模式
- 模板方法(Template methods)模式
- 组合(Composite)模式
- …
2、Golang 的语言知识与折中(trade off)
函数虽然高雅,但不是一般人容易理解与接受,并写出高品质的程序。运用面向对象的思想在非 OOP 语言中编程,就是必须 get 的技能了。
首先,我们复习一些语言知识,同时讨论 go 语言设计与折中的选择。
2.1 Go 语言的基本元素
- 数据与数据类型 type
- 函数 func
- 包 package
深入理解 Nicklaus Wirth 算法+数据结构=程序 这句话永远不错。
Go的理念: 简单、简单、简单 (没考证 !?对吗?)
- 数据就是数据,是不可变的。例如:
- Student 作为数据就是 Student,不是 Person
- Student 作为对象是 Person
- 函数是一种类型,值是 First-Class 的
- 函数类型 = 函数签名 (什么是函数签名?)
Go 是静态类型化的。每个变量都有一个静态类型,也就是说,在编译的时候变量的类型就被很精确地确定下来。
2.2 Go 的包装与隐藏
包导出类型与数据
go 的包装单位是 package。 将包中数据或类型的 第一个字母大写 ,就导出该包中的内容。
包命名
包的全名称是工作区 src 中目录结构的路径(除了语言内置包),短名称就是最后目录名。为了便于编程,包可以使用 别名(alias)
例如,我们不喜欢 go 内置包 flag
处理参数的风格,我们喜欢 POSIX 风格命令行,怎么办?
import (
flag "github.com/spf13/pflag"
)
搞定啦!你命令行程序不需要修改其他地方了。
实践: 将 delete.go 程序拖到 main.go 所在的目录,运行程序 go run main.go help
结果是?
go 程序文件
包由一些 go 文件构成。一个包中不能出现相同的类型或变量,包括函数名。它不似 java 那样一个文件对应一个对象类型,你必须机智的记住 go 文件中定义的包变量和类型。你可能需要编写 vars.go
或 types.go
来集中管理它们,但一个类型、一个变量一个文件也是最佳实践之一。
init() 和 main() 函数
每个 go 文件可以有一个特殊的函数 init()
, 它是包中唯一可以重名的函数。包初始化代码必须在 init() 中。
它们的执行顺序是? 参见 go/golang main() init()方法的调用
2.3 数据抽象
基本类型不用解释!
Java 数据抽象仅一类: Object, 它对外呈现一些 interface,对象之间通过消息协作。
Go 要复杂一些:
- struct
- func
- interface
- specials: chan
methods
go 的方法似乎很有特色,让我们操作数据时有了“面向对象”的感觉。其实,在上世纪面向对象语言Eiffel(埃菲尔) (第二个)就有了啊。感兴趣去看一看。
- var type 语法
- eiffelThreads vs goroutine 并发
- static typing
- …
go-method 的确是值得夸赞的创新,它与接口实现了无缝对接。它通过 receiver argument
概念,让不同类型的 operator 具有了相同的函数签名。而一组函数签名相同的操作与接口一致,则实现了接口与数据类型的隐式静态绑定。
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{
3, 4}
fmt.Println(v.Abs(), Abs(v))
}
在以上代码中,函数 Abs
与 方法 Abs
语义是一样的,方法可以被认为是“语法糖”。但对于编译,Vertex 类型的数据可 静态 推导到接口类型
type Abser interface {
Abs() float64
}
构造方法
为了实现 struct 的构造,golang 提供了一些编程约定(convention),例如:
- 提供
NewVertex(v Vertex) *Vertex