Programação Orientada a Objetos com Go: Conceitos Fundamentais – Herança e Polimorfismo

Programação Orientada a Objetos com Go: Conceitos Fundamentais – Herança e Polimorfismo

Buenas, cara pessoa que está lendo, hoje seguimos com a série de Orientação a Objetos com Golang que havia iniciado, mas como fiquei ausente não segui e no artigo da semana passada tivemos uma série nova começando, mas vamos recapitular um pouco antes de seguir falando de Herança e Polimorfismo em Go?No artigo passado da série falamos de classes, objetos e encapsulamento no estilo Go, claro que eu não me aprofundei em alguns detalhes técnicos da linguagem como declaração de variáveis, structs (que são as “classes”em Go de certa forma) e vou continuar assim no artigo de hoje, bem como na série, então vou falar que essa série você já deve conhecer um pouco da linguagem ok?!. 

Pois bem, seguindo ao tema, agora chegou a hora de falar de algo que eu sempre vi, e vejo, que as pessoas mais associam à Orientação a Objetos: herança e polimorfismo. Só que, rufem os tambores, Go não tem herança tradicional como conhecemos em outras linguagens, e mesmo assim vou mostrar que você consegue fazer de tudo (e mais um pouco) usando composição e interfaces em Go.

Spoiler: depois deste artigo você nunca mais vai sentir falta de extends ou implements da forma clássica.

Composição × Herança: a abordagem idiomática em Go

Antes de seguirmos, ou explicar brevemente o que é herança aqui, tudo bem? Bem, em programação orientada a objetos, herança é um conceito que permite que uma classe (também conhecida como subclasse ou classe derivada) herdar atributos e métodos de outra classe (também conhecida como superclasse ou classe base).

Agora voltando para o tema de Go, precisamos lembrar que em linguagens OO clássicas, como Java, C# por exemplo, a herança serve para reutilizar código e especializar comportamento. Em Go fazemos isso com composição embutida (embedding composition), que nada mais é que uma struct dentro da outra.

Vamos ver um comparativo entre Java e Go (tal qual fizemos no artigo passado também):

