Let's Go: объектно-ориентированное программирование на Голанге
Russian (Pусский) translation by Anna k.Ivanova (you can also view the original English article)
Go - это странная смесь старых и новых идей. У него очень освежающий подход, при котором он не боится отбросить устоявшиеся представления о том, «как делать вещи». Многие люди даже не уверены, является ли Go объектно-ориентированным языком. Позвольте мне сказать это прямо сейчас. Является!
В этом руководстве вы узнаете обо всех тонкостях объектно-ориентированного проектирования в Go, о том, как такие основы объектно-ориентированного программирования, как инкапсуляция, наследование и полиморфизм, выражены в Go, и сравнение Go с другими языками.
Философия дизайна Go
Корни Go основаны на C и, в более широком смысле, на семействе Algol. Кен Томпсон в шутку сказал, что Роб Пайк, Роберт Грейнджер и он сам собрались вместе и решили, что они ненавидят C ++. Будь то шутка или нет, Go сильно отличается от C ++. Подробнее об этом позже. Go - это предельная простота Это подробно объясняется Робом Пайком в Меньше в геометрической прогрессии.
Go против Другие языки
В Go нет классов, объектов, исключений и шаблонов. Он имеет сборщик мусора и встроенный параллелизм. Самое поразительное упущение в отношении объектно-ориентированного подхода состоит в том, что в Go нет иерархии типов. Это отличается от большинства объектно-ориентированных языков, таких как C ++, Java, C #, Scala, и даже динамических языков, таких как Python и Ruby.
Go Особенности ОО
У Go нет классов, но есть типы. В частности, он имеет структуры. Структуры являются определяемыми пользователем типами. Структурные типы (с методами) служат тем же целям, что и классы в других языках.
Структуры
Структура определяет состояние. Вот структура Существа. Она имеет поле «Имя» и логический флаг «Реал», который говорит нам, является ли это реальным существом или воображаемым. Структуры содержат только состояние и никакого поведения.
1 |
type Creature struct { |
2 |
|
3 |
Name string |
4 |
|
5 |
Real bool |
6 |
|
7 |
}
|
8 |
Методы
Методы - это функции, которые работают с определенными типами. У них есть пункт получателя, который указывает, на какой тип они работают. Вот метод Dump(), который работает со структурами Существ и печатает их состояние:
1 |
func (c Creature) Dump() { |
2 |
fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real) |
3 |
}
|
Это необычный синтаксис, но он очень явный и понятный (в отличие от неявного «this» или запутанного «self» Python).
Встраивание
Вы можете встраивать анонимные типы друг в друга. Если вы встраиваете безымянную структуру, то встроенная структура предоставляет свое состояние (и методы) непосредственно встраиваемой структуре. Например, в FlyingCreature встроена безымянная структура Creature, что означает, что FlyingCreature является Creature.
1 |
type FlyingCreature struct { |
2 |
Creature
|
3 |
WingSpan int |
4 |
}
|
Теперь, если у вас есть экземпляр FlyingCreature, вы можете напрямую обращаться к его атрибутам Name и Real.
1 |
dragon := &FlyingCreature{ |
2 |
Creature{"Dragon", false, }, |
3 |
15, |
4 |
}
|
5 |
|
6 |
fmt.Println(dragon.Name) |
7 |
fmt.Println(dragon.Real) |
8 |
fmt.Println(dragon.WingSpan) |
Интерфейсы
Интерфейсы являются отличительной чертой объектно-ориентированной поддержки Go. Интерфейсы - это типы, которые объявляют наборы методов. Подобно интерфейсам в других языках, они не имеют реализации.
Объекты, которые реализуют все методы интерфейса, автоматически реализуют интерфейс. Не существует наследования или подклассов или ключевого слова "Implements". В следующем фрагменте кода тип Foo реализует интерфейс Fooer (по соглашению имена интерфейсов Go заканчиваются на «er»).
1 |
type Fooer interface { |
2 |
Foo1() |
3 |
Foo2() |
4 |
Foo3() |
5 |
}
|
6 |
|
7 |
type Foo struct { |
8 |
}
|
9 |
|
10 |
func (f Foo) Foo1() { |
11 |
fmt.Println("Foo1() here") |
12 |
}
|
13 |
|
14 |
func (f Foo) Foo2() { |
15 |
fmt.Println("Foo2() here") |
16 |
}
|
17 |
|
18 |
func (f Foo) Foo3() { |
19 |
fmt.Println("Foo3() here") |
20 |
}
|
Объектно-ориентированный дизайн: путь Go
Давайте посмотрим, как Go соотносится со столпами объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм. Это особенности языков программирования на основе классов, которые являются наиболее популярными объектно-ориентированными языками программирования.
По сути, объекты - это языковые конструкции, которые имеют состояние и поведение, которые воздействуют на состояние и выборочно предоставляют его другим частям программы.
Инкапсуляция
Go инкапсулирует вещи на уровне пакета. Имена, начинающиеся со строчной буквы, видны только внутри этого пакета. Вы можете скрыть все что угодно в закрытом пакете и просто предоставлять определенные типы, интерфейсы и фабричные функции.
Например, здесь, чтобы скрыть приведенный выше тип Foo и предоставить только интерфейс, который вы могли бы переименовать в строчный foo, и предоставить функцию NewFoo(), которая возвращает открытый интерфейс Fooer:
1 |
type foo struct { |
2 |
}
|
3 |
|
4 |
func (f foo) Foo1() { |
5 |
fmt.Println("Foo1() here") |
6 |
}
|
7 |
|
8 |
func (f foo) Foo2() { |
9 |
fmt.Println("Foo2() here") |
10 |
}
|
11 |
|
12 |
func (f foo) Foo3() { |
13 |
fmt.Println("Foo3() here") |
14 |
}
|
15 |
|
16 |
func NewFoo() Fooer { |
17 |
return &Foo{} |
18 |
}
|
Тогда код из другого пакета может использовать NewFoo() и получить доступ к интерфейсу Fooer, реализованному внутренним типом foo:
1 |
f := NewFoo() |
2 |
|
3 |
f.Foo1() |
4 |
|
5 |
f.Foo2() |
6 |
|
7 |
f.Foo3() |
Наследование
Наследование или подтипы всегда были спорным вопросом. Существует много проблем с наследованием реализации (в отличие от наследования интерфейса). Множественное наследование, реализованное в C ++, Python и других языках, страдает от проблемы смертельного алмаза смерти, но даже одиночное наследование не является пикником для хрупкой проблемы базового класса.
Современные языки и объектно-ориентированное мышление теперь предпочитают композицию, а не наследование. Go принимает это близко к сердцу и не имеет никакой иерархии типов. Это позволяет вам делиться деталями реализации через композицию. Но Go, в очень странном повороте (который, вероятно, возник из-за прагматических проблем), позволяет анонимную композицию посредством встраивания.
Для всех намерений и целей составление путем встраивания анонимного типа эквивалентно наследованию реализации. Встроенная структура так же хрупка, как и базовый класс. Вы также можете встроить интерфейс, который эквивалентен наследованию от интерфейса в таких языках, как Java или C ++. Это может даже привести к ошибке времени выполнения, которая не обнаруживается во время компиляции, если тип встраивания не реализует все методы интерфейса.
Здесь SuperFoo встраивает интерфейс Fooer, но не реализует его методы. Компилятор Go с радостью позволит вам создать новый SuperFoo и вызвать методы Fooer, но, очевидно, потерпит неудачу во время выполнения. Это компилируется:
1 |
type SuperFooer struct { |
2 |
Fooer
|
3 |
}
|
4 |
|
5 |
func main() { |
6 |
s := SuperFooer{} |
7 |
s.Foo2() |
Запуск этой программы приводит к панике:
1 |
panic: runtime error: invalid memory address or nil pointer dereference |
2 |
[signal 0xb code=0x1 addr=0x28 pc=0x2a78] |
3 |
|
4 |
goroutine 1 [running]:
|
5 |
panic(0xde180, 0xc82000a0d0) |
6 |
/usr/local/Cellar/go/1.6/libexec/src/runtime/panic.go:464 +0x3e6 |
7 |
main.main()
|
8 |
/Users/gigi/Documents/dev/go/src/github.com/oop_test/main.go:104 +0x48 |
9 |
exit status 2
|
10 |
|
11 |
Process finished with exit code 1
|
Полиморфизм
Полиморфизм - это основа объектно-ориентированного программирования: способность обрабатывать объекты разных типов одинаково, если они придерживаются одного и того же интерфейса. Интерфейсы Go предоставляют эту возможность очень прямым и интуитивно понятным способом.
Вот подробный пример, когда несколько существ (и дверь!), Которые реализуют интерфейс Dumper, создаются и сохраняются в срезе, а затем вызывается метод Dump() для каждого из них. Вы также заметите разные стили создания объектов.
1 |
package main |
2 |
|
3 |
import "fmt" |
4 |
|
5 |
type Creature struct {
|
6 |
Name string |
7 |
Real bool |
8 |
} |
9 |
|
10 |
func Dump(c*Creature) {
|
11 |
fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
|
12 |
} |
13 |
|
14 |
func (c Creature) Dump() {
|
15 |
fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
|
16 |
} |
17 |
|
18 |
type FlyingCreature struct {
|
19 |
Creature |
20 |
WingSpan int |
21 |
} |
22 |
|
23 |
func (fc FlyingCreature) Dump() {
|
24 |
fmt.Printf("Name: '%s', Real: %t, WingSpan: %d\n",
|
25 |
fc.Name, |
26 |
fc.Real, |
27 |
fc.WingSpan) |
28 |
} |
29 |
|
30 |
type Unicorn struct {
|
31 |
Creature |
32 |
} |
33 |
|
34 |
type Dragon struct {
|
35 |
FlyingCreature |
36 |
} |
37 |
|
38 |
type Pterodactyl struct {
|
39 |
FlyingCreature |
40 |
} |
41 |
|
42 |
func NewPterodactyl(wingSpan int) *Pterodactyl {
|
43 |
pet := &Pterodactyl{
|
44 |
FlyingCreature{
|
45 |
Creature{
|
46 |
"Pterodactyl", |
47 |
true, |
48 |
}, |
49 |
wingSpan, |
50 |
}, |
51 |
} |
52 |
return pet |
53 |
} |
54 |
|
55 |
type Dumper interface {
|
56 |
Dump() |
57 |
} |
58 |
|
59 |
type Door struct {
|
60 |
Thickness int |
61 |
Color string |
62 |
} |
63 |
|
64 |
func (d Door) Dump() {
|
65 |
fmt.Printf("Door => Thickness: %d, Color: %s", d.Thickness, d.Color)
|
66 |
} |
67 |
|
68 |
func main() {
|
69 |
creature := &Creature{
|
70 |
"some creature", |
71 |
false, |
72 |
} |
73 |
|
74 |
uni := Unicorn{
|
75 |
Creature{
|
76 |
"Unicorn", |
77 |
false, |
78 |
}, |
79 |
} |
80 |
|
81 |
pet1 := &Pterodactyl{
|
82 |
FlyingCreature{
|
83 |
Creature{
|
84 |
"Pterodactyl", |
85 |
true, |
86 |
}, |
87 |
5, |
88 |
}, |
89 |
} |
90 |
|
91 |
pet2 := NewPterodactyl(8) |
92 |
|
93 |
door := &Door{3, "red"}
|
94 |
|
95 |
Dump(creature) |
96 |
creature.Dump() |
97 |
uni.Dump() |
98 |
pet1.Dump() |
99 |
pet2.Dump() |
100 |
|
101 |
creatures := []Creature{
|
102 |
*creature, |
103 |
uni.Creature, |
104 |
pet1.Creature, |
105 |
pet2.Creature} |
106 |
fmt.Println("Dump() through Creature embedded type")
|
107 |
for _, creature := range creatures {
|
108 |
creature.Dump() |
109 |
} |
110 |
|
111 |
dumpers := []Dumper{creature, uni, pet1, pet2, door}
|
112 |
fmt.Println("Dump() through Dumper interface")
|
113 |
for _, dumper := range dumpers {
|
114 |
dumper.Dump() |
115 |
} |
116 |
} |
Заключение
Go - добросовестный объектно-ориентированный язык программирования. Он позволяет объектно-ориентированное моделирование и продвигает лучшую практику использования интерфейсов вместо конкретных иерархий типов. Go сделал несколько необычных синтаксических решений, но в целом работа с типами, методами и интерфейсами кажется простой, легкой и естественной.
Встраивание не очень чистое, но, очевидно, что в целях прагматизма вложение было обеспечено вместо композиции только по имени.



