Пользователь
121,5
рейтинг
13 ноября 2015 в 18:52

Разработка → Пример решения типичной ООП задачи на языке Go tutorial

ООП*, Go*
Недавно попалась на глаза занимательная статья с аналогичным названием о языке Haskell. Автор предлагал читателю проследить за мыслью программиста, решающего типичную ООП задачу но в Хаскеле. Помимо очевидной пользы расширения представлений читателей о том, что ООП — это отнюдь не «классы» и «наследование», подобные статьи полезны для понимания того, как правильно пользоваться языком. Предлагаю читателю решить ту же самую задачу, но на языке Go, в котором ООП тоже реализован непривычно.

Задача


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

Формат вывода информации, на примере прямоугольника и круга, должен быть вот таким:
paint rectangle, Rect {left = 10, top = 20, right = 600, bottom = 400}
paint circle, radius=150 and centre=(50,300)

Кроме того, примитивы нужно уметь объединить в однородный список.

Решение


Структуры и свойства

Начнем с очевидного — с объявления примитивов и их свойств. За свойства в Go отвечают структуры, поэтому просто объявим нужные поля для примитивов Rectangle и Circle:
type Rectangle struct {
	Left, Right, Top, Bottom int64
}

type Circle struct {
	X, Y, Radius int64
}

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

Названия полей и методов будем писать с большой буквы, так как это не библиотека, а исполняемая программа, и видимость за пределами пакета нам не важна (в Go название с большой буквы — это аналог public, с маленькой — private).

Интерфейсы и поведение

Далее, по определению оригинальной задачи, примитивы должны уметь отдавать информацию о себе в определенном формате и отдавать значение площади примитива. Как же мы сделаем это в Go, если у нас нет классов и «нормального ООП»?

Тут в Go даже не приходится гадать, поскольку в языке очень четко разделено определение «свойств» и «поведения». Свойства — это структуры, поведение — это интерфейсы. Этот простой и мощный концепт сразу же дает нам ответ, что делать дальше. Определяем нужный интерфейс с нужными методами:
type Figure interface {
	Say() string
	Square() float64
}

Выбор имени интерфейса (Figure) тут продиктован оригинальным примером и задачей, но обычно в Go интерфейсы, особенно с одним методом, называют с суффиксом -er — Reader, Painter, Stringer и так далее. По идее, имя должно помогать понять назначение интерфейса и отражать его поведение. Но в данном случае Figure достаточно неплохо подходит и описывает сущность «фигуры» или «графического примитива».

Методы

Теперь, чтобы типы Rectangle и Circle стали «фигурами», они должны удовлетворять интерфейсу Figure, тоесть для них должны быть определены методы Say и Square. Давайте их напишем:
func (r Rectangle) Say() string {
	return fmt.Sprintf("rectangle, Rect {left=%d,top=%d,right=%d,bottom=%d)", r.Left, r.Top, r.Right, r.Bottom)
}

func (r Rectangle) Square() float64 {
	return math.Abs(float64((r.Right - r.Left) * (r.Top - r.Bottom)))
}

func (c Circle) Say() string {
	return fmt.Sprintf("circle, radius=%d and centre=(%d,%d)", c.Radius, c.X, c.Y)
}

func (c Circle) Square() float64 {
	return math.Pi * math.Pow(float64(c.Radius), 2)
}

На что здесь стоит обратить внимание — на ресивер метода, который может быть значением (как сейчас — «c Circle»), а может быть указателем "(c *Circle)". Общее правило тут такое — если метод должен изменять значение c или если Circle — большущщая структура, занимающая много места в памяти — тогда использовать указатель. В остальных случаях, будет дешевле и эффективней передавать значение в качестве ресивера метода.

Более опытные гоферы заметят, что метод Say в точности похож на стандартный интерфейс Stringer, который используется в стандартной библиотеке, пакетом fmt в том числе. Поэтому можно переименовать Say в String, убрать этот метод вообще из интерфейса Figure и дальше просто передавать объект данного типа в функции fmt для вывода, но пока что оставим так, для большей ясности и схожести с оригинальным решением.

Конструкторы

Собственно, все — теперь можно создавать структуру Rectangle или Circle, инициализировать ее значения, сохранять в слайсе (динамический массив в Go) типа []Figure и передавать функции, принимающей Figure и вызывающей методы Say или Square для дальнейшей работы с нашими графическими примитивами. К примеру, вот так:
func main() {
	figures := []Figure{
		NewRectangle(10, 20, 600, 400),
		NewCircle(50, 300, 150),
	}
	for _, figure := range figures {
		fmt.Println(figure.Say())
	}
}

func NewRectangle(left, top, right, bottom int64) *Rectangle {
	return &Rectangle{
		Left:   left,
		Top:    top,
		Right:  right,
		Bottom: bottom,
	}
}

func NewCircle(x, y, radius int64) *Circle {
	return &Circle{
		X:      x,
		Y:      y,
		Radius: radius,
	}
}

Методы NewRectangle и NewCircle — просто функции-конструкторы, которые создают новые значения нужного типа, инициализируя их. Это обычная практика в Go, такие конструкторы нередко еще могут возвращать ошибку, если конструктор делает более сложные вещи, тогда сигнатура выглядит как-нибудь так:
func NewCircle(x, y, radius int64) (*Circle, error) {...}

Также вы можете встретить сигнатуры с приставкой Must вместо New — MustCircle(x, y, radius int64) *Circle — обычно это означает, что функция выбросит панику, в случае ошибки.

Углубляемся в тему

