Если вы еще не пресытились постами в блогосфере на тему «является ли Go ООП-языком», то вот вам еще один. И краткий ответ — «да, но это не важно».
Впрочем, я постараюсь не зацикливаться на терминологии и академических определениях, а сделать акцент на том, как мы делаем это в Go.
Что такое ООП?
Как правило, вопрос об объектно-ориентированности Go происходит от пугающе популярного заблуждения «ООП-язык должен иметь классы», которых в Go, как известно нет. Чуть более глубокие размышления приводят вопрошающих и отвечающих к формулировкам вроде «если язык поддерживает инкапсуляцию, полиморфизм и наследование — то он ООП», на что Алан Кей (Alan Kay), который, собственно, изобрел ООП, смотрит с нескрываемым непониманием.
Рискну предположить, что для большинства читателей каноном ООП-языка является C++, про который сам Алан Кей говорит следующее:
I made up the term 'object-oriented', and I can tell you I didn't have C++ in mind
— Alan Kay, OOPSLA '97
(Видео, кстати, отличное, посмотрите, если еще не.)
И сразу же, о том, что же подразумевалось под ООП:
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
Вобщем, если углубиться в тему, и не настаивать, что ваша любимая книжка по ООП самая верная, то идея была была все же в переносе опыта восприятия реального мира в модели разработки кода. В мире есть объекты, у объектов есть свойства и поведение, объекты могут взаимодействовать. Точка. Мы так понимаем мир, в конце концов. Все остальные определения и понятия, появившиеся потом — лишь способы реализовать эту простую концепцию.
Какой-то «единственно правильной реализации» ООП нет и быть не может — и если кто вам скажет обратное, гоните в шею.
Go не любит late binding, но любит messaging, а концепция «свойств и поведения» объектов реализована великолепно, и это дает повод называть Go великолепным ОО-языком.
Субъективно — после полутора работы с Go, иметь принципиальное разделение на «свойства» и «поведение» кажется самым естественным способом представления сущностей, и удивляешься, почему к этому не пришли в мейнстримовых языках раньше. Классы с методами и свойствами в одной куче мне искренне кажутся сейчас неудобным и старым атавизмом.
ООП на примере
Возьмем какой-нибудь приближенный к жизни пример, вместо обычных Dog и Animal ) Например, вы — бывший напарник Сноудена и пишете систему, мониторящую приватную информацию известных личностей :) Вам нужно получить все сообщения, в которых упоминалась фраза «если бы бабушка». Сообщения могут быть голосовыми, skype, e-mail, твиттер сообщениями или какими-нибудь еще.
В class-oriented языках, мы бы скорее всего создали класс MessageSource с виртуальным методом LookupWord(), который бы возвращал объекты класса Message, Для работы, к примеру, с Twitter или Skype мы бы наследовались от MessageSource, имплементировали LookupWord() и назвали бы класс TwitterSource для Twitter, и SkypeSource — для Skype. Привычно и понятно всем, кто привык к классам.
В Go мы можем выразить это более прямолинейно — отдельно описать свойства, а отдельно поведение, и использовать композицию вместо привычного наследования, которого в Go нет.
Структуры
Сначала опишем свойства — для этого создадим структуру, содержащие необходимые поля для свойств объектов. Структуры в Go — основной способ создания своих типов данных.
type Message struct {
Data []byte
MimeType string
Timestamp time.Time
}
type TwitterSource struct {
Username string
}
type SkypeSource struct {
Login string
MSBackdoorKey string
}
Конечно, реальная имплементация будет содержать больше интересного, но идея понятна. Message описывает данные о сообщении, TwitterSource и SkypeSource — данные об источниках сообщений.
Видимость
В Go используется правило регистра первой буквы имени — если название начинается заглавной буквы — это public-доступ, если с прописной — private. Вначале может показаться посягательством на свободу слова неудобством, но с первых строк на Go, понимаешь, насколько это было удобное решение.
Если мы разрабатываем API и хотим скрыть поле — просто называем его username, вместо Username. И никто не запрещает нам создать геттеры и сеттеры. В Go принято использовать форму X() и SetX() для геттера и сеттера, соответственно.
Интерфейсы
Теперь займемся поведением. Первое, что приходит на ум — это источник сообщений должен уметь искать слова. Отлично, так и запишем:
// Finder represents any source for words lookup.
type Finder interface {
Find(word string) ([]Message, error)
}
Это распространненая практика, когда интерфейс с одним методом называют именем этого метода + 'er': Find -→ Finder. Большинство интерфейсов в Go описывают 1-2 метода, не больше.
Теперь, любой тип данных, для которого будут определены методы Find() с вышеприведенной сигнатурой — автоматически будут удовлетворять интерфейс Finder. Duck typing — если плавает, как утка, выглядит, как утка, и крякает, как утка — то это утка.
Методы
Методы в Go — это функции, определенные для конкретного типа. Можно представлять методы, как методы класса, а можно, как обычные функции с указателем (или копией) на this/self. Объект, которые будет this указывается в скобочках между ключевым словом func и названием.
func (s TwitterSource) Find(word string) ([]Message, error) {
return s.searchAPICall(s.Username, word)
}
func (s SkypeSource) Find(word string) ([]Message, error) {
return s.searchSkypeServers(s.MSBackdoorKey, s.Login, word)
}
Только не используйте имена this и self — это ненужная калька с других языков, в Go так не пишут.
Создав эти методы, мы, фактически получили два объекта, которые могут использоваться как Finder.
Немного анимации для разнообразия:
Теперь соберем все вместе:
type Sources []Finder
func (s Sources) SearchWords(word string) []Message {
var messages []Message
for _, source := range s {
msgs, err := source.Find(word)
if err != nil {
fmt.Println("WARNING:", err)
continue
}
messages = append(messages, msgs...)
}
return messages
}
Мы добавили новый тип Sources — массив из Finder-ов — всего, что может давать нам возможность поиска. Метод SearchWords() для этого типа возвращает массив с сообщениями.
Литералы
В Go любой объект можно инициализировать при декларации с помощью литералов:
var (
sources = Sources{
TwitterSource{
Username: "@rickhickey",
},
SkypeSource{
Login: "rich.hickey",
MSBackdoorKey: "12345",
},
}
person = Person{
FullName: "Rick Hickey",
Sources: sources,
}
)
Встраивание
Личность, в представлении NSA состоит из имени и источников приватной информации о ней ) Добавив безымянное поле типа Sources мы используем embedding — «встраивание», которое без лишних манипуляций позволяет объекту типа Person напрямую использовать функции Sources:
type Person struct {
FullName string
Sources
}
func main() {
msgs := person.SearchWords("если бы бабушка")
fmt.Println(msgs)
}
Если мы переопределим метод SearchWords() для Person, то он будет иметь приоритет, но у нас все равно остается возможность вызвать Sources.SearchWords().
Вывод
Сейчас, вывод программы будет выглядеть примерно так:
$ go run main.go
WARNING: NSA cannot read your skype messages ;)
[{[82 101 109 101 109 98 101 114 44 32 114 101 109 101 109 98 101 114 44 32 116 104 101 32 102 105 102 116 104 32 111 102 32 78 111 118 101 109 98 101 114 44 32 208 181 209 129 208 187 208 184 32 208 177 209 139 32 208 177 208 176 208 177 209 131 209 136 208 186 208 176 46 46 46] text/plain 2014-11-18 23:00:00 +0000 UTC}]
Это стандартная схема строкового представления структур. В стандартной библиотеке широко используется простой интерфейс Stringer, который определяет один метод — String() string. Любой объект, который реализует этот метод, автоматически становится Стрингером и при использовании в стандартных функциях вывода, будет отображаться тем способом, который реализован этим методом.
К примеру:
func (m Message) String() string {
return string(m.Data) + " @ " + m.Timestamp.Format(time.RFC822)
}
Теперь вывод fmt.Println(msg) приобретет более приятный вид:
$ go run main.go
WARNING: NSA cannot read your skype messages ;)
[Remember, remember, the fifth of November, если бы бабушка... @ 18 Nov 14 23:00 UTC]
Код целиком: play.golang.org/p/PYHwfRmDmK
Выводы
Так является ли Go ОО-языком?
Безусловно, возможно даже одним из самых ОО-х, в понимании Алана Кея.
Но даже если нет, и Алан Кей не одобрит подход Go — какая разница, если OO-дизайн Go вас восхищает и дает возможность эффективно переносить проблемную область на язык разработки?
Keep calm & write you next program in Go.
Автор: divan0