Объектно-дезориентированный язык

http://areyoufuckingcoding.me/2012/07/25/object-desoriented-language/
  • Перевод

Каждый раз когда речь заходит о Go приходится слышать один и тот же вопрос:
Является ли Go объектно-ориентированным языком?

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

Структуры на первый взгляд


авторская картинка

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

type Person struct {
        Name string
}

func (p *Person) Intro() string {
        return p.Name
}

type Woman struct {
        Person
}

func (w *Woman) Intro() string {
        return "Mrs. " + w.Person.Intro()
}

В примере выше показан простейший пример того, что большинство из вас назовут наследованием. Скажем пока что, что Woman наследуется от Person. Тип Woman содержит все поля типа Person, может переназначать его функции, может вызывать методы родительского типа. Как и было сказано выше, на первый взгляд это выглядит как наследование. Ага… но это всего лишь трюк!

Трюк?


Давайте посмотрим повнимательнее, и позвольте мне объяснить как это работает внутри. Перво-наперво здесь нет никакого настоящего наследования. Было бы замечательно, если бы вы забыли всё что вы знаете о наследовании во время чтения всего этого… Чик!

авторская картинка

Теперь представьте структуру в виде коробки. Обычная серая картонная коробка… И представьте поля в качестве вещей, каких-то магических предметов которые вы можете положить внутрь коробки. Вот пример:

type SmallBox struct {
        BaseballCards   []string
        BubbleGumsCount int
        AnyMagicItem    bool
}

Ага, именно так наша маленькая коробка может выглядеть. Но однажды её может стать недостаточно, так? Или возможно вам захочется разделить ваши вещи. В этом случае вы можете взять большую коробку и положить внутрь неё маленькую вместе с другими предметами. Например:

type BigBox struct {
        SmallBox
        Books []string
        Toys  []string
}

Великолепно, у нас есть большая коробка содержащая все предметы имеющиеся в маленькой плюс немного книг и игрушек. И вот тут происходит магия… Мы можем спросить:
Что находится в большой коробке?

Мы можем ответить по разному. С одной стороны мы можем коротко сказать что в ней книги, игрушки и какая-то маленькая коробка, но с другой мы можем быть более подробны сказав что в коробке находятся игрушки, книги, бейсбольные карточки и какие-то магические предметы. Оба ответа верны, но различны в своей подробности. Go кроме всего прочего позволяет определять уровень этой подробности, например:

bigBox := &BigBox{}
bigBox.BubbleGumsCount = 4          // correct...
bigBox.SmallBox.AnyMagicItem = true // also correct

Если вкратце, то BigBox не родня SmallBox, он лишь содержит внутри один экземпляр SmallBox и предоставляет быстрый доступ к его полям.

Переопределение?


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

func (sb *SmallBox) Capacity() int {
        return 20
}

func (bb *BigBox) Capacity() int {
        return bb.SmallBox.Capacity() * 3
}

Мы задаём что BigBox может содержать в три раза больше предметов по сравнению с маленькой коробкой, но мы не переопределяем функцию принадлежащую SmallBox. Мы всё ещё можем получить доступ к ним обоим, т.к. они принадлежат разным коробкам.

fmt.Println(bigBox.Capacity())          // => 60
fmt.Println(bigBox.SmallBox.Capacity()) // => 20

Однако, функции могут недвусмысленно вызываться из внешней коробки с использованием сокращений:

func (sb *SmallBox) Color() string {
        return "gray"
}

// *snip*

bigBox.SmallBox.Color() // => "gray"
bigBox.Color()          // => "gray"

Это киллер-фича которая привносит в Go глоток свежего воздуха в вопросе наследования. Функция Color в обоих вызовах относится к одной и той же функции связанной с SmallBox.

Жадничаем память!


