22.Go面向对象-接口

2022-01-17 10:31:29 浏览数 (1)

22.Go面向对象-接口

6 接口

在讲解具体的接口之前,先看如下问题。

使用面向对象的方式,设计一个加减的计算器

代码如下:

代码语言:javascript复制
// 父类
type ObjectOperate struct {
   num1 int
   num2 int
}

// 加法类
type AddOperate struct {
   ObjectOperate
}

// 加法类的方法
func (p *AddOperate) Operate(a, b int) int {
   p.num1 = a
   p.num2 = b
   return p.num1   p.num2
}

// 减法类
type SubOperate struct {
   ObjectOperate
}

// 减法类方法
func (p *SubOperate) Operate(a, b int) int {
   p.num1 = a
   p.num2 = b
   return p.num1 - p.num2
}

func main() {
   // 创建减法类
   var sub SubOperate
   i := sub.Operate(5, 2) // 执行减法
   fmt.Println(i)
} 

以上实现非常简单,但是有个问题,在main()函数中,当我们想使用减法操作时,创建减法类的对象,调用其对应的减法的方法。

但是,有一天,系统需求发生了变化,要求使用加法,不再使用减法,那么需要对main()函数中的代码,做大量的修改。

将原有的代码注释掉,创建加法的类对象,调用其对应的加法的方法。

有没有一种方法,让main()函数,只修改很少的代码就可以解决该问题呢?

有,要用到接下来给大家讲解的接口的知识点。

6.1 什么是接口

接口就是一种规范与标准,在生活中经常见接口,例如:笔记本电脑的USB接口,可以将任何厂商生产的鼠标与键盘,与电脑进行链接。

为什么呢?原因就是,USB接口将规范和标准制定好后,各个生产厂商可以按照该标准生产鼠标和键盘就可以了。

在程序开发中,接口只是规定了要做哪些事情,干什么。具体怎么做,接口是不管的。这和生活中接口的案例也很相似,例如:USB接口,只是规定了标准,但是不关心具体鼠标与键盘是怎样按照标准生产的.

在企业开发中,如果一个项目比较庞大,那么就需要一个能理清所有业务的架构师来定义一些主要的接口,这些接口告诉开发人员你需要实现那些功能。

6.2 接口定义

接口定义的语法如下:

代码语言:javascript复制
// 定义接口类型
type Humaner interface {
   // 方法,只有声明,没有实现,由别的类型(自定义类型)实现
   sayhi()
}  

怎样具体实现接口中定义的方法呢?

代码语言:javascript复制
// 定义Student类
type Student struct {
   name string
   id   int
}

// Student实现了此接口方法
func (tmp *Student) sayhi()  {
   fmt.Printf("Student[%s, %d] sayhin", tmp.name, tmp.id)
}

// 定义Teacher类
type Teacher struct {
   addr  string
   group string
}

// Teacher实现了此方法
func (tmp *Teacher) sayhi() {
   fmt.Printf("Teacher[%s, %s] sayhin", tmp.addr, tmp.group)
}

具体的调用如下:

代码语言:javascript复制
func main() {
   // 定义接口类型的变量
   var i Humaner
   // 只是实现了此接口方法的类型,那么这个类型的变量(接收者类型)就可以给i赋值
   s := &Student{"mike", 666}
   i = s
   i.sayhi()

   t := &Teacher{"beijing", "go"}
   i = t
   i.sayhi()
}

//执行
Student[mike, 666] sayhi
Teacher[beijing, go] sayhi 

只要类(结构体)实现对应的接口,那么根据该类创建的对象,可以赋值给对应的接口类型。

接口的命名习惯以er结尾。

6.3 多态

接口有什么好处呢?实现多态。

所谓多态指的是多种表现形式,如下图所示:

img

该拖拉机既可以扫地又可以当风扇。功能非常强大。

使用接口实现多态的方式如下:

代码语言:javascript复制
// 定义接口类型
type Humaner interface {
   // 方法,只有声明,没有实现,由别的类型(自定义类型)实现
   sayhi()
}

