28. 多态

欢迎来到 Golang 系列教程的第 28 篇。

Go 通过接口来实现多态。我们已经讨论过,在 Go 语言中,我们是隐式地实现接口。一个类型如果定义了接口所声明的全部方法,那它就实现了该接口。现在我们来看看,利用接口,Go 是如何实现多态的。

使用接口实现多态

一个类型如果定义了接口的所有方法,那它就隐式地实现了该接口。

所有实现了接口的类型,都可以把它的值保存在一个接口类型的变量中。在 Go 中,我们使用接口的这种特性来实现多态

通过一个程序我们来理解 Go 语言的多态,它会计算一个组织机构的净收益。为了简单起见,我们假设这个虚构的组织所获得的收入来源于两个项目:fixed billingtime and material。该组织的净收益等于这两个项目的收入总和。同样为了简单起见,我们假设货币单位是美元,而无需处理美分。因此货币只需简单地用 int 来表示。(我建议阅读 https://forum.golangbridge.org/t/what-is-the-proper-golang-equivalent-to-decimal-when-dealing-with-money/413 上的文章,学习如何表示美分。感谢 Andreas Matuschek 在评论区指出这一点。)

我们首先定义一个接口 Income

type Income interface {  
    calculate() int
    source() string
}

上面定义了接口 Interface,它包含了两个方法:calculate() 计算并返回项目的收入,而 source() 返回项目名称。

下面我们定义一个表示 FixedBilling 项目的结构体类型。

type FixedBilling struct {  
    projectName string
    biddedAmount int
}

项目 FixedBillin 有两个字段:projectName 表示项目名称,而 biddedAmount 表示组织向该项目投标的金额。

TimeAndMaterial 结构体用于表示项目 Time and Material。

type TimeAndMaterial struct {  
    projectName string
    noOfHours  int
    hourlyRate int
}

结构体 TimeAndMaterial 拥有三个字段名:projectNamenoOfHourshourlyRate

下一步我们给这些结构体类型定义方法,计算并返回实际收入和项目名称。

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

在项目 FixedBilling 里面,收入就是项目的投标金额。因此我们返回 FixedBilling 类型的 calculate() 方法。

而在项目 TimeAndMaterial 里面,收入等于 noOfHourshourlyRate 的乘积,作为 TimeAndMaterial 类型的 calculate() 方法的返回值。

我们还通过 source() 方法返回了表示收入来源的项目名称。

由于 FixedBillingTimeAndMaterial 两个结构体都定义了 Income 接口的两个方法:calculate()source(),因此这两个结构体都实现了 Income 接口。

我们来声明一个 calculateNetIncome 函数,用来计算并打印总收入。

func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

上面的函数接收一个 Income 接口类型的切片作为参数。该函数会遍历这个接口切片,并依个调用 calculate() 方法,计算出总收入。该函数同样也会通过调用 source() 显示收入来源。根据 Income 接口的具体类型,程序会调用不同的 calculate()source() 方法。于是,我们在 calculateNetIncome 函数中就实现了多态。

如果在该组织以后增加了新的收入来源,calculateNetIncome 无需修改一行代码,就可以正确地计算总收入了。:smile:

最后就剩下这个程序的 main 函数了。

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    incomeStreams := []Income{project1, project2, project3}
    calculateNetIncome(incomeStreams)
}

在上面的 main 函数中,我们创建了三个项目,有两个是 FixedBilling 类型,一个是 TimeAndMaterial 类型。接着我们创建了一个 Income 类型的切片,存放了这三个项目。由于这三个项目都实现了 Interface 接口,因此可以把这三个项目放入 Income 切片。最后我们将该切片作为参数,调用了 calculateNetIncome 函数,显示了项目不同的收益和收入来源。

以下完整的代码供你参考。

package main

import (  
    "fmt"
)

type Income interface {  
    calculate() int
    source() string
}

type FixedBilling struct {  
    projectName string
    biddedAmount int
}

type TimeAndMaterial struct {  
    projectName string
    noOfHours  int
    hourlyRate int
}

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    incomeStreams := []Income{project1, project2, project3}
    calculateNetIncome(incomeStreams)
}

在 playground 上运行

该程序会输出:

Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Net income of organisation = $19000

新增收益流

假设前面的组织通过广告业务,建立了一个新的收益流(Income Stream)。我们可以看到添加它非常简单,并且计算总收益也很容易,我们无需对 calculateNetIncome 函数进行任何修改。这就是多态的好处。

我们首先定义 Advertisement 类型,并在 Advertisement 类型中定义 calculate()source() 方法。

type Advertisement struct {  
    adName     string
    CPC        int
    noOfClicks int
}

func (a Advertisement) calculate() int {  
    return a.CPC * a.noOfClicks
}

