Polymorphism - OOP in Go

Welcome to tutorial no. 28 in Golang tutorial series.

Polymorphism in Go is achieved with the help of interfaces. As we have already discussed, interfaces are implicitly implemented in Go. A type implements an interface if it provides definitions for all the methods declared in the interface. Let’s see how polymorphism is achieved in Go with the help of interfaces.

Polymorphism using an interface

Any type which provides definition for all the methods of an interface is said to implicitly implement that interface. This will be more clear as we discuss an example of polymorphism shortly.

A variable of type interface can hold any value which implements the interface. This property of interfaces is used to achieve polymorphism in Go.

Let’s understand polymorphism in Go with the help of a program that calculates the net income of an organization. For simplicity, let’s assume that this imaginary organization has income from two kinds of projects viz. fixed billing and time and material. The net income of the organization is calculated by the sum of the incomes from these projects. To keep this tutorial simple, we will assume that the currency is dollars and we will not deal with cents. It will be represented using int. (I recommend reading https://forum.golangbridge.org/t/what-is-the-proper-golang-equivalent-to-decimal-when-dealing-with-money/413 to learn how to represent cents. Thanks to Andreas Matuschek in the comments section for pointing this out.)

Let’s first define an interface Income.

1type Income interface {
2	calculate() int
3	source() string
4}

The Income interface defined above contains two methods calculate() which calculates and returns the income from the source and source() which returns the name of the source.

Next, let’s define a struct for FixedBilling project type.

1type FixedBilling struct {
2	projectName string
3	biddedAmount int
4}

The FixedBilling project has two fields projectName which represents the name of the project and biddedAmount which is the amount that the organization has bid for the project.

The TimeAndMaterial struct will represent projects of Time and Material type.

1type TimeAndMaterial struct {
2	projectName string
3	noOfHours  int
4	hourlyRate int
5}

The TimeAndMaterial struct has three fields names projectName, noOfHours and hourlyRate.

The next step would be to define methods on these struct types which calculate and return the actual income and source of income.

 1func (fb FixedBilling) calculate() int {
 2	return fb.biddedAmount
 3}
 4
 5func (fb FixedBilling) source() string {
 6	return fb.projectName
 7}
 8
 9func (tm TimeAndMaterial) calculate() int {
10	return tm.noOfHours * tm.hourlyRate
11}
12
13func (tm TimeAndMaterial) source() string {
14	return tm.projectName
15}

In the case of FixedBilling projects, the income is just the amount bid for the project. Hence we return this from the calculate() method of FixedBilling type.

In the case of TimeAndMaterial projects, the income is the product of the noOfHours and hourlyRate. This value is returned from the calculate() method with receiver type TimeAndMaterial.

We return the name of the project as the source of income from the source() method.

Since both FixedBilling and TimeAndMaterial structs provide definitions for the calculate() and source() methods of the Income interface, both structs implement the Income interface.

Let’s declare the calculateNetIncome function which will calculate and print the total income.

1func calculateNetIncome(ic []Income) {
2	var netincome int = 0
3	for _, income := range ic {
4		fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
5		netincome += income.calculate()
6	}
7	fmt.Printf("Net income of organization = $%d", netincome)
8}

The calculateNetIncome function above accepts a slice of Income interfaces as argument. It calculates the total income by iterating over the slice and calling calculate() method on each of its items. It also displays the income source by calling source() method. Depending on the concrete type of the Income interface, different calculate() and source() methods will be called. Thus we have achieved polymorphism in the calculateNetIncome function.

In the future, if a new kind of income source is added by the organization, this function will still calculate the total income correctly without a single line of code change :).

The only part remaining in the program is the main function.

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

In the main function above we have created three projects, two of type FixedBilling and one of type TimeAndMaterial. Next, we create a slice of type Income with these 3 projects. Since each of these projects has implemented the Income interface, it is possible to add all three projects to a slice of type Income. Finally, we call calculateNetIncome function and pass this slice as a parameter. It will display the various income sources and the income from them.