Go в общем является языком системного программирования и позволяет нам до некоторых пределов управлять памятью используя указатели. Мы можем использовать их для экономии памяти при работе со структурами. Можно предположить что BigBox может или не может содержать внутри себя SmallBox. До сих пор мы постоянно выделяли память под маленькую коробку, хотя она и не использовалась. Мы можем проделать то же самое чуть более эффективно посредством включения указателя в нашу структуру:

type SkinflintBigBox struct {
        *SmallBox
        Books []string
        Toys  []string
}

Но тут имеется один трюк, данная включённая структура работает таким же образом как и любой другой указатель, так что нам необходимо помнить что её необходимо инициализировать, иначе может случиться много плохого:

bigBox := &SkinflintBigBox{}
bigBox.SmallBox     // => nil
bigBox.AnyMagicItem // ...

авторская картинка

Нам необходимо инициализировать нашу маленькую коробку абсолютно так же как и любой другой указатель:

bigBox := &SkinflintBigBox{SmallBox: &SmallBox{AnyMagicItem: true}}
bigBox.AnyMagicItem // => true

Ура! Теперь всё отлично работает! Кроме того, вам возможно будет интересно знать, что включённый указатель может быть инициализирован в любое время, совершенно необязательно делать это при инициализации внешней структуры.

Это не магия, это трюк...


Суммируя, здесь нет магии. Так называемое наследование это не более чем особый тип поля которое предоставляет сокращения к собственным функциям. Просто, умно и достаточно что бы ответить фанатикам ООП:
Конечно, это ООП… Вперёд!

авторская картинка

На этом всё, надеюсь вам понравилось!

