Por que Go não tem herança
Parte da seção Programação Orientada a Objetos do Journey de GO da Coddy. Lição 34 de 107.
Linguagens tradicionais orientadas a objetos, como Java e C++, usam herança para compartilhar código entre tipos. Uma classe filha estende uma classe pai, herdando todos os seus campos e métodos. Go deliberadamente omite esse recurso.
A herança cria um forte acoplamento entre os tipos. Quando uma classe pai muda, todas as classes filhas são afetadas.
Hierarquias de herança profundas se tornam difíceis de entender e manter. O "problema da classe base frágil" ocorre quando modificações em uma classe base quebram inesperadamente as classes derivadas.
Os designers de Go escolheram um caminho diferente: composição em vez de herança. Em vez de dizer "um Dog é um Animal", Go incentiva você a dizer "um Dog has comportamentos semelhantes aos de Animal". Essa mudança sutil leva a um código mais flexível e fácil de manter.
Go alcança a reutilização de código por meio de dois mecanismos que você já aprendeu:
- Interfaces definem contratos de comportamento sem detalhes de implementação
- Incorporação de structs permite que os tipos incluam outros tipos e reutilizem seus métodos
Considere esta comparação. Na OOP tradicional, você poderia escrever class Dog extends Animal. Em Go, você incorpora uma struct Animal dentro de Dog e implementa interfaces compartilhadas. O resultado é uma funcionalidade semelhante, com um acoplamento mais fraco entre os tipos.
Este capítulo explora a incorporação de estruturas em profundidade, mostrando como Go obtém os benefícios da herança sem suas desvantagens.
Desafio
FácilVamos criar um sistema de notificações que demonstra a abordagem de composição do Go em vez de herança. Você criará tipos que compartilham comportamento por meio de interfaces e incorporação de structs, em vez de hierarquias de classes.
Você organizará seu código em três arquivos:
notifier.go: Defina uma interfaceNotifiercom um único métodoNotify(message string) string. Crie também uma structBaseNotifiercom um campoNameque será incorporada por outros tipos. Dê àBaseNotifierum método chamadoFormat(message string) stringque retorna a mensagem prefixada com o nome do notificador entre colchetes.channels.go: Crie dois tipos de canais de notificação que incorporemBaseNotifiere implementem a interfaceNotifier:EmailNotifiercom um campo adicionalAddressSMSNotifiercom um campo adicionalPhone
Notifyde cada tipo deve usar o método incorporadoFormate incluir as informações específicas de seu canal na saída.main.go: Crie uma função chamadaSendAlertque aceite qualquerNotifiere uma mensagem, e então retorne o resultado da chamada deNotify. Leia os detalhes da notificação da entrada, crie os dois tipos de notificadores e demonstre como eles podem ser usados de forma intercambiável por meio da interface.
As seguintes entradas serão fornecidas:
- Linha 1: Nome do notificador para e-mail
- Linha 2: Endereço de e-mail
- Linha 3: Nome do notificador para SMS
- Linha 4: Número de telefone
- Linha 5: Mensagem de alerta
Seu método Format em BaseNotifier deve retornar:
[Name] messageSeus métodos Notify devem retornar:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Por exemplo, dadas as entradas Alerts, user@mail.com, Urgent, 555-1234 e Server down, sua saída deve ser:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downObserve como os dois tipos de notificadores reutilizam o método Format de BaseNotifier por meio da incorporação, enquanto cada um fornece sua própria implementação de Notify. A função SendAlert funciona com qualquer Notifier sem conhecer o tipo concreto. Isso é composição em vez de herança na prática.
Experimente você mesmo
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert aceita qualquer Notifier e uma mensagem, retorna o resultado de chamar Notify
// TODO: Implemente a função SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Lê o nome do notificador de email
scanner.Scan()
emailName := scanner.Text()
// Lê o endereço de email
scanner.Scan()
emailAddress := scanner.Text()
// Lê o nome do notificador de SMS
scanner.Scan()
smsName := scanner.Text()
// Lê o número de telefone
scanner.Scan()
phoneNumber := scanner.Text()
// Lê a mensagem de alerta
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Crie um EmailNotifier com emailName e emailAddress
// TODO: Crie um SMSNotifier com smsName e phoneNumber
// TODO: Use SendAlert para enviar o alertMessage através de ambos os notificadores
// e imprima os resultados
// Suprima avisos de variáveis não utilizadas (remova estes quando usar as variáveis)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.
Todas as lições de Programação Orientada a Objetos
1Fundamentos de OOP em Go
Arquivos ExternosWorkspace e Módulos do GoPacotes e ImportsNomes Exportados vs. Não ExportadosIntrodução à OOP em GoStructs como ClassesDefinindo Métodos em StructsReceptores de Ponteiro vs. de ValorInicialização de StructsFunções ConstrutorasRecapitulação — Calculadora Simples4Interfaces
Introdução às InterfacesImplementação ImplícitaInterface como ContratoInterface Vazia (any)Asserção de TipoSwitch de TipoComposição de InterfacesInterfaces Stringer e ErrorRecapitulação - Calculadora de Formas7Encapsulamento
Campos Exportados vs. Não ExportadosEncapsulamento no Nível de PacoteMétodos Getters e SettersOcultação de Informações em GoRecapitulação - Registros de Alunos10Genéricos (Go 1.18+)
Introdução aos genéricosParâmetros de tipoRestrições de tipoStructs genéricasAlternativa para métodos genéricosRecapitulação - Coleção genérica13Padrões de Projeto — Parte 1
Introdução aos Padrões de ProjetoPadrão SingletonPadrão FactoryPadrão Abstract FactoryPadrão ObserverPadrão Strategy2Tipos e Structs em Profundidade
Tipos Básicos e CompostosDefinições de Tipos PersonalizadosTags de StructStructs AnônimasStructs AninhadasValores Zero e PadrõesRecapitulação - Livro de Contatos5Composição em vez de herança
Por que Go não tem herançaNoções básicas de incorporação de structsPromoção de métodosIncorporação de várias structsIncorporação vs agregaçãoOcultação de métodos incorporadosRecapitulação - Hierarquia de funcionários8Tratamento de Erros e OOP
A interface errorTipos de Erro PersonalizadosEncapsulamento de Erros (fmt.Errorf)Erros Sentinelaerrors.Is() e errors.As()Panic, Defer e RecoverRecapitulação - Analisador de Arquivos3Ponteiros e Memória
Fundamentos de Ponteiros em GoPonteiros para StructsPassagem por Valor vs. ReferênciaA Função new()Coleta de Lixo em GoRecapitulação - Construtor de Lista Ligada6Polimorfismo em Go
Polimorfismo via InterfacesDuck Typing em GoRegras de Satisfação de InterfacesColeções PolimórficasInjeção de DependênciasRecapitulação - Processador de Pagamentos9Concorrência e OOP
Fundamentos de GoroutinesCanais e comunicaçãoCanais com e sem bufferInstrução selectsync.Mutex e sync.RWMutexsync.WaitGroupDesign de structs thread-safeRevisão - Worker PoolPratique por conta própria: Compilador de Go online