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()
方法,该方法是一个多态的方法,定义如下:
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")
}
下一章节,我们将接口其它的知识点再给大家说一下。