func (a Advertisement) source() string {  
    return a.adName
}

Advertisement 类型有三个字段,分别是 adNameCPC(每次点击成本)和 noOfClicks(点击次数)。广告的总收益等于 CPCnoOfClicks 的乘积。

现在我们稍微修改一下 main 函数,把新的收益流添加进来。

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
    popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
    incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
    calculateNetIncome(incomeStreams)
}

我们创建了两个广告项目,即 bannerAdpopupAdincomeStream 切片包含了这两个创建的广告项目。

package main

import (  
    "fmt"
)

type Income interface {  
    calculate() int
    source() string
}

type FixedBilling struct {  
    projectName  string
    biddedAmount int
}

type TimeAndMaterial struct {  
    projectName string
    noOfHours   int
    hourlyRate  int
}

type Advertisement struct {  
    adName     string
    CPC        int
    noOfClicks int
}

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

func (a Advertisement) calculate() int {  
    return a.CPC * a.noOfClicks
}

func (a Advertisement) source() string {  
    return a.adName
}
func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
    popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
    incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
    calculateNetIncome(incomeStreams)
}

在 playground 中运行

上面程序会输出:

Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Income From Banner Ad = $1000  
Income From Popup Ad = $3750  
Net income of organisation = $23750

你会发现,尽管我们新增了收益流,但却完全没有修改 calculateNetIncome 函数。这就是多态带来的好处。由于新的 Advertisement 同样实现了 Income 接口,所以我们能够向 incomeStreams 切片添加 AdvertisementcalculateNetIncome 无需修改,因为它能够调用 Advertisement 类型的 calculate()source() 方法。

本教程到此结束。祝你愉快。

上一教程 – 组合取代继承

下一教程 – Defer

若文章对你有帮助,可以点赞或打赏支持我们。发布者:Aurora,转载请注明出处:http://61.174.243.28:13541/AY-knowledg-hub/28-%e5%a4%9a%e6%80%81/

(0)
AuroraAurora站点维系者
上一篇 2023年 12月 5日 下午5:43
下一篇 2023年 12月 5日 下午5:45

相关推荐

  • iotop

    文章目录iotop补充说明安装语法选项实例 iotop 用来监视磁盘I/O使用状况的工具 补充说明 iotop命令 是一个用来监视磁盘I/O使用状况的top类工具。iotop具有与…

    入门教程 2023年 12月 19日
  • Helm 升级

    升级版本 文章目录简介可选项从父命令继承的命令请参阅 简介 该命令将发布升级到新版的chart。 升级参数必须是发布和chart。chart参数可以是:chart引用(&#8216…

    入门教程 2023年 12月 14日
  • smbclient

    文章目录smbclient补充说明语法选项参数实例 smbclient 交互方式访问samba服务器 补充说明 smbclient命令 属于samba套件,它提供一种命令行使用交互…

    入门教程 2024年 3月 5日
  • cu

    文章目录cu补充说明语法选项实例 cu 用于连接另一个系统主机 补充说明 cu命令 用于连接另一个系统主机。cu(call up)指令可连接另一台主机,并采用类似拨号终端机的接口工…

    入门教程 2023年 12月 7日
  • pv

    文章目录pv补充说明下载安装语法选项实例注意 pv 显示当前在命令行执行的命令的进度信息,管道查看器 补充说明 pv命令 Pipe Viewer 的简称,由Andrew Wood …

    入门教程 2024年 3月 1日
  • rsync

    文章目录rsync补充说明语法选项实例 rsync 远程数据同步工具 补充说明 rsync命令 是一个远程数据同步工具,可通过LAN/WAN快速同步多台主机间的文件。rsync使用…

    入门教程 2024年 3月 4日
  • IOS GameKit

    文章目录简介实例步骤输出 简介 GameKit是iOS SDK中一个常用的框架。其核心功能有3个: 交互游戏平台Game Center, P2P设备通讯功能 In-Game Voi…

    2023年 4月 1日
  • bunzip2

    文章目录bunzip2补充说明语法选项参数实例 bunzip2 可解压缩.bz2格式的压缩文件。 补充说明 bzip2可以对文件进行压缩与解压缩。此命令类似于“gzip/gunzi…

    入门教程 2023年 12月 6日
  • make

    文章目录make补充说明语法选项参数知识扩展 make GNU的工程化编译工具 补充说明 make命令 是GNU的工程化编译工具,用于编译众多相互关联的源代码文件,以实现工程化的管…

    入门教程 2024年 1月 3日
  • Java 条件语句 – if…else

    Java 中的条件语句允许程序根据条件的不同执行不同的代码块。 一个 if 语句包含一个布尔表达式和一条或多条语句。 文章目录语法Test.java 文件代码:if…e…

    入门教程 2023年 3月 4日
Translate »