Go Inheritance and Polymorphism: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 102: Line 102:
moving at 100 mph
moving at 100 mph
moving at 802 mph
moving at 802 mph
</syntaxhighlight>
Obviously, sub-type specific behavior can be declared on each sub-type. A plane can <code>SimulateZeroG()</code> and a car cannot.
<syntaxhighlight lang='go'>
type Plane struct {
    ...
}
func (p *Plane) SimulateZeroG() {
    fmt.Printf("engaging in parabolic flight and simulating zero G\n")
}
</syntaxhighlight>
</syntaxhighlight>



Revision as of 23:20, 4 August 2024

Internal

Overview

Go does not have formal inheritance at language level. Inheritance can be implemented via a combination of struct field embedding and interfaces. The Inheritance section of this article explains how that is done. Once an implicit inheritance structure is put in place, polymorphism is also available.

Inheritance

In its most generic form, inheritance in programming languages is the capability of declaring class hierarchies. A hierarchy includes generic classes, called superclasses, that are inherited by more specific and specialized classes, called subclasses.

The superclasses declare attributes (state) and behavior that are shared with all their descendants. This is a great capability when it comes to code reusability. The generic state and behavior is declared only once, in superclass, and it does not have to be repeated in subclasses. The subclasses also have the capability to declare state and behavior that is particular to them only, and differentiate them from siblings in the hierarchy. For more general information on inheritance see:

Inheritance

Inheritance as defined above ca be implemented in Go, but without formal support from the language. State inheritance is implemented using struct embedding. Behavior inheritance is implemented using interfaces.

Example Hierarchy

We use an example hierarchy involving vehicles. The base type, and the specialized types are declare in their own source files, in the same transportation package:

 pkg
  └── transportation
       ├── vehicle.go
       ├── car.go
       └── plane.go

State Inheritance

We implement state inheritance with struct embedding. We declare a base struct that contains all the attributes to be shared by inherited types. In this case, in a vehicle hierarchy, the base type is vehicle. The base type is declared package-private, as it does not need exposure outside the package. It contains fields that are common to all members of the hierarchy, like Speed, for example. The fields that need to be visible outside the package as fields of the sub-types (see Car and Plane below) are declared as package-public:

package transportation

type vehicle struct {
	Speed int
}

All sub-types, like a Car or a Plane, share a "speed" attribute that does not needed to be declared when we declared the corresponding types, as long as we embed the base type vehicle.

package transportation

type Car struct {
	vehicle
}

func NewCar(speed int) *Car {
	return &Car{vehicle{Speed: speed}}
}

Each sub-type, in this case Plane can declare its own specific fields, which are particular to the sub-type and not shared via the base type. A plane has a wingspan, while a car doesn't:

package transportation

type Plane struct {
	vehicle
    Wingspan int # Wingspan in feet, rounded to the closest foot.
}

func NewPlane(speed int, wingspan int) *Plane {
	return &Plane{vehicle{Speed: speed}, wingspan}
}

Note that Car and Plane are exported, presumably we need to use these types outside the package, unlike the base type vehicle. The base type state becomes available as sub-types state:

c := transportation.NewCar(100)
p := transportation.NewPlane(802, 197)
	
fmt.Printf("car speed:      %d mph\n", c.Speed)
fmt.Printf("plane speed:    %d mph\n", p.Speed)
fmt.Printf("plane wingspan: %d feet\n", p.Wingspan)
car speed:      100 mph
plane speed:    802 mph
plane wingspan: 197 feet

Behavior Inheritance

Behavior inheritance in case of type hierarchies is implemented with interfaces. The base type is declared as receiver of a method that expresses behavior common to all sub-types, and automatically all subtypes implement that interface.

In our example, all vehicles move, so that behavior can be defined as a method of the base type vehicle. At the same time, we declare a Vehicle interface with a Move() methods, so automatically vehicle and its sub-types Car and Plane implement it. The implementation of the shared behavior accesses shared state, which is represented by the Speed field, which is part of the state of both Cars and Planes:

package transportation

type vehicle struct {
    ...  
}

type Vehicle interface {
	Move()
}

func (v *vehicle) Move() {
	fmt.Printf("moving at %d mph\n", v.Speed)
}

Note that while the base type is package-private and declared with a lower-case identifier vehicle, the interface that expresses the behavior it shares with the hierarchy is package-public and has the same name as the base type, but starting with an upper-case: Vehicle.

The sub-types automatically implement this interface because they all have the based type as an embedded field (more explanations needed here), so the behavior is automatically available to them:

c := transportation.NewCar(...)
p := transportation.NewPlane(...)

c.Move()
p.Move()
moving at 100 mph
moving at 802 mph

Obviously, sub-type specific behavior can be declared on each sub-type. A plane can SimulateZeroG() and a car cannot.

type Plane struct {
    ...
}

func (p *Plane) SimulateZeroG() {
    fmt.Printf("engaging in parabolic flight and simulating zero G\n")
}

Overriding

Polymorphism

Polymorphism is available in Go, but it is implemented differently than in other object-oriented languages.


For a generic discussion on polymorphism, see:

Object-Oriented Programming | Polymorphism

TO DEPLETE

Read and merge into document: