Go Inheritance and Polymorphism: Difference between revisions
Line 4: | Line 4: | ||
=Overview= | =Overview= | ||
<font color=darkkhaki>Needs Reviewing. Go does not really have type inheritance, only type embedding and compiler tricks.</font> | |||
Go does not have formal [[Object-Oriented_Programming#Inheritance|inheritance]] at language level. Inheritance can be implemented via a combination of [[Go_Structs#Embedded_Fields|struct field embedding]] and [[Go_Interfaces#Overview|interfaces]]. The [[#Inheritance|Inheritance]] section of this article explains how that is done. Once an implicit inheritance structure is put in place, [[#Polymorphism|polymorphism]] is also available. | Go does not have formal [[Object-Oriented_Programming#Inheritance|inheritance]] at language level. Inheritance can be implemented via a combination of [[Go_Structs#Embedded_Fields|struct field embedding]] and [[Go_Interfaces#Overview|interfaces]]. The [[#Inheritance|Inheritance]] section of this article explains how that is done. Once an implicit inheritance structure is put in place, [[#Polymorphism|polymorphism]] is also available. | ||
Revision as of 22:40, 13 August 2024
Internal
Overview
Needs Reviewing. Go does not really have type inheritance, only type embedding and compiler tricks.
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 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.
Type 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
The hierarchical relationship is declared by struct field embedding, the base type is declared as embedded field in all sub-types. This automatically allows for the base type instances to share state with the sub-types instances.
However, the lack of a formal type hierarchy with syntactic support makes "objects" in Go feel much more lightweight than in languages such as C++ or Java.
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
State and Deeper Hierarchies of Embedded Fields
The pattern holds recursively for deeper hierarchies. If we extend the vehicle
-Plane
hierarchy with a Plane
sub-type Fighter
, the Fighter
has access to the vehicle
's Speed
and the plane
's Wingspan
:
package transportation
type Fighter struct {
Plane
}
func NewFighter(speed int, wingspan int) *Fighter {
return &Fighter{Plane{vehicle{Speed: speed}, wingspan}}
}
f := transportation.NewFighter(1345, 33)
fmt.Printf("Fighter speed: %d mph, wingspan: %d ft\n", f.Speed, f.Wingspan)
Fighter speed: 1345 mph, wingspan: 33 ft
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 Car
s and Plane
s:
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")
}
Rather than requiring the programmer to declare ahead of time that two types are related, in Go a type automatically satisfies any interface that specifies a subset of its methods. Besides reducing the bookkeeping, this approach has real advantages. Types can satisfy many interfaces at once, without the complexities of traditional multiple inheritance. Interfaces can be very lightweight—an interface with one or even zero methods can express a useful concept. Interfaces can be added after the fact if a new idea comes along or for testing—without annotating the original types. Because there are no explicit relationships between types and interfaces, there is no type hierarchy to manage or discuss.
Behavior and Deeper Hierarchies of Embedded Fields
For the same vehicle
-Plane
-Fighter
hierarchy, the Fighter
has access to vehicle
's Move()
and plane
's SimulateZeroG()
method:
f.Move()
f.SimulateZeroG()
Polymorphism
Reprocess, incorporate this:
When a method call is made against an interface value, the equivalent method of the stored user-defined type value it is executed. The user-defined type that implements an interface is often called a concrete type for that interface.
Go implements polymorphism by allowing overriding of methods associated with embedded fields by the struct that embeds the field. This is done by declaring the struct that embeds the base type as a receiver type for a method with the same signature and return type. In our example, a vehicle
is the receiver type of a Move()
method, making it an implementation of the Vehicle
interface. We can override the Move()
method in the Plane
sub-type, by declaring Plane
also a Move()
receiver type. This makes the Move()
method subject to polymorphic behavior.
type Plane struct {
...
}
...
func (p *Plane) Move() {
fmt.Printf("flying at %d mph\n", p.Speed)
}
The code that invokes the Move()
method does not change, but the underlying behavior is different:
c := transportation.NewCar(...)
p := transportation.NewPlane(...)
c.Move()
p.Move()
moving at 100 mph
flying at 802 mph
In case of the Car
type, which does not override the Move()
method, the behavior is that of the method associated with the base type. For the Plane
type, the behavior is polymorphic, and it is the behavior defined by the Plane
type, which overrides Move()
.
Polymorphism is possible only with dynamic method dispatch. The only way to have dynamically dispatched methods in Go is through an interface. Methods on a struct or any other concrete type are always resolved statically.
For a generic discussion on polymorphism, see:
TODO
Polymorphism seems to work well when invoked on the subtypes, but it does not work when a supertype is used for invocation.