// 定义Student类
type Student struct {
   name string
   id   int
}

// Student实现了此接口方法
func (tmp *Student) sayhi() {
   fmt.Printf("Student[%s, %d] sayhin", tmp.name, tmp.id)
}

// 定义Teacher类
type Teacher struct {
   addr  string
   group string
}

// Teacher实现了此方法
func (tmp *Teacher) sayhi() {
   fmt.Printf("Teacher[%s, %s] sayhin", tmp.addr, tmp.group)
}

// 定义一个普通函数,函数的参数为接口类型
// 只有一个函数,可以有不同的表现,实现了多态
func WhoSayHi(i Humaner)  {
   i.sayhi()
}

func main() {
   // 定义接口类型的变量
   var i Humaner
   // 只是实现了此接口方法的类型,那么这个类型的变量(接收者类型)就可以给i赋值
   s := &Student{"mike", 666}
   t := &Teacher{"beijing", "go"}

   // 调用同一个函数,传递不同的类对象,不同的表现,实现了多态
   WhoSayHi(s)
   WhoSayHi(t)
}
练习:
练习1:
代码语言:javascript复制
type Person struct{
    name string
}

func (p *Person) SayHello(){
    fmt.Println("我是人类,我叫:" p.name)
}

type Chinese struct{
    Person
}

func (c *Chinese) SayHello(){
    fmt.Println("我是中国人,我叫:" c.name)
}

type English struct{
    Person
}

func (e *English) SayHello(){
    fmt.Println("我是英国人,我叫:" e.name)
}

type Personer interface{
    SayHello()
}

func main(){
    c := &Chinese{Person{"zhangsan"}}
    e := &English{Person{"MrZhang"}}
    x := make([]Personer,2)
    x[0] = c
    x[1] = e
    for i:=0;i<len(x);i  {
        x[i].SayHello()
 }
}

上面的案例与第一个案例,基本上是一样的,不同之处是在输出方面,通过一个循环获取切片中存储的所有对象,然后分别调用SayHello()方法。

如果没有接口,那么只能一个一个的调用其方法。

练习2:

用多态来实现 将 移动硬盘或者U盘或者MP3插到电脑上进行读写数据(分析类,接口,方法)

代码语言:javascript复制
type MobileStorager interface {
 Read()
 Write()
}

type MobileDisk struct { //移动硬盘
}

func (m *MobileDisk) Read() {
 fmt.Println("移动硬盘在读取数据")
}
func (m *MobileDisk) Write() {
 fmt.Println("移动硬盘在写入数据")
}

type UDisk struct {
}

func (u *UDisk) Read() {
 fmt.Println("U盘在读取数据")
}
func (u *UDisk) Write() {
 fmt.Println("U盘在写入数据")
}

type Computer struct {
}

func (c *Computer) CpuRead(i MobileStorager) {
 i.Read()
}
func (c *Computer) CpuWrite(i MobileStorager) {
 i.Write()
}

func main() {
 m := &MobileDisk{}
 c := &Computer{}
 c.CpuRead(m)
 c.CpuWrite(m)
}

// 执行:
移动硬盘在读取数据
移动硬盘在写入数据

练习3:

麻雀会飞 鹦鹉会飞,直升飞机会飞

代码语言:javascript复制
type Flyabler interface {
 Fly()
}

type Bird struct {
}

func (b *Bird) EatAndDrink() { //为Bird定义方法
 fmt.Println("鸟儿吃喝")
}

type MaQue struct { //麻雀
 Bird
}

func (m *MaQue) Fly() {
 fmt.Println("麻雀会飞")
}

type YingWu struct {
 Bird
}

func (y *YingWu) Fly() {
 fmt.Println("鹦鹉飞")
}

type Plane struct {
}

func (p *Plane) Fly() {
 fmt.Println("飞机飞")
}

func WhoFly(i Flyabler) {
 i.Fly()
}

func main() {
 m := &MaQue{}
 m.EatAndDrink()
 WhoFly(m)
 plane := &Plane{}
 WhoFly(plane)
}