+---------------------------+        +----------------------------+
|  Java (herança)           |        |  Go (composição)           |
+---------------------------+        +----------------------------+
|class Motor {              |        | type Motor struct {        |
|   HorsePower int          |        |   HorsePower int           |
| }                         |        | }                          |
|                           |   vs   |                            |
|class Carro extends Motor {|        | type Carro struct {        |
|   Modelo string           |        |   Motor     // embedding!  |
| }                         |        |   Modelo string            |
+---------------------------+        +----------------------------+

E agora vendo o comparativo acima, aplicado ao Go de forma simples, temos o seguinte código abaixo:

type Motor struct {
	Potencia int
}

func (m Motor) DarPartida() string {
	return fmt.Sprintf("Ligando motor com %d CV", m.Potencia)
}

type Carro struct {
	Motor       // métodos e campos de Motor “surgem” aqui
	Modelo string
}

func main() {
	c := Carro{
		Motor: Motor{Potencia: 150},
		Modelo: "Gopher GT",
	}
    
	fmt.Println(c.DarPartida())         // método promovido por composição
    fmt.Println(c.Modelo)               // campo próprio
}

Simples assim temos algo parecido com herança, mas não é herança, vamos dizer que é “igual mas diferente”. Agora do meu ponto de vista, de quem trabalha com Java e Go,  no Java, por exemplo, não dá para ter múltiplas heranças (teria que fazer uma classe, herdar de outra, que por sua vez herdaria de outra, fazendo uma cadeia de herança, que pode ser bem complicado depois), mas em Go eu posso ter múltiplas composições embutidas. Isso mesmo, várias structs dentro de outra. Ao meu ver isso simplifica muitas coisas, como simplicidade ao necessitar fazer composição, auxilia ao criar mocks para testes unitários, e na minha opinião é mais amigável para o iniciante.

Criando e implementando interfaces - Polimorfando em Go

Bem, antes de mostrar como fazer polimorfismo em Go, precisamos entender o que é polimorfismo. Em programação orientada a objetos, polimorfismo é a capacidade de tratar objetos de diferentes classes de maneira uniforme, através de uma interface comum, permitindo que cada objeto responda de forma específica a uma mesma mensagem. Tentando simplificar, podemos dizer que o polimorfismo permite que diferentes classes implementem métodos com o mesmo nome, mas com comportamentos distintos, adaptados a cada tipo de objeto.

Agora sim, antes de mostrar o polimorfismo em Go, preciso dizer que algo que eu acho interessante é que interfaces em Go são contratos implícitos. Mas o que isso quer dizer, você pode estar se perguntando. Bem, isso diz que um tipo (uma struct) satisfaz a interface se possuir todos os métodos declarados da interfaces, implementados. Basta isso! Não precisa explicitar que uma struct A implementa interface B. É só cumprir o contrato, e está pronto o sorvetinho! Ficou confuso? Vamos a um exemplo em código simples

type Animal interface {
	Fala() string
}

type Cachorro struct{}

func (c Cachorro) Fala() string { 
  return "Au au!"
}

type Gato struct{}

func (g Gato) Fala() string { 
  return "Miau!"
}

E é isso! Não precisamos dizer que o tipo Cachorro implementa Animal, algo que teria que fazer em java, ao falar que a Classe Cachorro implementa Animal. Tá achando confuso ainda? Pois vamos adicionar uma nova parte ao nosso código acima, você vai executar o arquivo em sua máquina e me ver o que acontece depois.

func FaçaBaralho(a Animal) {
	fmt.Println(a.Fala())
}

func main() {
    var chihuahua Cachorro
    
    var frajola Gato
    
	FaçaBaralho(chihuahua)
    
	FaçaBaralho(frajola )
}

E desta forma, você vai ver que, mesmo a função FaçaBarulho pedindo um Animal, que é uma interface, mas como temos Cachorro e Gato implementado ela, vamos conseguir passar dois animas diferentes como parametro para ela. E assim,o mesmo código deve aceita qualquer novo bicho que você quiser criar que implemente o método Fala() – sem mexer na função FaçaBaralho(). Desta forma temos diferentes structs (o equivalente a classes de Go) tendo comportamentos diferentes quando o mesmo método Fala() é chamado dentro da função  FaçaBaralho().

Composição com Interfaces: Interfaces Embutidas (interface embedding)

Agora que sabemos como usar interface e aplicar o polimorfismo em Go, podemos dizer que se quisermos ter mais flexibilidade com o polimorfismo, saiba que podemos ainda combinar interfaces em outra interface e assim temos as interfaces embutidas (embeded interfaces). Vamos ver um exemplo?

type Somar interface { 
  Soma(a int, b int) int 
}

type Subtrair interface {
  Subtrai(a int, b int) int
}

type SomarESubtrarir interface { // união de contratos de duas interfaces
  Somar
  Subtrair
}  

Agora se você quiser que um tipo some e subtrair, basta que ele implemente as funções Soma() e Subitrai(). E isso é bem usado em Go, não vai pensando que é loucura minha, pois é exatamente assim que a biblioteca padrão (stdlib) define o tipo io.ReadWriter (que junta os tipos de Reader + Writer).  Mas pra que isso? Bem, de forma resumida, isso facilita criar códigos granulares e reutilizar comportamentos.

Go e seus tipos dinâmicos e as asserções de tipos

Às vezes (é raro, mas sempre acontece),  precisamos aceitar em uma função um tipo qualquer, e depois descobrir o que veio, para então poder fazer algo.  Isso acontece pela forma como a linguagem foi construída e pode ser algo bom ou ruim a depender de como isso é usado, mas em vias de regra sempre tentaremos usar Composição (o equivalente de Herança em Go) e o Polimorfismo através das interfaces, tudo bem?

Pois bem, seguindo, para isso podemos utilizar o tipo interface{} , como a partir das versão 1.22 do Go, podemos simplesmente chamar de `any `(mas o nome dela é empty interface). Bem, não vou dizer quando fazer isso, mas se quiser fazer, vou mostrar um exemplo simples abaixo.

func InspetorDeTipo(tipoQualquer any) {

	switch tipo := tipoQualquer.(type) {          // type switch, posso escrever depois sobre isso, mas deixo como dever para você descobrir mais
	case int:
		fmt.Println("É inteiro:", tipo)
	case string:
		fmt.Println("É string:", tipo)
	default:
		fmt.Printf("Tipo desconhecido: %T\n", tipo)
	}

}

func main() {
	InspetorDeTipo(42)
	InspetorDeTipo("hello")
	InspetorDeTipo([]byte{1, 2, 3})
}

De novo, um aviso: use isso com moderação! Já dizia o tio Ben, com grandes poderes vem grande responsabilidades. Geralmente uma interface bem-definida, para ser usado o polimorfismo como mencionei antes, é mais segura e legível do que espalhar interface{}  ou any pelo código, parecendo o pessoal que migra do Javascript para o Typescript. Não me vem fazer isso em código Go, viu?

E agora?

Bem, acho que este artigo já está um pouco longo e vamos encerra-lo por arqui. Mas fique de prontidão, pois o assunto de Orientação a Objetos em Go chegou ao final em sua base, mas a série continua pois no próximo artigo vamos começar a entender os princípios SOLID e ver como eles se encaixam com esse estilo de composição + interfaces que o Go tem. E já dando um pequeno spoiler, que nem é tanto um spoiler,  o primeiro princípio da lista é o Single Responsibility Principle (que representa a letra S de SOLID).

E se você curtiu o conteúdo, compartilhe com a galera dev que tá aprendendo Go, ou tá querendo melhorar suas habilidade nas linguagem, para que possamos todos aprender juntos. Também já deixa o feedback e não esquece de se inscrever na newsletter para receber os próximos capítulos fresquinhos e poder interagir no blog!

Até o próximo artigo da série de Programação Orientada a Objetos com Go!