10 Go面向“对象”:面向接口编程
一、面向对象初探
在软件开发领域,你应该听到过过程式编程、面向对象编程、甚至函数式编程等软件开发方式。而面向对象编程更是在现今大行其道,JAVA就是面向对象语言的代表,在JAVA中一切皆对象,它让编程中的一切元素、甚至设计方式都标准化,这更有利于大型应用的编写。
1.什么是面向对象编程?
面向对象编程,简称OOP。在OOP的理念下,任何事物无论简单还是复杂都可以用对象表示,每个对象都包含属性和方法,属性表示对象是什么?有什么特征?方法表示对象能做什么?有什么能力?任何应用的构建都转化成对象关系的设计,这演化成一套标准化的面向对象设计模式。
类和对象
要理解OOP,首先要理解类和对象的关系,类是设计层面的概念,而对象则是程序运行时的概念,OOP程序设计基于类的设计,类在程序运行时实例化为对象实现真正的业务逻辑。简而言之,所谓类可以理解成对象的模板,你编写一个类,在运行时需要实例化才能在程序调用栈中传递。
属性和方法
在过程式编程中,我们熟悉变量和函数,使用这些基本元素我们实现业务逻辑。而属性和方法是对象内部的特征,咋一看他们很像,其实本质上是将实现特定功能的函数和变量封装成一个整体,即对象。一个对象包含一系列的属性和方法专注于实现某种特定功能。
接口
接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能),接口是多态实现的基础。
2.面向对象三大特性
封装
将实现特定功能的属性和方法抽象封装成类,提供public/private/protected访问修饰符,控制外部访问的可见性。
.对业务相近的变量和函数封装在类/结构体中
.变量=>类/结构体的属性
.函数=>类/结构体的方法
继承
对已经实现的类或接口提供重用或扩展的能力,子类可完全继承父类的所有能力。继承的过程,就是从一般到特殊的过程,其过程可以通过继承和组合来实现。
.继承的类拥有父类/父结构体的全部属性和方法
.接口继承则是一行代码拥有父类接口的全部抽象方法
.继承可以节约大量重复代码
.继承的目的:
.提高代码的复用度
.拓展出新的属性和方法
.改进父类/结构体的方法
.以覆写父类/结构体的方法类实现
多态
多态是指一个父类/接口可以拥有多种具体的子类实现形态;多态的好处是可以根据业务需要去方便地调度子类们的共性和个性
3.面向对象编程的优缺点
优点
- 高效:面向对象设计结构、模块清晰的应用,有利于大型应用的开发,团队成员各自维护局部模块,降低了成员开发的复杂性。
- 易维护:由于高内聚低耦合,各个模块的维护都是局部的,这非常方便定位问题。
- 易扩展:继承、封装、多态的特性,以及标准化的设计模式设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
缺点
俗话说,“如果你手上只有锤子,那么你看什么都是钉子”,OOP把一切都当成对象,但现实世界是复杂的,虽然设计模式就是解决应用中抽象的设计问题的。这把编码阶段的复杂性提到设计阶段。
- 相对过程式编程,面向对象编程的程序结构性能有所下降;
- 提高系统设计的复杂度;
二、Go的面向“对象”
了解了面向对象编程的思想,我们再来看Go的面向对象,严格来说,Go并非面向对象编程语言,Go有自己的设计理念,其团队把它定位为系统级别的语言,也就是说它能做更底层的应用。面向对象只是一种软件开发方法,Go对面向对象有自己的支持方式。
1.没有类和对象,只有类型和值
传统的JAVA类和对象:
//类定义
public class Person {
private String name
...
//构造方法
public Person( String name){
this.name = name
}
//公开方法
public String getName (){
return this.name
}
...
}
//类实例化成对象
Person p = new Person("name")
//调用并打印公开方法
System.out.print(p.getName())
Go的类型和值:
- 通过定义结构体类型的方式实现类似类的结构
- 没有构造方法,直接使用NewXXX()工厂方法
//类型定义
type Preson struct {
name string
}
//类型方法
func (p *Person) SetName(name string) {
p.name = name
}
//类型方法
func (p *Person) GetName() string {
return p.name
}
//工厂方法
func NewPerson(name string) *Person{
p := new(Person)
p.SetName(name)
return p
}
//获取person类型的值指针
p := NewPerson("name")
fmt.Println(p.GetName())
2.聚合和嵌入优于继承
传统的JAVA继承:
public class Student extend Person{
private String school
//构造方法
public Student(){
super() //直接使用父类的构造方法
}
public void doSomething(){
//block
}
...
}
Go的聚合和嵌入:
type Preson struct {
Name string
age int
}
func (p *Person) SetAge(age int){
p.age = age
}
//嵌入
type Student1 struct {
Person //匿名字段为嵌入类型
School string
}
//聚合
type Student2 struct {
Ps Person //命名字段为聚合类型
School string
}
func OOPDemo() {
s1 := new(Student1)
s1.Name = "fun1" //嵌入的类型可直接使用其内部属性,更像继承
s1.School = "Social University1"
s1.SetAge(18) //可以直接使用Person的方法
s2 := new(Student2)
s2.Ps.Name = "fun2" //聚合的类型需要先访问属性值名,在访问属性值内部的属性
s2.School = "Social University2"
s1.Ps.SetAge(18) //可以间接使用Person的方法
//OUTPUT:
//s1: &{{fun1} Social University1}
//s2: &{{fun2} Social University2}
}
3.自由的结构体属性类型
//接口
type IPerson interface {
SetName(string)
GetName() string
}
//自定义函数
type MyFuncType func(int) int
//大杂烩结构体
type Something struct {
a int //基本数据类型
b []byte //切片
p Person //结构体
s *Student //指针
i IPerson //接口
f MyFuncType //自定义函数类型
any interface{} //任意类型
}
4.独立的方法定义更灵活
Go的类型方法在外部任意地方,只要定义的方法接收者为该类型,那定义的方法就是该类型的方法。
//类型定义
type Preson struct {
name string
}
//类型方法,接收者为该类型的指针
func (p *Person) SetName(name string) {
p.name = name
}
//类型方法,接收者为该类型的值
func (p Person) GetName() string {
return p.name
}
//调用示例
p := new(Person)
//SetName()方法接收者为指针,使用指针类型或值类型去调用都可以
p.SetName("fun") //可以
*p.SetName("func") //指针取值后再去调用也可以。
//GetName()方法接收者为值类型,所以调用该方法只能为值
p.GetName() //不可以
*p.GetName() //可以
方法接收者一般有两种情况:
- 接收者为指针:允许该类型的指针和值调用该方法;
- 接收者为值:只允许该类型的值调用该方法。
一般无特殊需要,建议把接收者直接设置为指针类型
5.没有显式public/private/protected,只有隐式大小写控制
Go的访问控制基于包,包内的成员变量、常量、类型、函数基于命名首字母的大小写控制。
其结构体类型的属性方法也类似,基于命名首字母的大小写控制。
type Preson struct {
Name string //外部可见并可修改
age int //仅内部可见内部方法修改
}
//类型方法,外部可见
func (p *Person) SetAge(age int) {
//调用内部方法
p.setage(name)
}
//类型方法,内部可见
func (p *Person) setage(age int) {
p.age = age
}
三、Go面向接口编程
严格意义讲,Go因没有对象概念,所以并非面向对象编程语言,但因其对OOP的深刻理解,使其设计理念更切合“面向接口”,接口的多态特性使其在设计高内聚低耦合的系统发挥更重要的作用。面向接口概念是面向对象的衍生,在多年的开发积累中,人们发现针对接口设计系统可以让系统扩展性和维护性更好。因此,Go的OOP针对接口设计,可以说接口是头等的类型也不为过。
在Go语言中,接口拥有举足轻重的地位,而面向接口编程也是Go语言核心的设计理念。接口是高度抽象的概念,它是一种类型可由type关键字声明,接口内部声明一个或多个方法签名,因此不能实例化,一般创建一个类型为接口的变量,它可以被赋值为任何满足该接口声明的实际类型的值,作为类型传递。
1.接口 —— 实现鸭子类型
当它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。
接口本身是类型,但它却不关心类型,它只关心行为,如果类型T的行为(实现的方法)和定义的接口I声明的方法签名符合,那么类型T就实现了I的接口。在方法参数传递或各种类型校验中,T就是I的实现。
一个Go接口也是类型定义,其内部声明了其规定的方法签名:
//任何实现了IAnimal签名方法的类型都属于IAnimal类型
type IAnimal interface {
Live()
Dead()
}
//Humen结构体
type Monkey struct {}
func (m *Monkey) Live() {
fmt.Println("猴子活着吃香蕉!!!")
}
func (m *Monkey) Dead() {
fmt.Println("香蕉有毒,猴子死了!!!")
}
//Cat结构体
type Cat struct {}
func (c *Cat) Live() {
fmt.Println("猫活着吃鱼!!!")
}
func (c *Cat) Dead() {
fmt.Println("猫吃了河豚死翘翘!!!")
}
//演示:
//声明一个IAnimal的切片
var zoo []IAnimal
func addAnimal(animal IAnimal) {
zoo = append(zoo,animal)
}
func OOPDemo02(){
m := new(Monkey)
c := new(Cat)
addAnimal(m)
addAnimal(c)
//遍历
for _,animal := range zoo {
animal.Live()
animal.Dead()
}
}
//OUTPUT:
//猴子活着吃香蕉!!!
//香蕉有毒,猴子死了!!!
//猫活着吃鱼!!!
//猫吃了河豚死翘翘!!!
2.接口类型限定赋值
类型值赋值给接口类型
type IPerson {
GetName()
SetName()
}
//任何实现GetName()、SetName()方法的类型都可赋值给person
var person IPerson
接口类型赋值给另一接口类型
type Writer interface{ //父接口
Write(buf []byte) (n int,err error)
}
type ReadWriter interface{ //子接口
Read(buf []byte) (n int,err error)
Write(buf []byte) (n int,err error)
}
var file1 ReadWriter=new(File) //子接口实例
var file2 Writer=file1 //子接口实例赋值给父接口
3.非侵入式接口
上面演示我们看到,接口的运用在编码中是非侵入式的,在经典的OOP语言中,实现接口需要类显式实现,例如:
public class Person implements IPerson {
//block
}
而Go并不需要显式实现,类型只需实现特定接口的方法签名即可。当然这种极度宽松的实现方式有可能让你定义的类型“不小心”就实现了某些接口能力,一旦你的类型方法和某些接口方法签名一致时就会如此。
//该类型实现了标准包的io.Writer接口
type F struct {}
func (f *F) Write(p []byte) (n int, err error) {
//block
}
4.接口嵌入
和结构体类型类似,接口也可以嵌入其他接口,接口只能嵌入不能聚合!
以下演示接口嵌入的示例:
//生物接口
type IBeing interface{
Live()
Dead()
}
//动物接口嵌入生物接口
type IAnimal interface {
IBeing
Hunting()
}
//植物接口嵌入生物接口
type IPlant interface {
IBeing
Growing()
}
type Tiger struct {}
func (t *Tiger) Live() {
fmt.Println("老虎活着称大王!!!")
}
func (t *Tiger) Dead() {
fmt.Println("老虎战斗死了!!!")
}
func (t *Tiger) Hunting() {
fmt.Println("老虎捕猎!!!")
}
type Flower struct{}
func (t *Flower) Live() {
fmt.Println("花儿享受阳光!!!")
}
func (t *Flower) Dead() {
fmt.Println("花儿落下死了!!!")
}
func (t *Flower) Growing() {
fmt.Println("花儿茁壮成长!!!")
}
//声明一个interface{}的切片
var earth []interface{}
func addBeing(b interface{}) {
earth = append(earth, b)
}
func OOPDemo03() {
tiger := new(Tiger)
flower := new(Flower)
addBeing(tiger)
addBeing(flower)
//遍历
for _, being := range earth {
if animal, ok := being.(IAnimal); ok {
animal.Live()
//如果是动物则捕猎
animal.Hunting()
animal.Dead()
}
if plant, ok := being.(IPlant); ok {
plant.Live()
//如果是植物则成长
plant.Growing()
plant.Dead()
}
}
}
//OUTPUT:
//老虎活着称大王!!!
//老虎捕猎!!!
//老虎战斗死了!!!
//花儿享受阳光!!!
//花儿茁壮成长!!!
//花儿落下死了!!!
...
5.没有泛型
在其他面向对象语言中,我们都会接触到泛型的概念,当我们遇到要对不同类型做统一的内部实现是,使用泛型是非常常规的做法,有了泛型后我们不必对每种数据类型做同样的实现。但是在Go中没有泛型,为什么Go团队设计时不加入泛型?我们需要了解泛型的本质是什么。
无论是JAVA中的泛型支持还是C++中的模板方法,使用泛型的初衷在于程序编码时减少同时支持多种类型相同内部实现的方法的编码量,把编码的复杂性延迟到运行时自动创建对相应类型的支持,Go团队认为泛型的支持在类型系统和运行时的复杂性花费太大,还没有找到更优化的设计,这是Go团队比较犹豫的地方。
Go语言对泛型有其替代方案:interface{},一般叫空接口,空接口内部没有声明任何方法,因此指代Go中的任何类型,但声明为空接口类型时,指代接收任何数据类型,在使用通过空接口传递的数据时,通过类型断言的方式接收数据并做进一步处理。这种做法把运行时出错的可能性提前到编译时发现,如有运行时错误也能等到友好的处理。
type Person struct {
name string
}
func (p *Person) PrintName() {
fmt.Println(p.name)
}
func main() {
var person interface{} = new(Person)
p, ok := person.(*Person)
if ok {
p.name="fun"
p.PrintName()
}else{
fmt.PrintLn("Type Error!")
}
}