// 执行:
鸟儿吃喝
麻雀会飞
飞机飞
计算器案例

关于接口的定义,以及使用接口实现多态,大家都比较熟悉了,但是多态有什么好处呢?

现在还是以开始提出的计算器案例给大家讲解一下,在开始我们已经实现了一个加减功能的计算器,但是有同学感觉太麻烦了,因为实现加法,就要定义加法操作的类(结构体),实现减法就要定义减法的类(结构体),所以该同学实现了一个比较简单的加减法的计算器,如下所示:

1. 使用面向对象的思想实现一个加减功能的计算器,可能有同学感觉非常简单,代码如下:
代码语言:javascript复制
type Operation struct {
}

func (p *Operation) GetResult(numA float64, numB float64, operate string) float64 {
   var result float64
   switch operate {
   case " ":
      result = numA   numB
   case "-":
      result = numA - numB
   }
   return result
}

func main() {
   var operation Operation
   sum := operation.GetResult(10, 13, " ")
   fmt.Println(sum)
} 

我们定义了一个类(结构体),然后为该类创建了一个方法,封装了整个计算器功能,以后要使用直接使用该类(结构体)创建对象就可以了。

这就是面向对象的封装性。

也就是说,当你写完这个计算器后,交给你的同事,你的同事要用,直接创建对象,然后调用GetResult()方法就可以, 根本不需要关心该方法是怎样实现的.

这不是我们前面在讲解面向对象概念时说到的,找个对象来干活吗?不需要自己去实现该功能。

2.大家仔细观察上面的代码,有什么问题吗?

现在让你在改计算器中,再增加一个功能,例如乘法,应该怎么办呢?你可能会说很简单啊,直接在GetResult()方法的switch中添加一个case分支就可以了。

问题是:在这个过程中,如果你不小心将加法修改成了减法怎么办?或者说,对加法运算的规则做了修改怎么办?

举例子说明:

你可以把该程序方法想象成公司中的薪资管理系统。如果公司决定对薪资的运算规则做修改,由于所有的运算规则都在Operation类中的GetResult()方法中,所以公司只能将该类的代码全部给你,你才能进行修改。

这时,你一看自己作为开发人员工资这么低,心想“TMD,老子累死累活才给这么点工资,这下有机会了”。直接在自己工资后面加了3000

numA numB 3000

所以说,我们应该将 加减等运算分开,不应该全部糅合在一起,这样你修改加的时候,不会影响其它的运算规则:

具体实现如下:

代码语言:javascript复制
// 定义操作父类
type Operation struct {
   numA float64
   numB float64
}

// 定义结果接口
type GetResulter interface {
   GetResult() float64 // 方法有返回值
}

// 加法
type OperationAdd struct {
   Operation
}

func (a *OperationAdd) GetResult() float64 {
   return a.numA   a.numB
}

// 减法
type OperationSub struct {
   Operation
}

func (s *OperationSub) GetResult() float64 {
   return s.numA - s.numB
}

img

现在已经将各个操作分开了,并且这里我们还定义了一个父类(结构体),将公共的成员放在该父类中。如果现在要修改某项运算规则,只需将对应的类和方法发给你,进行修改就可以了。

这里的实现虽然将各个运算分开了,但是与我们第一次实现的还是有点区别。我们第一次实现的加减计算器也是将各个运算分开了,但是没有定义接口。那么该接口的意义是什么呢?继续看下面的问题。

3:现在怎样调用呢?

这就是我们一开始给大家提出的问题,如果调用的时候,直接创建加法操作的对象,调用对应的方法,那么后期要改成减法呢?

需要做大量的修改,所以问题解决的方法如下:

代码语言:javascript复制
// 创建工厂类
type OperationFactory struct {

}

func (f *OperationFactory) CreateOption(option string, numA float64, numB float64) float64  {
   var result float64
   switch option {
   case " ":
      add := &OperationAdd{Operation{numA, numB}}
      result = OperationWho(add)
   case "-":
      sub := &OperationSub{Operation{numA, numB}}
      result = OperationWho(sub)
   }
   return result
}