Наблюдательный читатель может заметить, что мы кладем в массив фигур ([]Figure) переменные типов *Rectangle и *Circle (то есть, указатель на Rectangle и указатель на Circle), хотя методы мы таки определили на значение, а не на указатель (func (c Circle) Say() string). Но это правильный код, так Go работает с ресиверами методов, упрощая программистам жизнь — если тип реализует интерфейс, то «указатель на этот тип» тоже его реализует. Ведь логично, не так ли? Но чтобы не заставлять программиста лишний раз разыменовывать указатель, чтобы сказать компилятору «вызови метод» — Go компилятор сделает это сам. А вот обратную сторону — что тоже очевидно — такое не сработает. Если интерфейсный метод реализован для «указателя на тип», то вызов метода от переменной не-указателя вернет ошибку компиляции.

Чтобы вызвать метод Say у каждого примитива, мы просто проходимся по слайсу с помощью ключевого слова range и печатаем вывод метода Say(). Важно понимать, что каждая переменная интерфейсного типа Figure содержит внутри информацию о «конкретном» типе. figure в цикле всегда является типом Figure, и, одновременно, либо Rectangle, либо Circle. Это справедливо для всех случаев, когда вы работает с интерфейсными типами, даже с пустыми интерфейсами (interface{}).

Усложняем код


Далее автор усложняет задачу, добавляя новый примитив «закругленный прямоугольник» — RoundRectangle. Это, по сути, тот же примитив Rectangle, но с дополнительным свойством «радиус закругления». При этом, чтобы избежать дубликации кода, мы должны как-то переиспользовать уже готовый код Rectangle.

Опять же, Go дает абсолютно четкий ответ, как это делать — никаких «множественных путей сделать это» тут нет. И этот ответ — embedding, или «встраивание» одного типа в другой. Вот так:
type RoundRectangle struct {
	Rectangle
	RoundRadius int64
}

Мы определяем новый тип-структуру, который уже содержит все свойства типа Rectangle плюс одно новое — RoundRadius. Более того, RoundRectangle уже автоматически удовлетворяет интерфейс Figure, так как его удовлетворяет встроенный Rectangle. Но мы можем переопределять функции, и вызывать функции встроенного типа напрямую, если нужно. Вот как это выглядит:
func NewRoundRectangle(left, top, right, bottom, round int64) *RoundRectangle {
	return &RoundRectangle{
		*NewRectangle(left, top, right, bottom),
		round,
	}
}

func (r RoundRectangle) Say() string {
	return fmt.Sprintf("round rectangle, %s and roundRadius=%d", r.Rectangle.Say(), r.RoundRadius)
}

Конструктор типа использует конструктор NewRectangle, при этом разыменовывая указатель (так как мы встраиваем Rectangle, а не указатель на Rectangle), а метод Say вызывает r.Rectangle.Say(), чтобы вывод был точно таким же как и для Rectangle, без дубликации кода.

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

Теперь просто добавим в слайс новый примитив:
figures := []Figure{
		NewRectangle(10, 20, 600, 400),
		NewCircle(50, 300, 150),
		NewRoundRectangle(30, 40, 500, 200, 5),
}

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

Финальные правки


Хотя этот код и является синтетическим примером, но опишу пару моментов, которые я бы делал дальше. Первым делом — я напишу комментарии ко всем методам, даже к конструкторам. Последнее, конечно, не обязательно, но мне нравится идея того, что достаточно написать по одной строчке, чтобы получить документацию ко всему пакету с помощью go doc, даже если она пока не нужна, и вообще, это не библиотека, а запускаемая программа. Но, если в будущем подобный код будет выделен в отдельный пакет-библиотеку, то мы автоматом получим документированный пакет. Пусть даже пока что описания банальные, но мне не сложно потратить 5 секунд на написание одной строки текста, зато есть чувство «полноты» кода, да и линтеры (go vet) не будут ругаться, что тоже приятно.

Далее, логичным выглядит разнести код на несколько отдельных файлов — определение интерфейса и main() оставить в main.go, а для каждого примитива и его функций создать отдельные файлы — circle.go, rectangle.go и roundrectangle.go. Описание интерфейса, впрочем, тоже можно вынести в отдельный файл.

Финальным штрихом будет прогонка через GoMetaLinter — это пакет, запускающий параллельно все линтеры и статические анализаторы кода, которые умеют много чего ловить и подсказывать, позволяя делать код еще лучше, чище и читабельней. Если gometalinter не вывел сообщений — отлично, код достаточно чист.

Полный код тут
main.go:
package main

import "fmt"

// Figure describes graphical primitive, which can Say
// own information and return it's Square.
type Figure interface {
	Say() string
	Square() float64
}

func main() {
	figures := []Figure{
		NewRectangle(10, 20, 600, 400),
		NewCircle(50, 300, 150),
		NewRoundRectangle(30, 40, 500, 200, 5),
	}
	for _, figure := range figures {
		fmt.Println(figure.Say())
	}
}

rectangle.go:
package main

import (
	"fmt"
	"math"
)

// Rectangle defines graphical primitive for drawing rectangles.
type Rectangle struct {
	Left, Right, Top, Bottom int64
}

// NewRectangle inits new Rectangle.
func NewRectangle(left, top, right, bottom int64) *Rectangle {
	return &Rectangle{
		Left:   left,
		Top:    top,
		Right:  right,
		Bottom: bottom,
	}
}

// Say returns rectangle details in special format. Implements Figure.
func (r Rectangle) Say() string {
	return fmt.Sprintf("rectangle, Rect {left=%d,top=%d,right=%d,bottom=%d)", r.Left, r.Top, r.Right, r.Bottom)
}

// Square returns square of the rectangle. Implements Figure.
func (r Rectangle) Square() float64 {
	return math.Abs(float64((r.Right - r.Left) * (r.Top - r.Bottom)))
}

circle.go:
package main

