Статьи

Let’s Go: объектно-ориентированное программирование на Голанге

Go — странная смесь старых и новых идей. У него очень свежий подход, при котором он не боится отбросить устоявшиеся представления о том, «как делать вещи». Многие люди даже не уверены, является ли Go объектно-ориентированным языком. Позвольте мне положить это на отдых прямо сейчас. Это!

В этом руководстве вы узнаете обо всех тонкостях объектно-ориентированного проектирования в Go, о том, как такие основы объектно-ориентированного программирования, как инкапсуляция, наследование и полиморфизм, выражены в Go, и как Go сравнивается с другими языками.

Go — невероятно мощный язык программирования, изучите все от написания простых утилит до создания масштабируемых, гибких веб-серверов в нашем полном курсе.

  • Go Lang
    Основные принципы построения веб-серверов
    Дерек Дженсен

Корни Go основаны на C и в более широком смысле на семействе Algol . Кен Томпсон в шутку сказал, что Роб Пайк, Роберт Грейнджер и он сам собрались вместе и решили, что они ненавидят C ++. Будь то шутка или нет, Go сильно отличается от C ++. Подробнее об этом позже. Go — это предельная простота. Это подробно объясняется Робом Пайком в « Меньше» в геометрической прогрессии .

В Go нет классов, объектов, исключений и шаблонов. Он имеет сборщик мусора и встроенный параллелизм. Самое поразительное упущение в отношении объектно-ориентированного подхода заключается в том, что в Go нет иерархии типов. Это отличается от большинства объектно-ориентированных языков, таких как C ++, Java, C #, Scala, и даже динамических языков, таких как Python и Ruby.

У Go нет классов, но есть типы. В частности, это имеет структуру. Структуры являются определяемыми пользователем типами. Структурные типы (с методами) служат тем же целям, что и классы в других языках.

Структура определяет состояние. Вот структура Существа. У него есть поле «Имя» и логический флаг «Реал», который говорит нам, является ли это реальным существом или воображаемым существом. Структуры содержат только состояние и никакого поведения.

1
2
3
4
5
6
7
type Creature struct {
 
  Name string
 
  Real bool
 
}

Методы — это функции, которые работают с определенными типами. У них есть пункт получателя, который указывает, на какой тип они работают. Вот метод Dump() который работает со структурами Существ и печатает их состояние:

1
2
3
func (c Creature) Dump() {
  fmt.Printf(«Name: ‘%s’, Real: %t\n», c.Name, c.Real)
}

Это необычный синтаксис, но он очень явный и понятный (в отличие от неявного «this» или запутанного «я» Python).

Вы можете встраивать анонимные типы друг в друга. Если вы встраиваете безымянную структуру, то встроенная структура предоставляет свое состояние (и методы) непосредственно встраиваемой структуре. Например, в FlyingCreature безымянная структура Creature , что означает, что FlyingCreature является Creature .

1
2
3
4
type FlyingCreature struct {
  Creature
  WingSpan int
}

Теперь, если у вас есть экземпляр FlyingCreature, вы можете напрямую обращаться к его атрибутам Name и Real.

1
2
3
4
5
6
7
8
dragon := &FlyingCreature{
    Creature{«Dragon», false, },
    15,
}
 
fmt.Println(dragon.Name)
fmt.Println(dragon.Real)
fmt.Println(dragon.WingSpan)

Интерфейсы являются отличительной чертой объектно-ориентированной поддержки Go. Интерфейсы — это типы, которые объявляют наборы методов. Подобно интерфейсам в других языках, они не имеют реализации.

Объекты, которые реализуют все методы интерфейса, автоматически реализуют интерфейс. Не существует наследования или подклассов или ключевого слова «Implements». В следующем фрагменте кода тип Foo реализует интерфейс Fooer (по соглашению имена интерфейсов Go заканчиваются на «er»).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
type Fooer interface {
  Foo1()
  Foo2()
  Foo3()
}
 
type Foo struct {
}
 
func (f Foo) Foo1() {
    fmt.Println(«Foo1() here»)
}
 
func (f Foo) Foo2() {
    fmt.Println(«Foo2() here»)
}
 
func (f Foo) Foo3() {
    fmt.Println(«Foo3() here»)
}

Давайте посмотрим, как Go соотносится со столпами объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм. Это особенности языков программирования на основе классов , которые являются наиболее популярными объектно-ориентированными языками программирования.

По сути, объекты — это языковые конструкции, которые имеют состояние и поведение, которые воздействуют на состояние и выборочно предоставляют его другим частям программы.

Go инкапсулирует вещи на уровне пакета. Имена, начинающиеся со строчной буквы, видны только внутри этого пакета. Вы можете скрыть все что угодно в закрытом пакете и просто предоставлять определенные типы, интерфейсы и фабричные функции.

Например, здесь, чтобы скрыть приведенный выше тип Foo и предоставить только интерфейс, вы можете переименовать его в foo в нижнем регистре и предоставить NewFoo() функция, которая возвращает публичный интерфейс Fooer:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
type foo struct {
}
 
func (f foo) Foo1() {
    fmt.Println(«Foo1() here»)
}
 
func (f foo) Foo2() {
    fmt.Println(«Foo2() here»)
}
 
func (f foo) Foo3() {
    fmt.Println(«Foo3() here»)
}
 
func NewFoo() Fooer {
    return &Foo{}
}