// 使用接口实现多态
func OperationWho(i GetResulter) float64 {
   return i.GetResult()
} 

创建了一个类OperationFactory,在改类中添加了一个方法CreateOption()负责创建对象,如果输入的是“ ”,创建

OperationAdd的对象,然后调用OperationWho()方法,将对象的地址传递到该方法中,所以变量i指的就是OperationAdd,接下来在调用GetResult()方法,实际上调用的是OperationAdd类实现的GetResult()方法。

同理如果传递过来的是“-”,流程也是一样的。

所以,通过该程序,大家能够体会出多态带来的好处。

4:最后调用
代码语言:javascript复制
func main() {
   var factory OperationFactory
   s := factory.CreateOption("-", 10, 12)
   fmt.Println(s)
} 

这时会发现调用,非常简单,如果现在想计算加法,只要将”-”,修改成” ”就可以。

也就是说,除去了main( )函数与具体运算类的依赖。

当然程序经过这样设计以后:

如果现在修改加法的运算规则,只需要修改OperationAdd类中对应的方法,不需要关心其它的类,如果现在要增加“乘法” 功能,应该怎样进行修改呢?

第一:定义乘法的类,完成乘法运算。

第二:在OperationFactory类中CrateOption()方法中添加相应的分支。

但是这样做并不会影响到其它的任何运算。

大家可以自己尝试实现“乘法”与“除法”的运算。

在使用面向对象思想解决问题时,一定要先分析,定义哪些类,哪些接口,哪些方法。把这些分析定义出来,然后在考虑具体实现。

最后完整代码如下:

代码语言:javascript复制
// 定义操作父类
type Operation struct {
   numA float64
   numB float64
}

// 定义结果接口
type GetResulter interface {
   GetResult() float64 // 方法有返回值
}

// 加法
type OperationAdd struct {
   Operation
}

func (a *OperationAdd) GetResult() float64 {
   return a.numA   a.numB
}

// 减法
type OperationSub struct {
   Operation
}

func (s *OperationSub) GetResult() float64 {
   return s.numA - s.numB
}

// 创建工厂类
type OperationFactory struct {

}

func (f *OperationFactory) CreateOption(option string, numA float64, numB float64) float64  {
   var result float64
   switch option {
   case " ":
      add := &OperationAdd{Operation{numA, numB}}
      result = OperationWho(add)
   case "-":
      sub := &OperationSub{Operation{numA, numB}}
      result = OperationWho(sub)
   }
   return result
}

// 使用接口实现多态
func OperationWho(i GetResulter) float64 {
   return i.GetResult()
}


func main() {
   var factory OperationFactory
   s := factory.CreateOption("-", 10, 12)
   fmt.Println(s)
}

通过以上案例,大家应该能够体会出多态的好处。

下面我们再通过一个练习,体会一下接口和多态的应用。

练习:设计一个4s店卖车的程序。(分析要设计多个类,多个方法,接口)

首先我们思考一下,该程序需要设计几个类(结构体)

大家想到的有汽车类,还有4s店类。

所以基本设计的代码:

(1):创建汽车类(结构体)
代码语言:javascript复制
// 汽车类
type Car struct {
   
}

func (p *Car) Move()  {
   fmt.Println("汽车移动")
}

func (p *Car) Stop()  {
   fmt.Println("汽车停止")
} 

并在改结构体中定义了两个方法。

(2):创建4S店类(结构体)
代码语言:javascript复制
// 4s店类
type CarStore struct {
}

func (c *CarStore) Order(money float64) *Car {
   if money > 50000 {
      return &Car{}
   } else {
      return nil
   }
} 

为该类(结构体)添加Order()方法,该方法的作用就是卖车,所以需要给该方法传递“钱”,然后进行判断,如果条件满足,就返回Car地址,所以返回类型为*Car

(3):下面进行调用
代码语言:javascript复制
func main() {
   var carStore CarStore
   car := carStore.Order(80000)
   car.Move()
   car.Stop()
}
(4):如果在增加不同品牌的车,应该怎样处理呢?