import (
	"fmt"
	"math"
)

// Circle defines graphical primitive for drawing circles.
type Circle struct {
	X, Y, Radius int64
}

// NewCircle inits new Circle.
func NewCircle(x, y, radius int64) *Circle {
	return &Circle{
		X:      x,
		Y:      y,
		Radius: radius,
	}
}

// Say returns circle details in special format. Implements Figure.
func (c Circle) Say() string {
	return fmt.Sprintf("circle, radius=%d and centre=(%d,%d)", c.Radius, c.X, c.Y)
}

// Square returns square of the circle. Implements Figure.
func (c Circle) Square() float64 {
	return math.Pi * math.Pow(float64(c.Radius), 2)
}

roundrectangle.go:
package main

import "fmt"

// RoundRectangle defines graphical primitive for drawing rounded rectangles.
type RoundRectangle struct {
	Rectangle
	RoundRadius int64
}

// NewRoundRectangle inits new Round Rectangle and underlying Rectangle.
func NewRoundRectangle(left, top, right, bottom, round int64) *RoundRectangle {
	return &RoundRectangle{
		*NewRectangle(left, top, right, bottom),
		round,
	}
}

// Say returns round rectangle details in special format. Implements Figure.
func (r RoundRectangle) Say() string {
	return fmt.Sprintf("round rectangle, %s and roundRadius=%d", r.Rectangle.Say(), r.RoundRadius)
}


Выводы


Надеюсь, статья помогла проследить за ходом мысли, обратить внимание на некоторые аспекты Go. Go именно такой — прямолинейный и не способствующий трате времени на размышления, какими бы фичами решить ту или иную задачу. Его простота и минимализм заключается в том, чтобы предоставлять как раз только то, что необходимо для решения практических задач. Никаких ЭкзистенциальныхКвантизаций, только тщательно отобранный набор строительных блоков в духе философии Unix.

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

