Composition vs. Inheritance
Composition and inheritance are two ways to achieve code reuse and build complex systems. Inheritance is a mechanism that allows a class to inherit properties and behavior from a parent class. Composition is a way of building complex objects by combining smaller, simpler objects.
In Go, composition is favored over inheritance. This is because Go does not have classes like traditional object-oriented programming languages. Instead, Go uses structs to define objects and interfaces to define behavior. Composition is achieved by embedding one struct into another.
For example, a solution to compose a simple report in Go can be model as follows:
type celsius float64type temperature struct {
maximum, minimum celsius
}type location struct {
latitude, longitude float64
}type report struct {
sol int
temperature temperature
location location
}
Composition in Golang
In Go, composition refers to the structuring of data and behavior by combining multiple smaller types into a single, larger type. Composition in Go is achieved through embedding, which allows a struct
to inherit the fields and methods of another struct
. This allows for code reuse and modular design. With composition, complex types can be built from simpler components, promoting separation of concerns and making the code easier to understand and test.
Example
Example 1: Composition of Structs(匿名嵌入结构体类型)
// Golang program to store information
// about games in structs and display them
package main
import "fmt"
// We create a struct details to hold
// generic information about games
type details struct {
genre string
genreRating string
reviews string
}
// We create a struct game to hold
// more specific information about
// a particular game
type game struct {
name string
price string
// We use composition through
// embedding to add the
// fields of the details
// struct to the game struct
details
}
// this is a method defined
// on the details struct
func (d details) showDetails() {
fmt.Println("Genre:", d.genre)
fmt.Println("Genre Rating:", d.genreRating)
fmt.Println("Reviews:", d.reviews)
}
// this is a method defined
// on the game struct
// this method has access
// to showDetails() as well since
// the game struct is composed
// of the details struct
func (g game) show() {
fmt.Println("Name: ", g.name)
fmt.Println("Price:", g.price)
g.showDetails()
}
func main() {
// defining a struct
// object of Type details
action := details{"Action","18+", "mostly positive"}
// defining a struct
// object of Type game
newGame := game{"XYZ","$125", action}
newGame.show()
}
与 Java对比
再补充一点便于理解 Golang 语言本身在组合上的努力。Golang从语言级别对组合做了充分的语法糖,使得组合更加高效。我们来看一段 Java的组合实现。
public interface IHello {
public void hello();
}
public class A implements IHello {
@Override
public void hello() {
System.out.println("Hello, I am A.");
}
}
public class B implements IHello {
@Override
public void hello() {
System.out.println("Hello, I am B.");
}
}
public class C {
IHello h;
public void hello() {
h.hello();
}
}
public static void main(String args[]) {
C c = new C();
c.h = new A();
c.hello();
c.h = new B();
c.hello();
}
例中类C组合了接口IHello, 如需暴露IHello的方法则需要添加一个代理方法,这样在代码量上会多于继承方式。golang中无需额外代码即可提供支持。
Shape : Area 0
Circle : Area 314.1592653589793
Rectangle : Area 0
Example 2: Composition through Embedding in an Interface(匿名嵌入接口类型)
type Token interface {
Type() TokenType
Lexeme() string
}
type Match struct {
toktype TokenType
lexeme string
}
func (m *Match) Type() TokenType {
return m.toktype
}
func (m *Match) Lexeme() string {
return m.lexeme
}
type IntegerConstant struct {
Token
value uint64
}
func (i *IntegerConstant) Value() uint64 {
return i.value
}
IntegerConstant中匿名嵌入了Token类型,使得IntegerConstant"继承"了Token的字段和方法。很酷的方法!我们可以这样写代码:
t := IntegerConstant{&Match{KEYWORD, "wizard"}, 2}
fmt.Println(t.Type(), t.Lexeme(), t.Value())
x := Token(t)
fmt.Println(x.Type(), x.Lexeme())
我们没有编写Type()和Value()方法的代码,但是*IntegerConstant也实现了Token接口,非常棒。
Example 3: Composition through Embedding in Composite Interfaces
In the Go language interfaces are implicitly implemented. That is to say, if methods, which are defined in an interface, are used on objects such as structs, then the struct is said to implement the interface. An interface can be embedded with other interfaces in order to form composite interfaces. If all the interfaces in a composite interface are implemented, then the composite interface is also said to be implemented by that object.
// Golang Program to implement composite interfaces
package main
import "fmt"
type purchase interface {
sell()
}
type display interface {
show()
}
// We use the two previous
// interfaces to form
// The following composite
// interface through embedding
type salesman interface {
purchase
display
}
type game struct {
name, price string
gameCollection []string
}
// We use the game struct to
// implement the interfaces
func (t game) sell() {
fmt.Println("--------------------------------------")
fmt.Println("Name:", t.name)
fmt.Println("Price:", t.price)
fmt.Println("--------------------------------------")
}
func (t game) show() {
fmt.Println("The Games available are: ")
for _, name := range t.gameCollection {
fmt.Println(name)
}
fmt.Println("--------------------------------------")
}
// This method takes the composite
// interface as a parameter
// Since the interface is composed
// of purchase and display
// Hence the child methods of those
// interfaces can be accessed here
func shop(s salesman) {
fmt.Println(s)
s.sell()
s.show()
}
func main() {
collection := []string{"XYZ",
"Trial by Code", "Sea of Rubies"}
game1 := game{"ABC", "$125", collection}
shop(game1)
}
Example 3
Example 4
A common example is having a struct/map with a Mutex.
type SafeStruct struct {
SomeField string
*sync.Mutex
}
It is much easier to type
safe := SafeStruct{SomeField: "init value"}
safe.Lock()
defer safe.Unlock()
safe.SomeField = "new value"
than having to either write appropriate wrapper functions (which are repetitive) or have the stutter of
safe.mutex.Unlock()
when the only thing you would ever do with the mutex field is access the methods (Lock()
and Unlock()
in this case)
This becomes even more helpful when you are trying to use multiple functions on the embedded field (that implemement an interface like io.ReadWriter
).
Reference
- https://go.dev/doc/effective_go#embedding
- https://stackoverflow.com/questions/36704522/why-would-you-want-to-use-composition-in-golang
- https://www.geeksforgeeks.org/composition-in-golang/
- https://juejin.cn/post/6844903977964797965
- https://hackthology.com/golangzhong-de-mian-xiang-dui-xiang-ji-cheng.html