代码如下:

代码语言:javascript复制
// 汽车类
type Car struct {
   catType string 
   money   float64
}

func (p *Car) Move() {
   fmt.Println(p.catType   "汽车移动")
}

func (p *Car) Stop() {
   fmt.Println(p.catType   "汽车停止")
}

// 定义BMW车
type BMWCar struct {
   Car
} 

在以上的代码中,定义了“宝马车”类,让其继承Car类,并且在Car类中定义了两个成员 。

(5) 定义接口

在定义接口前,又定义了“奥迪车”类,也让其继承Car类

代码语言:javascript复制
// 定义奥迪车类
type AudiCar struct {
   Car
}

然后定义一个接口:

代码语言:javascript复制
type CarTyper interface {
   GetCar()
} 

下面实现该接口中定义的方法,

代码语言:javascript复制
func (p *BMWCar) GetCar() {
   if p.money >= 60000 {
      p.Move()
      p.Stop()
   } else {
      fmt.Println("钱不够,无法买宝马!")
   }
}

该方法的作用就是判断钱是否够了,如果钱够了,就可以调用车具有的方法。

代码语言:javascript复制
func (p *AudiCar) GetCar() {
   if p.money >= 70000 {
      p.Move()
      p.Stop()
   } else {
      fmt.Println("钱不够,无法买奥迪!")
   }
}
(6):对Order()的改造
代码语言:javascript复制
func (c *CarStore) Order(money float64, carType string) {
   switch carType {
   case "BMW":
      CarWho(&BMWCar{Car{carType, money}})
   case "Audi":
      CarWho(&AudiCar{Car{carType, money}})
   }

} 

根据传递过来的车的类型,进行判断,然后调用CarWho()方法,该方法是一个多态的方法,定义如下:

代码语言:javascript复制
func CarWho(i CarTyper)  {
   i.GetCar()
} 

如果传递过来的类型是”BMW”,在调用CarWho()方法时,将BMWCar{}类(结构体)的地址传递到该方法中(同时完成了父类Car中两个属性的赋值)。

由于CarWho()方法参数的类型是一个接口,但是BMWCar{}类(结构体)实现了该接口,所以是完全可以BMWCar{}类的地址传递过来。

这时参数i指的就是BMWCar{},调用GetCar()方法,指的就是BMWCar{}实现的方法。

在该方法中完成钱数的判断。

同理如果传递的过来的类型是“Audi”,那么过程也是一样的。

(7) main( )函数的调用
代码语言:javascript复制
func main() {
   var carStore CarStore
   carStore.Order(30000, "Audi")
}
(8) 完整代码如下:
代码语言:javascript复制
// 汽车类
type Car struct {
   catType string
   money   float64
}

func (p *Car) Move() {
   fmt.Println(p.catType   "汽车移动")
}

func (p *Car) Stop() {
   fmt.Println(p.catType   "汽车停止")
}

// 定义BMW车
type BMWCar struct {
   Car
}

func (p *BMWCar) GetCar() {
   if p.money >= 60000 {
      p.Move()
      p.Stop()
   } else {
      fmt.Println("钱不够,无法买宝马!")
   }
}

// 定义奥迪车类
type AudiCar struct {
   Car
}

func (p *AudiCar) GetCar() {
   if p.money >= 70000 {
      p.Move()
      p.Stop()
   } else {
      fmt.Println("钱不够,无法买奥迪!")
   }
}

type CarTyper interface {
   GetCar()
}

// 4s店类
type CarStore struct {
}

func (c *CarStore) Order(money float64, carType string) {
   switch carType {
   case "BMW":
      CarWho(&BMWCar{Car{carType, money}})
   case "Audi":
      CarWho(&AudiCar{Car{carType, money}})
   }

}

func CarWho(i CarTyper)  {
   i.GetCar()
}

func main() {
   var carStore CarStore
   carStore.Order(30000, "Audi")
}

下一章节,我们将接口其它的知识点再给大家说一下。

0 人点赞