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

Что такое инкапсуляция?

Одна из основных целей ООП - инкапсуляция или сокрытие данных. Почему это важно? Без сокрытия данных к полям структуры можно получить доступ напрямую, и полю можно присвоить любое значение, допустимое для типа данных этого поля. Например, с учетом этого определения структуры:

type Person struct {
  name string
  age int
}

С помощью этого определения мы можем присвоить возрасту человека любое действительное целое число:

p1 := Person{“Jane Doe”, 823}

Ого! Если мы не живем в библейские времена, люди не могут дожить до 823 лет. Но целочисленная переменная определенно может содержать 823. Очевидно, нам нужен какой-то процесс для управления доступом к полям наших структур.

Большинство языков программирования ООП предоставляют механизмы доступа, позволяющие скрыть поля класса, предоставляя им частный доступ, то есть к полю можно получить доступ только внутри определения класса. В Go этого нет, поэтому нам нужно найти другой способ.

Другой способ - экспорт имени пакета.

Как пакеты Go экспортируют имена

Когда вы создаете пакет, вы можете контролировать, какие функции и другие объекты будут отображаться при импорте пакета. Для этого нужно использовать заглавные буквы в именах сущностей, которые нужно экспортировать в программу импорта. У меня нет места в этой статье, чтобы обсуждать, как создавать пакеты, но эта статья прекрасно справляется со своей задачей.

Чтобы продемонстрировать, вот определение пакета, экспортирующего функции для работы с геометрическими точками:

package point
type Point struct {
  x, y float64
}
func (p *Point) Create(newx, newy float64) {
  p.x = newx
  p.y = newy
}
func (p *Point) X() float64 {
  return p.x
}
func (p *Point) Y() float64 {
  return p.y
}
func (p *Point) MoveTo(newx, newy float64) {
  p.x = newx
  p.y = newy
}

Давайте посмотрим на это определение. Поля объявлены в определении структуры. Метод Create используется как конструктор для инициализации новой структуры Point. Методы X() и Y() похожи на методы получения в таком языке, как Java, для возврата значений, хранящихся в полях x и y.

Что вы должны заметить в этом определении, так это то, что все имена методов пишутся с заглавной буквы, то есть они экспортируются в программу импорта, но имена полей не пишутся с заглавной буквы. Из-за этого вы не можете напрямую получить доступ к этим полям, и попытка сделать это приведет к панике.

Это может показаться радикальным способом реализации инкапсуляции, если вы знакомы с другими языками ООП, но использование пакетов также дает еще одну причину для того, чтобы сделать ваши программы более модульными, разделив большинство определений структур.

Интерфейсы Go

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

Чтобы продемонстрировать, как интерфейсы работают в Go, я разработаю набор структур, реализующих геометрические формы (правда, без реальной графики). Эти формы будут основаны на интерфейсе shape, который определяет, какие типы поведения должна иметь форма, но без явного определения этого поведения.

Я начну с определения интерфейса shape. Как интерфейс он абстрактный и не может быть реализован напрямую. Но его можно использовать как основу для определения конкретных типов фигур. Вот определение:

type shape interface {
  X() float64
  Y() float64
  Draw()
  MoveTo(newx, newy float64)
  Radius() float64
}

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

Теперь мы можем определить конкретную фигуру как структуру:

type circle struct {
  x, y float64
  radius float64
}

Структура определяет поля, в которых хранятся данные для типа. Теперь мы можем объединить их, создав объект, реализующий интерфейс:

var circ shape
circ = Circle(1,2,5)

Теперь вы можете увидеть, как реализован интерфейс. Сначала вы объявляете переменную типа интерфейса. Затем вы назначаете эту переменную с помощью вызова структурного литерала, чтобы завершить реализацию.

Этот код вышел из строя, так как мне все еще нужно определить все методы, указанные в интерфейсе. Вот эти определения:

func (c circle) X() float64 {
  return c.x
}
func (c circle) Y() float64 {
  return c.y
}
func (c circle) Radius() float64 {
  return c.radius
}
func (c circle) MoveTo(newx, newy float64) {
  c.x = newx
  c.y = newy
}
func (c circle) Draw() {
  fmt.Printf("Drawing a circle at %.0f,
             %.0f with radius of %.0f.",
             c.X(), c.Y(), c.Radius())
}

Эти определения методов выглядят так же, как если бы вы строили свой код только на основе определения структуры, но есть последствия, если вы не определяете все методы, указанные в интерфейсе.

Вот пример того, что произойдет, если мы закомментируем определение функции Radius:

c:\Go\bin\shapes>go run shapeprog.go
# command-line-arguments
.\shapeprog.go:37:29: c.Radius undefined (type circle has no field or method Radius, but does have radius)
.\shapeprog.go:42:8: cannot use circle literal (type circle) as type shape in assignment:
circle does not implement shape (missing Radius method)

Последняя строчка паники наиболее поучительна. Он сообщает мне, что моя circle struct не реализует интерфейс, потому что метод Radius не определен. Это подтверждает идею о том, что интерфейс - это контракт, в котором говорится, что любая структура, которая решает реализовать интерфейс, должна определять все методы, указанные в интерфейсе.

Теперь я объединю весь этот код в одну программу, чтобы было легче увидеть, что происходит:

package main
import "fmt"
type shape interface {
  X() float64
  Y() float64
  Draw()
  MoveTo(newx, newy float64)
  Radius() float64
}
type circle struct {
  x, y float64
  radius float64
}
func (c circle) X() float64 {
  return c.x
}
func (c circle) Y() float64 {
  return c.y
}
func (c circle) Radius() float64 {
  return c.radius
}
func (c circle) MoveTo(newx, newy float64) {
  c.x = newx
  c.y = newy
}
func (c circle) Draw() {
  fmt.Printf("Drawing a circle at %.0f,
             %.0f with radius of %.0f.",
             c.X(), c.Y(), c.Radius())
}
func main() {
  var circ shape
  circ = circle{1,2,5}
  fmt.Println("circle radius: ",circ.Radius())
  circ.Draw()
}

Я настоятельно рекомендую вам расширить мой пример, включив в него другие фигуры, чтобы лучше понять, как работают интерфейсы в Go.

Иди и ООП

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

Спасибо, что прочитали эту статью, и пишите мне с комментариями и предложениями.