(прим.: согласно требованиям публики авторские картинки спрятаны за ссылками)
Поделиться публикацией
Комментарии 43
    +26
    Картиночки как-то поднадоели уже.
      +14
      Авторский стиль, я не более чем переводчик.
      +8
      type Person struct {
              Name string
      }
      
      func (p *Person) Intro() string {
              return p.Name
      }
      
      type Woman struct {
              Person
      }
      
      func (w *Woman) Intro() string {
              return "Mrs. " + w.Person.Intro()
      }
      

      А что тут можно назвать наследованием?
      Как-то очень все у вас мутно написано.
        +2
        Тут тип Woman «наследуется» от типа Person и соответственно имеет поле Name и метод Intro, который затем «переопределяется». Статья не ставит целью разъяснить синтаксис Go, так что для совершенно не знакомого с языком человека действительно может быть мутновато.
          0
          Наследование типов это немного не то, о чем вас постоянно спрашивают.
            +2
            Ну во-первых спрашивают не меня, а автора статьи, а во-вторых считаю что ответ на поставленный вопрос в статье содержится. Для полноты конечно стоило бы ещё рассказать об интерфейсах, но это уже другая тема.
              0
              Прошу прощения, не сразу увидел, что это перевод.
        +3
        Композиция из Go (в варианте без звездочки) эквивалентна наследованию без виртуальных методов. Я не вижу различий между ними.
          +4
          Вопрос терминологии, Go удаётся очень удачно мимикрировать под привычные термины и конструкции в мозгу программиста, хотя под капотом там используются отличные от привычных механизмы, знание которых позволяет делать очень интересные и удобные штуки. Об этом как раз и рассказывает статья.
            0
            В качестве примера, можно привести вот такое «двойное наследование»:
            Код
            type A struct {
                x int
            }
            
            func (self *A) F1() () {
            	fmt.Println("I'm A: ", self.x)
                return
            }
            
            type B struct {
                y int
            }
            
            func (self *B) F2() () {
            	fmt.Println("I'm B: ", self.y)
                return
            }
            
            type C struct {
                A
                B
                z int
            }
            
            func (self *C) F3() () {
            	fmt.Println("I'm C: ", self.x, " and can call A and B: ")
                self.F1()
                self.F2()
                return
            }
            


            Лично мне неизвестно как такое может уложиться в привычные ООП рамки.
              +4
              Множественное наследование же, или тут скрыто что-то сильно большее?

              Пример на C++:

              class A {
              public:
                int x;
                void F1() {
                  std::cout << "I'm A: " << x << std::endl;
                }
              };
              
              class B {
              public:
                int y;
                void F2() {
                  std::cout << "I'm B: " << y << std::endl;
                }
              };
              
              class C : public A, public B {
              public:
                int z;
              
                void F3() {
                   std::cout << "I'm C: " << x << " and can call A and B" << std::endl;
                   F1();
                   F2();
                }
              };
              
              
                +1
                Хм, тут я видимо погорячился. Предыдущий комментарий прошу считать недействительным.
                +2
                trait
              +1
              Является ли Go объектно-ориентированным языком?


              golang.org/doc/faq#Is_Go_an_object-oriented_language

              Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

              Also, the lack of type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.
                +1
                А это ООП?
                data Animal = Animal {
                  speak :: Int -> String, 
                  move :: Int -> String,
                  act :: Int -> String
                }
                
                base self = Animal {
                   speak = \repeats -> "",
                   move = \repeats -> "",
                   act = \repeats -> (self  `move` repeats) ++ (self  `speak` repeats)
                }
                
                {- One type of animal -}
                bird self = let super = base self in Animal {
                   speak = \repeats -> concat $ replicate repeats "cheep ", 
                   move = \repeats -> concat $ replicate repeats "flap ",
                   act = (super `act`)
                }
                
                {- Another type of animal -}
                dog self = let super = base self in Animal {
                   speak = \repeats -> concat $ replicate repeats "bark ", 
                   move = \repeats -> concat $ replicate repeats "run ",
                   act = (super `act`)
                }
                
                new f = f (new f)
                
                b = new bird
                d = new dog
                
                *Main> b `act` 2
                "flap flap cheep cheep "
                

                Взято отсюда. BTW интересное обсуждение.
                  +4
                  Я думаю, что если рассматривать ООП с точки зрения трех основных понятий — инкапсуляция, наследование, полиморфизм, то Go с натяжкой, но все же можно отнести к ООП-языкам:

                  1. Инкапсуляция — объединение в одной структуре данных и функций, которые работают с этими данными — есть, описано в статье. Средства скрытия некоторых членов структур тоже есть (аналогично public/private; protected нет, но из-за отсутствия традиционного наследования этого никто не замечает :).

                  2. Наследование. Опять же описано в статье. Пусть данная функциональность реализована через композицию, а не через «традиционное» наследование, своей цели оно достигает — структура ведет себя так же, как ее «предок», расширяя функциональность «предка» своими методами и полями.

                  3. Полиморфизм. Опять-таки с натяжкой полиморфизм в Go есть, реализуется он через механизм «утиной» типизации (duck typing). Подход простой — если нам нужен интерфейс с функциями add(), del(), get() (для примера), то любая структура Go, у которой есть данные функции, поддерживает данный интерфейс. Таким образом, абсолютно неважен реальный тип данных, которых нам передают для обработки — главное, чтобы у него были нужные функции (нужный интерфейс). Полиморфизм с точки зрения независимости интерфейса от реальной реализации соблюден.

                  Лично мне подход Go импонирует. Он немного непривычен, но довольно интересен.
                    +1
                    а какие у языка «Go» есть плюсы по сравнению с другими ЯП?
                      +1
                      С одной стороны: есть указатели, но при этом есть сборка мусора. С другой стороны: есть указатели, но при этом нет арифметики с ними. Лично для меня это оказалось определяющей характеристикой языка, которая у меня вызвала диссонанс, и я никак не могу определиться со своим отношением к этому языку. Возможно, за последнее время что-то изменилось, поправьте меня, если так.
                        +2
                        Так это же классно: наличие указателей дает определенное ускорение, где это критично, а отсутствие адресной арифметики и сборка мусора делает использование указателей безопасным.
                          +1
                          Арифметика всё-таки есть, просто она не на виду.
                            0
                            В смысле, до неё вполне можно добраться, модуль специальный есть (причём, это не какое-то нововведение).
                              0
                              Э, ну так в классическом Паскале тоже были указатели без арифметики.
                              А в Обероне, якобы, и сборку мусора навернули.
                              Всё это лет десять назад как.
                              +3
                              — компиляция в нативный код
                              — статическая типизация
                              — всякие «плюшки» из современных интерпретируемых языков: замыкания, слайсы, множественные возвращаемые из функции значения
                                0
                                Common Lisp. Всё это и ещё немного. :)
                                  +2
                                  (и (ещё (немного (скобочек ;-)))))
                                    0
                                    Похоже, это действительно лучший из существующих языков на все случаи жизни, если, у кого ни спроси, все единственным недостатком видят только префиксный синтаксис, к которому нужно привыкнуть так же, как все мы в школе привыкали к инфиксному.

                                    На самом деле я своим комментарием хотел сказать, что Го в сравнении с некоторыми «другими ЯП» по этим фичам опоздал чуть больше чем на двадцать лет. Вечность по меркам IT. Вы, кстати, ещё рестарты забыли, которые, да, тоже есть в Common Lisp (и Smalltalk). Более того, в этих двух, в отличие от Go, они являются стандартным и единственным способом обработки ошибок, за что авторам стандарта большое человеческое спасибо. Благодаря им существует хотя бы два языка с вменяемой системой обработки ошибок.

                                    Второй обзац, в отличие от первого, троллоло не является. Весь. И да, я в курсе, что у Go немного другие решаемые задачи и существуют другие преимущества. Это ответ на конкретный комментарий.
                                      0
                                      Основной недостаток CL (с моей точки зрения) — разделение пространств имен функций и значений. В результате в программах мусорного слова funcall не многим меньше, чем скобочек.
                                      Лечится, конечно, но осадочек остается :-).
                                +4
                                Основная фишка go в _крайне простой_ параллелизации, засчёт каналов и go-рутин. Кроме этого, от прочих асинхронных языков и фреймворков go отличает не ограниченная многопоточность (например, node.js однопоточная), экономная работа с памятью (а не как scala), сравнительно быстрая общая работа (т.к. статически типизированный), и довольно быстрый сборщик мусора.

                                В целом, это системный язык со встроенным обеспечением параллельности и синхронизации между серверами, при этом с лаконичным синтаксисом и прочими плюшками (например, import прямо с гитхаба).
                                  +1
                                  Да уж, scala по прожорливости — просто жесть! Сам столкнулся на практике, пришлось переписывать некоторые классы прямо на яве, чтобы не позорится ява стилем в scala. Согласен тут с авторами yammer. Но в малых дозах, как в playframework'е scala как бальзам на душу.
                                    0
                                    А можно по-подробней про синхронизацию между серверами?
                                      +1
                                      Из плюшек языка, был netchannel (канал в терминах go, но по сети). До 1.0 он был непосредственно в package'ах языка, в ближайших релизах обещают вернуть. Я его руками делал в несколько десятков строк из gob'ов поверх обычного сетевого сокета.

                                      В целом, сам язык способствует удобной и лаконичной межсерверной работе, засчёт многопоточной асинхронности и заточенности на потоковую обработку. В качестве примера есть прекрасный github.com/ha/doozerd — кластер наподобие zookeeper'а.
                                        0
                                        Имеется ещё chanio (по ссылке описывается реализация) от автора этой статьи который в некоторой степени повторяет функции netchan.
                                  +4
                                  Смысл всей статьи:
                                  в Go нет наследования, но есть агрегация
                                  .
                                    0
                                    Агрегация есть везде. То есть можно остановиться на «в Go нет наследования». А раз нет наследования, то нет и полиморфизма. Итого только инкапсуляция.
                                      +2
                                      Там прекрасный полиморфизм времени исполнения, основанный на структурной типизации.
                                      Вот рабочий пример, основанный на примере из книги Ivo Balbaert — The way to Go.
                                      package main
                                      
                                      import (
                                      	"fmt"
                                      )
                                      // квадрат
                                      type Square struct {
                                      	side float32
                                      }
                                      
                                      func (sq *Square) Area() float32 {
                                      	return sq.side * sq.side
                                      }
                                      
                                      // круг
                                      type Circle struct {
                                      	radius float32
                                      }
                                      
                                      func (c Circle) Area() float32 {
                                      	return 3.14159 * c.radius * c.radius
                                      }
                                      
                                      func main() {
                                      	var areaIntf Shaper  // объект интерфейса
                                      
                                      	// создаем объект Квадрат
                                      	sq1 := new(Square)
                                      	sq1.side = 5
                                      	// присваиваем объект интерфейсу и вызываем полиморфно функцию через интерфейс
                                      	areaIntf = sq1
                                      	fmt.Printf("The square has area: %f\n", areaIntf.Area())
                                      
                                      	// создаем объект Круг
                                      	cr1 := new(Circle)
                                      	cr1.radius = 5
                                      	// присваиваем объект интерфейсу и вызываем полиморфно функцию через интерфейс
                                      	areaIntf = cr1
                                      	fmt.Printf("The circle has area: %f\n", areaIntf.Area())
                                      }

                                      Даже догадываюсь как это может быть устроено на низком уровне, если доберусь — надо будет Идой посмотреть ассемблерный код, проверить предположение:)
                                        +1
                                        Как уже сказали, это не полиморфизм, это утиная типизация. Почти то же, но неявно. Хочешь-не хочешь, но если сигнатура совпала — ты реализуешь этот интерфейс.
                                        Cобственно, сама возможность полиморфизма без наследования появилась с тем, что интерфейс отпочковался от абстрактного класса. То бишь не стоило мне говорить, что это взаимосвязано. Вообще полиморфизм действительно возможен без наследования.

                                        И, раз уж появились пишущие на этом языке, вопрос — в семплах с# есть такой пример. Два интерфейса, метрический и английский. Если присвоить квадрат в переменную метрического типа, то Area() вернет значение в квадратных метрах, если в переменную английского — в квадратных футах. Здесь такое реализуемо?
                                          0
                                          Реализуемо через возврат интерфейса, но требует небольшой обвязки:
                                          func Area(i  interface{}) interface{} {
                                           ...
                                          }
                                          
                                          func main() {
                                             out_value := Area(in_value)
                                              if value, ok := out_value.(MetricArea); ok {
                                                  use_metric(value)
                                              }
                                              if value, ok := out_value.(ImperialArea); ok {
                                                  use_imperial(value)
                                              }
                                          }
                                          
                                          0
                                          Проблема: чтобы «унаследоваться» от класса, придется переопределить все объявленные в нем методы (я верно понимаю этот код?). И не дай бог потом набор методов в предке поменяется…
                                            +3
                                            Нет, при инкапсуляции структуры в структуре во внешней доступны все методы внутренней.
                                            play.golang.org/p/Qqw6v1oWNF

                                            Мне кажется, go не стоит воспринимать как объектно-ориентированный язык — удобней просто работать со структурами, методами структур и интерфейсами — тогда всё логично, просто, легко, и всего хватает.
                                      +4
                                      Весьма похоже на прототипы в Javascript.
                                        0
                                        Объекты в Objective-C устроены так же, первым полем структуры является поле Class isa; — указатель на супер класс.
                                          +3
                                          Не совсем — там указатель не на «суперкласс» (базовый класс в терминах С++), а на «метакласс», то есть сам класс (тип данных) в ObjC является объектом, доступным в рантайме, на него и указывает isa. Это скорее рефлексия.
                                          Но зато рантайм ObjC позволяет реализовать структурный полиморфизм, похожий на то, что я привел здесь.
                                            0
                                            Блин я дурень, описался, конечно же на класс объекта.

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

                                        Самое читаемое