Тогда код из другого пакета может использовать NewFoo() и получить доступ к интерфейсу Fooer реализованному внутренним типом foo :

1
2
3
4
5
6
7
f := NewFoo()
 
f.Foo1()
 
f.Foo2()
 
f.Foo3()

Наследование или наследование всегда были спорным вопросом. Существует много проблем с наследованием реализации (в отличие от наследования интерфейса). Множественное наследование, реализованное в C ++, Python и других языках, страдает от проблемы смертельного алмаза смерти , но даже одиночное наследование не является пикником для хрупкой проблемы базового класса .

Современные языки и объектно-ориентированное мышление теперь предпочитают композицию наследованию. Go принимает это близко к сердцу и не имеет никакой иерархии типов. Это позволяет вам делиться деталями реализации через композицию. Но Go, в очень странном повороте (который, вероятно, возник из-за прагматических проблем), позволяет анонимную композицию посредством встраивания.

Для всех намерений и целей составление путем встраивания анонимного типа эквивалентно наследованию реализации. Встроенная структура так же хрупка, как и базовый класс. Вы также можете встроить интерфейс, который эквивалентен наследованию от интерфейса в таких языках, как Java или C ++. Это может даже привести к ошибке времени выполнения, которая не обнаруживается во время компиляции, если тип встраивания не реализует все методы интерфейса.

Здесь SuperFoo встраивает интерфейс Fooer, но не реализует его методы. Компилятор Go с радостью позволит вам создать новый SuperFoo и вызвать методы Fooer, но, очевидно, потерпит неудачу во время выполнения. Это компилирует:

1
2
3
4
5
6
7
type SuperFooer struct {
  Fooer
}
 
func main() {
  s := SuperFooer{}
  s.Foo2()

Запуск этой программы приводит к панике:

01
02
03
04
05
06
07
08
09
10
11
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x28 pc=0x2a78]
 
goroutine 1 [running]:
panic(0xde180, 0xc82000a0d0)
  /usr/local/Cellar/go/1.6/libexec/src/runtime/panic.go:464 +0x3e6
main.main()
  /Users/gigi/Documents/dev/go/src/github.com/oop_test/main.go:104 +0x48
exit status 2
 
Process finished with exit code 1

Полиморфизм — это сущность объектно-ориентированного программирования: способность обрабатывать объекты разных типов равномерно, если они придерживаются одного и того же интерфейса. Интерфейсы Go предоставляют эту возможность очень прямым и интуитивно понятным способом.

Вот подробный пример, когда несколько существ (и дверь!), Которые реализуют интерфейс Dumper, создаются и сохраняются в срезе, а затем для каждого из них вызывается метод Dump() . Вы также заметите разные стили создания объектов.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package main
 
import «fmt»
 
type Creature struct {
  Name string
  Real bool
}
 
func Dump(c*Creature) {
  fmt.Printf(«Name: ‘%s’, Real: %t\n», c.Name, c.Real)
}
 
func (c Creature) Dump() {
  fmt.Printf(«Name: ‘%s’, Real: %t\n», c.Name, c.Real)
}
 
type FlyingCreature struct {
  Creature
  WingSpan int
}
 
func (fc FlyingCreature) Dump() {
  fmt.Printf(«Name: ‘%s’, Real: %t, WingSpan: %d\n»,
    fc.Name,
    fc.Real,
    fc.WingSpan)
}
 
type Unicorn struct {
  Creature
}
 
type Dragon struct {
  FlyingCreature
}
 
type Pterodactyl struct {
  FlyingCreature
}
 
func NewPterodactyl(wingSpan int) *Pterodactyl {
  pet := &Pterodactyl{
    FlyingCreature{
      Creature{
        «Pterodactyl»,
        true,
      },
      wingSpan,
    },
  }
  return pet
}
 
type Dumper interface {
  Dump()
}
 
type Door struct {
  Thickness int
  Color string
}
 
func (d Door) Dump() {
  fmt.Printf(«Door => Thickness: %d, Color: %s», d.Thickness, d.Color)
}
 
func main() {
  creature := &Creature{
    «some creature»,
    false,
  }
 
  uni := Unicorn{
    Creature{
      «Unicorn»,
      false,
    },
  }
 
  pet1 := &Pterodactyl{
    FlyingCreature{
      Creature{
        «Pterodactyl»,
        true,
      },
      5,
    },
  }
 
  pet2 := NewPterodactyl(8)
 
  door := &Door{3, «red»}
 
  Dump(creature)
  creature.Dump()
  uni.Dump()
  pet1.Dump()
  pet2.Dump()
 
  creatures := []Creature{
    *creature,
    uni.Creature,
    pet1.Creature,
    pet2.Creature}
  fmt.Println(«Dump() through Creature embedded type»)
  for _, creature := range creatures {
    creature.Dump()
  }
 
  dumpers := []Dumper{creature, uni, pet1, pet2, door}
  fmt.Println(«Dump() through Dumper interface»)
  for _, dumper := range dumpers {
    dumper.Dump()
  }
}

Go — добросовестный объектно-ориентированный язык программирования. Он позволяет объектно-ориентированное моделирование и продвигает лучшую практику использования интерфейсов вместо конкретных иерархий типов. Go сделал несколько необычных синтаксических решений, но в целом работа с типами, методами и интерфейсами кажется простой, легкой и естественной.

Встраивание не очень чистое, но, по-видимому, прагматизм был на работе, и вложение было обеспечено вместо только композиции по имени.