И, конечно, было бы интересно увидеть ответы-продолжения на оригинальную статью на других новых и не очень языках. Мне, например, было жутко интересно «подсмотреть» за ходом мысли в оригинальной статье, и, я более чем уверен, что это из лучших способов учиться и осваивать новые вещи. Отдельное спасибо автору оригинального материала (@KolodeznyDiver).
divan0 @divan0
карма
128,0
рейтинг 121,5
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (87)

  • +2
    За наводку на GoMetaLinter отдельное спасибо!
  • +2
    Хочу спросить совета по поводу повторного использования кода в Go. Делаю парсер сайтов. Создал структуру Source в ней свойство baseURL(строка шаблона, в которую нужно подставлять разные значения get параметров) и метод parseURL(), который из baseURL собирает валидные урлы. Также создал несколько структур для разных сайтов и с разным набором параметров. Идея в том, что «text/template» умеет парсить строки, по именам свойств структуры, поэтому скармливать ему нужно baseURL, для каждого сайта свою и сам объект сайта и на выходе получать валидный url. Как это организовать, не переопределяя для каждого сайта parseURL()? Надеюсь, я понятно изложил свою мысль. На питоне очень просто это реализовать, а вот Go ставит меня в тупик.
    П.С. Также спасибо за GoMetaLinter, хорошо показал типичные ошибки, отличное решение
    • 0
      Я не очень понял проблему по описанию.
      Могу посоветовать зайти к нам в слак- чат, и там спросить — будет продуктивнее :)
      • –1
        К сожалению меня нету в слаке, я выложил все на github, буду благодарен, если вы укажете, правильно ли я решил проблему, или может, есть более елегантный способ
        https://github.com/Crandel/flatParser/blob/master/parser.go
        • +1
          Слак — это не очередной мессенджер, в котором ты можешь «быть или не быть», — это чат комната с обязательной регистрацией.
  • 0
    Вот такие статьи по Go и нужны. Ничего против не скажешь.

    ООП — это отнюдь не «классы» и «наследование»

    Как раз недавно в комментариях в статье про Swift вылезла эта тема. Go и без привычного наследования является ООП языком, а структуры Go вообще не вижу причин не называть классами. Да, есть явно разделение на интерфейс и свойства, но все вместе это дает вполне себе классы — мы создаем объекты, которые имеют методы для работы с ними. Вроде все как у всех. Ну и полиморфизм, что важно, на месте. После протоколов Obj-C и интерфейсов C# все это выглядит вполне знакомо.
    • +6
      С таким же успехом можно и Haskell приписать к ООП -))
    • +2
      Вот такие статьи по Go и нужны. Ничего против не скажешь.

      Присоединяюсь! Таким, как эта статья, хотелось бы видеть каждое краткое введение в язык.
    • –1
      Go и без привычного наследования является ООП языком, а структуры Go вообще не вижу причин не называть классами.
      Серьезно? Или вы спать будете плохо, если окажется что Go не объектно-ориентированный язык? Так вот, в Go нет ООП. В нем нет ни классов, ни наследования, ни прототипов, ни полиморфизма. Go – не объектно-ориентированный язык. Извините.
      • 0
        • +3
          Существуют более-менее общепринятые признаки языка с поддержкой ООП. И Go не подходит под эти признаки. Это не значит, что на Go нельзя писать объектно-ориентированный код. Но с таким же успехом объектно-ориентированный код можно писать и на C, Haskell и Rust.

          И да, нет ничего разумного в том, чтобы придумывать собственные определения для уже существующих терминов. Уж очень получается похоже на «Я считаю Go объектно-ориентированным, потому что мне так очень хочется».
          • 0
            • +1
              И что? Такие ответы в faq пишутся в основном для тех, кто считает что если язык не объектно-ориентированный, то на него даже смотреть не стоит. «Go можно считать объектно-ориентированным языком, потому что у нас есть типы и функции, правда нет иерархии классов, и классов тоже нет». С таким успехом Pascal — тоже объектно-ориентированный язык.
              • +1
                Просто FYI, что этот вопрос уже даже рассмотрен в офф. документации, и не стоит за это минусовать мой коммент.
                И там первым же предложением пишут «Yes and no» с пояснением.
                • +2
                  Я не против, но если вы публикуете ссылку на что-то, предполагается, что вы полностью согласны с тем, что там написано.

                  В ответе написано, что на Go можно писать объектно-ориентированный код. С этим я полностью согласен. Но на прямой вопрос «является ли Go объектно-ориентированным языком» дан ответ «и да и нет», что лично я считаю лукавством, потому что правильный ответ «нет». И причины этого лукавства находятся скорее в плоскости маркетинга, чем в плоскости computer science.
                  • –1
                    В этом я с вами согласен, но вот смотрите как выходит — на Go можно писать объектно-ориентированный код (т.е. в языке есть механизмы для реализации соотв. вещей или их аналогов). Но при этом Go не объектно-ориентированный язык в классическом смысле. Как тут еще можно сказать, кроме «и да и нет»?
                    • +3
                      Если бы ответ писал я, я бы написал так: «Нет, но это не мешает вам писать на Go в объектно-ориентированном стиле. Для этого в Go существуют такие-то механизмы...» и дальше по тексту.

                      В целом, я придираюсь к ответу только потому, что из-за его формулировки люди могут сделать неправильные выводы. А с сутью ответа я полностью согласен.
          • +2
            Если что-то общепринято, оно не становится по умолчанию правильным. Алан Кей, создатель ООП, как-то сказал:
            «Я придумал термин «объектно-ориентированный», и я уверяю вас, что не имел в виду C++».

            Так кто из нас придумывает собственные определения существующих терминов? Ваши придирки напоминают мне споры о том, есть ли классы в javascript'e. От того, что вместо «type Circle struct» будет написано «class Circle», язык не станет вдруг более оопэшным. И наоборот.

            И да, нет ничего разумного в том, чтобы следовать букве определения, игнорируя его дух. Уж очень получается похоже на человека по Платону.
            • 0
              Ну да, ведь когда Алан Кей придумал термин «ООП», он на самом деле имел ввиду Smalltalk. :)

              Вы не вполне правы по поводу «моих придирок». Я просто хочу докопатся до сути. Мне нет дела до конкретных названий, будет там «class» или «struct», «interface» или «trait» или «abstract class». И я, так же как и вы, готов следовать «духу» а не «букве».

              Но есть определенные вещи, которые отличают ООП-подход от просто хорошего дизайна структурного кода. И самая главная такая вещь – это возможность наследования (будь оно прототипным или нет), потому что такие понятия как «полифорфизм», «инкапсуляция» и «абстрактные сущности» не являются эксклюзивными для ООП.
              • 0
                Полиморфизм через подтипирование (subtype polymorphism) встречается в не-ООП языках?
                • 0
                  Википедия определяет «subtyping» и «subtype polymorphism» как одно и тоже. Что вы имете ввиду?
                  • 0
                    Извините, я не понял к чему это утверждение, но повторю вопрос, котрый видимо был непонят. Я спрашиваю частный вид полиморфизма, а именно полиморфизм через подтипирование, встречается ли где либо кроме ООП языков.
                    • 0
                      «Полиморфизм через подтипирование» — это тоже самое что и «наследование». Я не знаю такие не-ООП языки программирования, в которых можно встретить наследование. Я ответил на ваш вопрос?
                      • 0
                        Хорошо, в данном случае этот вид полиморфизма все же является понятием эксклюзивным для ООП :)
                        • 0
                          Что никак не противоречит моему изначальному утверждению: «полиморфизм не является эксклюзивным понятием для ООП».

                          Потому что есть еще параметрический полиморфизм, ad-hoc полиморфизм и т.д., которые не специфичны только для ООП.

                          В то же время я сказал, что «есть определенные вещи, которые отличают ООП-подход от просто хорошего дизайна структурного кода. И самая главная такая вещь – это возможность наследования», что означает именно то, о чем вы говорите.
                          • 0
                            OK принято :)
              • 0
                Если судить по цитатам Алана, то больше всего под его описание подходит Erlang (ну, а теперь и Go). :)

                Прочитав ваш комментарий ниже, я понял что, возможно, не слишком хорошо понимаю ооп в «общеупотребительном» смысле. Что композиция, что наследование — в некотором смысле это схожие подходы, которые применяются для решения схожих или даже одинаковых проблем. Почему наследование считается ооп-инструментом, а композиция — нет, для меня загадка. И в том и в другом случае у нас есть структура/класс, которая, грубо говоря, расширяется за счет одной или нескольких других структур.
            • +1
              К слову, Go предлагает заменить наследование композицией. Но композиция – это как раз тот инструмент, который используется в структурном подходе для написания расширяемого кода. Так что врядли это можно назвать следованием «духу» ООП.
      • –1
        Вы с языком то знакомы? Полиморфизм достигается через интерфейсы, при этом голыми структурами вообще не принято оперировать. fmt.print функции вкупе с интерфейсом Stringer отлично это используют. Классы, а что есть классы, что их там нет? Наследование? Embedding позволяет «наследовать» все методы, интерфейсы и поля исходного типа, что из-за этих самых интерфейсов позволяет получить в этом случае полиморфизм. При этом есть приватные и публичные поля и методы. Все как по ООП. Мне понятно, что вы цепляетесь к определениям, которые заучили в школе и универе. И Go действительно не ложится на них идеально, но концепции, которые в них заложены, он реализует, а значит и может называться ООП языком. Просто это не C# и Java, вот и все.

        Спать я буду спокойно, но меня удивляет такая вот вера в непоколебимость определений ООП, за которыми стоят не концепции, а не какие-то конкретные реализации в языках. Мне вот хочется называть его ООП, потому что таково мое мнение, я вижу концепции ООП в таком свете. Вам так не кажется — можете придумать для Go какие-то другие определения. Не суть важно.
        • +4
          «Полиморфизм достигается через интерфейсы.» Наверное все-таки не полиморфизм, а dynamic dispatch? Я имел ввиду именно параметрический полиморфизм (или generics).

          У вас в голове как-будто смешаны в одну кучу понятия наследования, полиморфизма и композиции (embedding). Можно ли их заменять друг другом? Можно, но это уже не будет наследованиям. Выше я уже писал, что композиция появлиась за долго до того, что было придумано само понятие ООП и использовалось повсевместно в структурном коде. Так что по-поводу «Все как в ООП» вы строго не правы.

          Я не цепляюсь к определениям. Я еще своей головой умею думать. И я хочу что бы понятие ООП было именно понятием, а не маркетинговым buzz word и ярлыком, которой вешают на все что должно блестесть. Вы же наоборот размываете понятие ООП до такой степени, что вообще любой язык можно назвать ООП. В чем тогда вообще смысл такого понятия? Чтобы блестело?
          • –1
            Да, я размываю определение ООП до степени концепций, а не конкретных реализаций в любимых языках. Смысл этого? Не знаю, просто дискуссия. Поднята тема ООП в Go, я высказал свое мнение на этот счет. Если почитаете интернеты, то эта тема частенько поднимается. Люди пытаются найти в Go ООП, которое они видели в других языках. В ответ же звучит — Go реализует концепции ООП, но не так, как это делают другие языки. В конечном итоге важны именно концепции и их практическая польза, а не четкость понятий, которые живут только в академических кругах. Вам это сильно не нравится и вы зачем-то мне пытаетесь что-то доказывать. Еще раз, мне не важны определения. Мне важные концепции и в вопросе ООП, которые подняла статья, я имею уже упомянутое мнение.

            Что до параметрического полиморфизма. Пожалуйста, кладите в список интерфейсы и вы получите то, что вам надо. Я не понимаю, вам так важно, чтобы все выглядело как в каких-то языках или все таки удовлетворялись идеи определенные? Основная идея полиморфизма — положил кучу объектов не важно какого типа, но реализующих единый интерфейс, в один массив. Все, ты работаешь с ними единым образом. Это полиморфизм и никакие generics тут не нужны. Objective-C как-то прекрасно живет на позиции ООП языка (даже частенько называет более православным, чем С++), но в нем generics нет (недавно появились всего лишь аннотации для компилятора). Это не говоря о том, что вы зачем-то ограничились одним узким примером полиморфизма, да еще дополнительно ограничили его до дженериков.
            • +1
              У меня нет «любимых» языков или привычных для меня реализаций, я довольно много совершенно разных подходов видел. Мне все равно, как синтаксически это будет выглядить. У меня нет сомнений в том, что на Go можно писать объектно-ориентированный код. Впрочем, как и на любом другом языке. Делает ли это язык объектно-ориентированным? Нет.

              Если вам не важны определения, то что вы мне, простите, хотите доказать? Мне важны определения. Давайте на этом и закончим.

              Мне вот хочется называть его ООП, потому что таково мое мнение, я вижу концепции ООП в таком свете.
              Это такой шедевр, что я даже не знаю, что вам на это ответить. Вы точно правильно профессию выбрали? Вы инженер или литературный критик, чтобы использовать аргумент в стиле «я художник, я так вижу»?
        • 0
          «Полиморфизм достигается через интерфейсы, при этом голыми структурами вообще не принято оперировать.»

          Это кстати интересно. Получается что статическая типизация коту под хвост?
          • 0
            Но оперируем же мы не пустыми интерфейсами (по сути, аналог object), а конкретными типами. Статическая типизация. Просто как-то принято наружу из библиотек выставлять не структуры, а интерфейсы. Вы открыли сокет — вам наружу интерфейс, статический определенный тип. Зачем вам знать, что за структура за ним стоит? Кажется испокон веков такое используется в ООП языках и когда я начал писать на Go мне сразу вспомнилось — да это же как интерфейсы и протоколы в C# и obj-C соответственно. Прям вспоминаются все эти умные книжки, где талдычат — везде наружу выставляем интерфейсы. Ибо слабое сцепление и прочие умные концепции.
            • 0
              Чем это принципиально отличается от PyObject или NSObject? Это динамическая типизация, а не статическая (пусть и строгая).

              «Выставлять наружу только интерфейсы» оправдано только тогда, когда язык не поддерживает параметрический полиморфизм. С параметрическим полиморфизмом включается механизм мономорфизации, позволяющий заменить dynamic dispatch на static dispatch. Соответственно, прятать все за интерфейсы нет необходимости.
  • +2
    у меня возник вопрос. даже два :)

    1) Как принято в большом go проекте быстро понимать реализует ли «класс» какой-то интерфейс? Смотреть все 10-20 методов класса, пытаясь найти все 5, которые должны быть в интерфейсе?
    2) как работают с сериализацией в го? где можно тонкости стандартных подходов посмотреть?
    • +1
      По первому можно ответить словами самих авторов. В Go принято и всячески рекомендуется делать интерфейсы из минимального количества методов вплоть до одного. Тогда проблема уходит само собой. С другой стороны, такой проблем по идее и не должно происходить. Есть метод, есть аргументы — сразу понятно, что он хочет. Библиотеки обычно возвращают интерфейсы, а не голые структуры, что, опять же, снимает эту проблему. А так да, только смотреть на список методов. Иных механизмов язык вроде бы не предоставляет, хотя помощь IDE здесь была бы все таки уместна.
    • +1
      1. godoc со специальным ключиком сгенерирует документацию по коду, которая по каждому интерфейсу напишет список реализующих его типов. С блекджеком и гиперссылками.

      2. Сериализацию можно или делать из коробки, дописывая аннотации к полям структур, как они должны называться на вводе-выводе, а потом используя стандартные пакеты encoding/json, encoding/xml, или использовать protobuf со своими плюсами и минусами. Это что часто используется.
    • +1
      1) Обычно это не нужно «понимать», потому что это понимает компилятор — если переменная этого типа где-то используется как интерфейс (передается в функцию, принимающую интерфейс, или кастится к интерфейсу), то компилятор выдаст ошибку, если тип не реализует интерфейс. Вот прямо так и скажет:
      cannot use T as MyIface… T does not implement MyIface (missing SomeMethod method)


      Если же тип нигде и не используется как интерфейс, то, собственно, и надобности нет.
      Теоретически, может быть специфическая ситуация, когда нужно все таки заставлять компилятор делать проверку (это может быть полезно в какой-то плагин-подобной архитектуре, например), то можно просто сделать кастинг к интерфейсу и этим гарантировать compile-time check:
      var _ MyIface = (MyType)(nil)
      

      Но я такой хак видел только в статьях, на реальном коде, пожалуй, и не видел.
      Ну или тестами это можно «гарантировать».
      Кстати, на смежную тему — есть утилита impl, которая позволяет быстро сгенерировать код функций-пустышек для нужного интерфейса.

      2) в стандартной библиотеке — посмотрите всю папку encoding.
    • +1
      Если надо много и часто сериализовать советую смотреть на ffjson или messagepack и кода генерация для этого. Можно получить очень большой прирост в скорости. стандартная сериализации получает interface поэтому должна искать что как сериализовать. Кода генерация даёт методы заточеные для страктов и поэтому очень быстро работает
  • +2
    Я так понимаю такой крутой расчет площади ради упрощения, чтобы float64 не вводить везде?
    func (c Circle) Square() int64 {
    	return (2 * c.Radius) ^ 2
    }
    

    Переопределения площади для RoundRectangle по той же причине нету?

    Простите за занудство, но все же ввести float64 ради нормальных расчетов было бы не большим усложнением.
    • 0
      Согласен, спасибо за замечание. Старался сделать кальку с оригинального примера, но, и вправду, поправить эту оплошность ничего не стоит.
      • +1
        Тогда и площадь для rounded rectangle добавьте, чтобы всё честно было :)

        S = a*b + (4 - pi)*r^2
        • +1
          Тогда уж
          func (r RoundRectangle) Square() float64 {
          	return r.Rectangle.Square() - (4 - PI)*r.RoundRadius^2
          }
          

          Предполагается, что все имеет тип float64.

          Кажется у вас ошибка:
          S = a*b - (4 - pi)*r^2
          
          • 0
            Да, минус конечно же :)
            Не надо в полчетвёртого утра писать комментарии...
    • +4
      Ну и это, в Go оператор ^ — это XOR :)
      • 0
        Оу! Вот это я лопухнулся! Спасибо, поправил. Вот еще думал, писать в «финальных правках» о надобности тестами покрыть, или нет )
        package main
        
        import (
        	"math"
        	"testing"
        )
        
        func TestCircle(t *testing.T) {
        	square := NewCircle(0, 0, 10).Square()
        	want := 100 * math.Pi
        	if square != want {
        		t.Fatalf("Want %v, but got %v", want, square)
        	}
        }
        
  • +8
    Nitpick: площадь (фигуры) будет Area.
    • +1
      Долго не мог понять, зачем кругу метод с названием «квадрат».
  • 0
    Месяц назад ломал голову над примерно такой проблемой:
    type Figure interface {
    	...
    	AboutSquare() string
    }
    
    type Rectangle struct {
    	...
    }
    
    func (r RoundRectangle) Square() float64 {
    	return math.Abs(float64((r.Right - r.Left) * (r.Top - r.Bottom)))
    }
    
    func (r Rectangle) AboutSquare() string {
    	fmt.Sprintf("my square: %d", r.Square())
    }
    
    type RoundRectangle struct {
    	...
    }
    
    func (r RoundRectangle) Square() float64 {
    	circSquar := math.Pi * float64(r.Radius^2)
    	rectSquar := math.Abs(float64((r.Right - r.Left) * (r.Top - r.Bottom)))
    	return rectSquar - float64((r.Radius*2)^2) + circSquar
    }
    
    func newRoundRectangle(...) *RoundRectangle {
    	...
    }
    
    myRoundRectangle := newRoundRectangle(4, 4, 2)
    fmt.Println(myRoundRectangle.AboutSquare()) // my square: 16
    

    А хочется, чтобы myRoundRectangle.AboutSquare() возвращал 12.566370614359172. Ну не генерировать же для каждого типа свой метод/функцию AboutSquare.
    Решил так:
    type Rectangle struct {
    	this *Figure
    }
    
    func newRoundRectangle(...) *RoundRectangle {
    	ret := return RoundRectangle{
    		...
    	}
    	ret.this = &ret
    	return &ret
    }
    
    func (r Rectangle) AboutSquare() string {
    	fmt.Sprintf("my square: %d", r.this.Square())
    }
    


    , но чует мое сердце, что я не прав.
    • 0
      sry, * func (r Rectangle) Square() float64 {
      return math.Abs(float64((r.Right — r.Left) * (r.Top — r.Bottom)))
      }
    • +1
      Можно и так:
      func AboutSquare(r Figure) string {
      	fmt.Sprintf("my square: %d", r.Square())
      }
      
      • 0
        Можно, но еще хочется, чтобы можно было переопределить на одной из цепочек наследования этот метод (AboutSquare)
        func (r RoundRectangle) AboutSquare() string {
        	fmt.Sprintf("my square: %d +- 10^-20", r.Square()) // +- - т.к math.Pi
        }
        
    • 0
      Ну тут и вправду незачем делать AboutSquare методом, правильней будет сделать функцию, принимающей параметр Figure.
      Ну или определите AboutSquare для того типа, который встраивает, а не который встраивается (для RoundRectangle, в примере).
  • +6
    return &RoundRectangle{
    *NewRectangle(left, top, right, bottom),
    round,
    }

    Тут вы нарушили инкапсуляцию, завязав публичный интерфейс на способе агрегации. Есть несколько способов агрегации и их периодически придётся менять:
    Прямоугольник + радиус
    Прямоугольник + 4 радиуса
    Прямоугольник + 8 радиусов
    4 круга
    4 элипса
    8 точек
    16 чисел

    Вообще говоря, наследовать одну фигуру от другой — типичная ошибка ООП. Так что такую «типичную задачу ООП» и решать не стоит.
    • +1
      наследовать одну фигуру от другой — типичная ошибка ООП


      тяжело такое слышать :) можно поподробнее?
      • +8
        Подозреваю, что речь про https://en.wikipedia.org/wiki/Circle-ellipse_problem или, в более общем случае, https://en.wikipedia.org/wiki/Liskov_substitution_principle
        • +1
          Если я правильно понял, то проблема получается если мы оперируем объектами типа «круг» («квадрат») с расчетом на особенности их поведения (при изменении радиуса (высоты) изменятся линейные размеры по обоим осям, но это не верно для «элипса» («прямоугольника»)).
          Но если мы оперируем исключительно базовыми классами «фигура», то эта проблема не возникнет.

          Похожу проблему описывал Скотт Майерс в «Эффективное использование С++»:
          www.e-reading.by/chapter.php/1002058/80/Mayers_-_Effektivnoe_ispolzovanie_CPP.html
          Когда делается предположение, что птицы умеют летать, пингвин — птица и значит класс пингвин может быть наследником класса птица. Но что в этом случае делать с методом fly класса птица. :)
  • 0
    Было бы классно увидеть подобный разбор написания тестов на go.
  • 0
    А вот как это решается на языке D: http://dpaste.dzfl.pl/539fd2b9c850
    К area, я добавил ещё габариты, координаты цента, для координат выделил отдельный класс точек, ну и покрыл всё это дело тестами.
  • 0
    Хочется отметить, что existential quantification тут используется, но идёт оно из коробки. Инженерный смысл данного расширения в том, что мы таскаем словарь вместе с типом данных в рантайм (что я так понимаю делается в Go всегда), т.е. позволяет делать массив чего-то, что является instance of Figure. В Haskell классы типов (которые чуть-чуть похожи на instance) разрешаются в compile time, но существуют случаи когда этого не достаточно, как в этом примере
  • 0
    Хорошая статья. По делу. Спасибо.

    Странно, правда, что первое движение после того, как код готов — написать коментарии, а не тесты.
    • +1
      Потому что тесты должны быть написаны еще до кода )
      • 0
        Да и коментариий тоже. Но коментарии в конце написаны, а тесты таки нет.
    • 0
      Я стараюсь тесты писать по мере написания кода. Если отложить «на потом», то. как правило, до них дело не дойдет. Тут же пример был несколько искусственный — фактически, переносил код с другой статьи. Подумал про тесты, но решил, что для статьи не релевантно. За что потом словил баг в комментариях и пожелал. :)
      • 0
        Кто-то уже в коментариях высказался — надо статью про тестирование в Go.
  • 0
    Спасибо за статью.
    Есть пара вопросов по указателям:
    Общее правило тут такое — если метод должен изменять значение c или если Circle — большущщая структура, занимающая много места в памяти — тогда использовать указатель. В остальных случаях, будет дешевле и эффективней передавать значение в качестве ресивера метода.

    С этим более-менее понятно, хотя как понять насколько у меня «большущая структура», что пора использовать указатель?

    func NewRectangle(left, top, right, bottom int64) *Rectangle

    Почему конструктор возвращает указатель на структуру? Ведь структура Rectangle не большая, не выгоднее ли возвращать саму структуру?
    Всегда ли принято, что конструктор возвращает именно ссылку или нет?

    • +1
      Почему конструктор возвращает указатель на структуру?

      Ну а что ещё вернуть конструктору. Он создаёт новый объект, которого до этого не было. Возвращать саму структуру, как я понимаю, значит лишний раз делать её копию без получения каких-либо плюсов.
      • –4
        copy-on-write в Go не завезли?
        • +1
          Вы случайно не путаете copy-on-write с move semantics?
          • –2
            Не путаю. COW — просто более общий случай, который не требует какой-то отдельной семантики (хотя семантические подсказки порой необходимы). COW правильнее было бы называть copy-when-necessary, но так уж прижилось.
            • +1
              Тогда давайте обсудим.

              COW это когда при необходимости записать значение в какую-нибудь переменную, значение записывается не туда, а в другой кусок памяти. После того, как процедура записи удачно завершена, система меняет область памяти, на которую ссылается переменная со старой на новую. Другие переменные, которые всё ещё ссылаются на старую область памяти — значения не изменят.

              Move semantics — это когда вместо того, чтобы копировать содержимое области памяти, на которую ссылается переменная, в новую переменную, туда передаётся ссылка на старую область памяти. Старая переменная после этого считается не подлежащей к использованию. Копирования нет.

              Возникает 2 вопроса.
              1. Как move semantics может быть общим случаем для COW, если move semantics копирования не предполагает?
              2. Как COW может устранить необходимость копирования созданного объекта, если там копирование есть по определению?
              • –1
                Это кривая реализация COW :-) Прямая выглядит так:
                1. При копировании просто создаётся дополнительная ссылка
                2. Если на область памяти есть более чем одна ссылка, то при записи происходит реальное копирование данных.
                3. Если компилятор может понять, что по старой ссылке больше не происходит обращений (например, ссылка явно или неявно обнуляется), то он может безболезненно вырезать код отвечающий за подсчёт ссылок. Это и есть тот самый частный случай с перемещением данных.
                • 0
                  Таким образом move semantics никак не может являться частным случаем COW, потому, что там переменную, из которой вытащено значение, использовать нельзя категорически.

                  Но вот насчёт COW интересно. В каких языках есть COW, реализованный правильным способом?
                  • 0
                    Как и к любую другую обнулённую переменную. Я не зря упомянул явное и неявное обнуление.

                    В PHP, например: http://habrahabr.ru/post/226707/
                    • 0
                      Спасибо за ссылку, ознакомился с COW в php. Если я правильно понял, для объектов COW в php не работает ибо в переменной содержится id объекта, а не сам объект. COW для примитивных значений в php это наверное разумно, так как они занимают в памяти много места. Но в Go эти значения занимают столько же или меньше, чем ссылки на них, поэтому тут COW может даже помешать.

                      Со строками, массивами и объектами, может это было бы полезно. Но подозреваю расплатой за такую штуку будет серьёзная просадка в производительности.
      • 0
        Возвращать саму структуру, как я понимаю, значит лишний раз делать её копию без получения каких-либо плюсов.

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

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

        В доке об этом мало что написано, может кто поможет разобраться когда что использовать?
        • 0
          К примеру, рассмотрим стандартный пакет image (https://golang.org/pkg/image/#pkg-index)
          Практически все типы пакета имеют конструкторы, которые возвращают указатель, и все функции написаны для ресивера-указателя.
          Но есть пара типов (Rectangle, Point), конструкторы которых возвращают именно значение и все функции написаны для значения (естественно, go копирует их и для указателя).
          Почему там так, и почему в этой статье конструктор аналогичный Rectangle-сущности из пактета image возвращает указатель?
        • 0
          Да, чаще всего вы возвращаете указатель. Отчасти он «более популярен», потому что сложные структуры инициализируются с помощью new(), который возвращает именно указатель. В Effective Go это рассмотрено.

          При этом ничего не мешает возвращать реальную структуру. Это может быть выгодней, в определенных случаях. Но тогда лучше не называть метод New..., чтобы избежать путаницы. Пример с image.Rect как раз об этом — можно спокойно возвращать не ссылку, называть это не New.., и все будет замечательно (хотя, конкретно по image я не знаю, почему решили так, а не иначе — но это однозначно не принципиальный вопрос, а каких-то мелких удобств в коде)
    • +1
      хотя как понять насколько у меня «большущая структура»

      Хороший вопрос. Как я понимаю, однозначного ответа на него нет, все попытки объяснить «насколько большой должна быть большая структура» сводились к простыням рассмотрения различных случаев. Но вот что написано в вики:
      If the receiver is a large struct or array, a pointer receiver is more efficient. How large is large? Assume it's equivalent to passing all its elements as arguments to the method. If that feels too large, it's also too large for the receiver.

      Если ресивер (метода) это большая структура или массиво, будет эффективнее использовать указатель. Насколько большая «большая структура»? Представьте, что вам нужно передать все её поля как параметры функции. Если это кажется слишком громоздким, значит это большая структура для ресивера.
      • 0
        Спасибо за ответы.
        Я обычно исхожу из того, что преждевременная оптимизация — зло и лучше иметь некое единообразие в коде и удобство, а оптимизировать только тогда, когда это становится узким местом. Ведь маленькая структура может легко перерасти в большую, с развитием проекта, а это значит надо рефаторить все функции на использование указателя, вместо значения.

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

        Все верно?
        • 0
          Про преждевременную оптимизацию — безусловно, верно. Но значения vs указатели — это не только про размеры. Например, если используется ресивер-значение, то этот метод автоматически безопасен для исполнения во многих горутинах. Если же ресивер — указатель, нужно думать о том, будет ли этот метод вызываться параллельно и будет ли меняться переменная-ресивер (и тогда уже защищать мьютексом).
          Вот тут неплохо попытались расписать разные случаи: stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values
          • 0
            В случае с конструктором рассуждения про безопасность для исполнения в горутинах ведь бессмысленны, да?
            • 0
              Ну да, выше речь шла про ресиверы и методы. А методы от функций отличаются лишь целесообразностью использования — метод подразумевает некоторый стейт, и операции с ним. А конструктор — это просто функция, которая создает новый объект, это безопасно для параллельного исполнения.
    • +4
      как понять насколько у меня «большущая структура», что пора использовать указатель?

      На x86_64 указатель занимает 64 байта. Как только ваша структура начинает занимать более 64 байт имеет смысл использовать указатель. Если сформулировать по другому — указатель имеет смысл исползовать с момента, когда копирование указателя происходит гораздо быстрее, чем копирование структуры. «Гораздо» — в каждом случае своё. Иногда можно подождать даже милисекунду, потому что объекты создаются раз в минуту. А иногда подождать нельзя вообще, потому что объекты создаются непрерывно.
      • +2
        Может всё таки 64 бита а не байта?
        • –1
          Может и бита, кто в наше время их считает :)

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