Here is the full program for your reference.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type Income interface {
 8	calculate() int
 9	source() string
10}
11
12type FixedBilling struct {
13	projectName string
14	biddedAmount int
15}
16
17type TimeAndMaterial struct {
18	projectName string
19	noOfHours  int
20	hourlyRate int
21}
22
23func (fb FixedBilling) calculate() int {
24	return fb.biddedAmount
25}
26
27func (fb FixedBilling) source() string {
28	return fb.projectName
29}
30
31func (tm TimeAndMaterial) calculate() int {
32	return tm.noOfHours * tm.hourlyRate
33}
34
35func (tm TimeAndMaterial) source() string {
36	return tm.projectName
37}
38
39func calculateNetIncome(ic []Income) {
40	var netincome int = 0
41	for _, income := range ic {
42		fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
43		netincome += income.calculate()
44	}
45	fmt.Printf("Net income of organization = $%d", netincome)
46}
47
48func main() {
49	project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
50	project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
51	project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
52	incomeStreams := []Income{project1, project2, project3}
53	calculateNetIncome(incomeStreams)
54}

Run in playground

This program will output

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

Adding a new income stream to the above program

Let’s say the organization has found a new income stream through advertisements. Let’s see how simple it is to add this new income stream and calculate the total income without making any changes to the calculateNetIncome function. This becomes possible because of polymorphism.

Lets first define the Advertisement type and the calculate() and source() methods on the Advertisement type.

 1type Advertisement struct {
 2	adName     string
 3	CPC        int
 4	noOfClicks int
 5}
 6
 7func (a Advertisement) calculate() int {
 8	return a.CPC * a.noOfClicks
 9}
10
11func (a Advertisement) source() string {
12	return a.adName
13}

The Advertisement type has three fields adName, CPC (cost per click) and noOfClicks (number of clicks). The total income from ads is the product of CPC and noOfClicks.

Let’s modify the main function a little to include this new income stream.

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

We have created two ads namely bannerAd and popupAd. The incomeStreams slice includes the two ads we just created.

Here is the full program after adding Advertisement.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type Income interface {
 8	calculate() int
 9	source() string
10}
11
12type FixedBilling struct {
13	projectName  string
14	biddedAmount int
15}
16
17type TimeAndMaterial struct {
18	projectName string
19	noOfHours   int
20	hourlyRate  int
21}
22
23type Advertisement struct {
24	adName     string
25	CPC        int
26	noOfClicks int
27}
28
29func (fb FixedBilling) calculate() int {
30	return fb.biddedAmount
31}
32
33func (fb FixedBilling) source() string {
34	return fb.projectName
35}
36
37func (tm TimeAndMaterial) calculate() int {
38	return tm.noOfHours * tm.hourlyRate
39}
40
41func (tm TimeAndMaterial) source() string {
42	return tm.projectName
43}
44
45func (a Advertisement) calculate() int {
46	return a.CPC * a.noOfClicks
47}
48
49func (a Advertisement) source() string {
50	return a.adName
51}
52func calculateNetIncome(ic []Income) {
53	var netincome int = 0
54	for _, income := range ic {
55		fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
56		netincome += income.calculate()
57	}
58	fmt.Printf("Net income of organization = $%d", netincome)
59}
60
61func main() {
62	project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
63	project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
64	project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
65	bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
66	popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
67	incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
68	calculateNetIncome(incomeStreams)
69}

Run in playground

The above program will output,

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 organization = $23750

You would have noticed that we did not make any changes to the calculateNetIncome function though we added a new income stream. It just worked because of polymorphism. Since the new Advertisement type also implemented the Income interface, we were able to add it to the incomeStreams slice. The calculateNetIncome function also worked without any changes as it was able to call the calculate() and source() methods of the Advertisement type.

This brings us to the end of this tutorial. I hope you liked it. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.

If you would like to advertise on this website, hire me, or if you have any other software development needs please email me at naveen[at]golangbot[dot]com.

Next tutorial - Defer