CodeGym /Cursos /Kotlin SELF /super e ordem de inicializacao: estendendo a logica base

super e ordem de inicializacao: estendendo a logica base

Kotlin SELF
Nível 35, Lição 2
Disponível

1. Introducao

Na aula passada, aprendemos a sobrescrever metodos e propriedades com override e a abrir os pontos de extensao necessarios com open; tambem conhecemos protected como “acessivel a classes derivadas, mas nao ao mundo externo”. Hoje, vamos adicionar uma habilidade pratica importante: como nao quebrar a logica base ao sobrescrever um comportamento e por que, as vezes, super nao significa “eu desisti”, e sim “eu fiz do jeito certo”. E ainda vamos analisar a ordem de inicializacao, por causa da qual iniciantes frequentemente sentem que “o Kotlin esta me trollando”.

Quando voce sobrescreve um metodo, aparece uma tentacao: “pronto, agora eu mando; a classe base pode se aposentar”. Mas, em codigo real, a classe base frequentemente faz algo importante: valida entradas, normaliza texto, registra logs, calcula alguns campos, mantem invariantes. Se voce “cortar” esse comportamento, o programa pode comecar a agir de forma estranha — e sem nenhum erro de compilacao. E e ai que super entra como uma pessoa educada que diz: “primeiro fazemos a parte base, depois eu adiciono a minha”.

Em Kotlin, a palavra-chave super permite, a partir de um membro sobrescrito, acessar a implementacao da classe base (do metodo ou do acessor de propriedade). A formulacao oficial costuma soar mais ou menos assim: “o codigo na classe derivada pode chamar a implementacao da superclasse via super”.

A ideia mais importante: super nao e necessario sempre, mas, se a classe base executa passos obrigatorios, super e a forma de estender, e nao de quebrar.

2. super em um metodo sobrescrito

Normalmente a vida nao pede “substitua o metodo inteiro”, e sim “faca a mesma coisa, mas com um extra”. Por exemplo: a classe base ja sabe imprimir uma mensagem, e a derivada quer adicionar um prefixo. Ou a classe base valida uma string, e a derivada adiciona mais uma verificacao — mas ainda quer usar a validacao base.

Primeiro, vamos ver um exemplo minimo para pegar a intuicao:

open class Logger {
    open fun log(message: String) {
        println("[LOG] $message")
    }
}

class TaggedLogger : Logger() {
    override fun log(message: String) {
        super.log("TAG: $message")
    }
}

fun main() {
    val logger = TaggedLogger()
    logger.log("Hello")
    // [LOG] TAG: Hello
}

Aqui, a classe derivada nao reinventa a impressao: ela reutiliza o “como exatamente exibir o log” e so adiciona a sua parte.

Agora vamos levar a ideia para o nosso contexto de estudo (um aplicativo de console para controle de gastos). Imagine que queremos imprimir “linhas de relatorio”. A classe base e responsavel pelo formato geral (por exemplo, identacao e simbolos consistentes), e as derivadas adicionam o conteudo especifico.

open class ReportLine {
    open fun render(): String = "- (empty)"
}

class TextLine(private val text: String) : ReportLine() {
    override fun render(): String = super.render() + " $text"
}

fun main() {
    val line = TextLine("hello")
    println(line.render())
    // - (empty) hello
}

O exemplo e propositalmente “bobalhao”, mas ele mostra a mecanica: super.render() e a parte base do formato; depois adicionamos a nossa. Na vida real, o render() base vai fazer algo mais util do que escrever "empty", e e justamente por isso que vale a pena estender via super, em vez de copiar o formato para cada derivado.

Uma versao um pouco mais realista: a classe base garante que a linha do relatorio nunca seja vazia (por exemplo, retorna "(no data)"), e a derivada adiciona detalhes sem esquecer esse contrato.

3. super e propriedades: acesso ao getter

Para iniciantes, muitas vezes parece que propriedade e “apenas um campo”, e metodo e “logica”. Em Kotlin, essa e uma ilusao perigosa: propriedade quase sempre significa getter (e as vezes setter), isto e, “um pequeno metodo por baixo dos panos”. Por isso, super.someProp nao e magia: e “chame a implementacao do getter da classe base”.

Na documentacao sobre heranca isso costuma ser mostrado assim: a classe derivada pode acessar a propriedade da superclasse via super.property e usa-la como parte da sua logica.

Exemplo:

open class Document {
    open val title: String = "Untitled"
}

class NamedDocument(private val name: String) : Document() {
    override val title: String
        get() = super.title + ": " + name
}

fun main() {
    val doc = NamedDocument("Budget report")
    println(doc.title)
    // Untitled: Budget report
}

Repare no detalhe: nos sobrescrevemos o val title nao como um “valor armazenado”, mas via get(). Isso e pratico quando o “valor” precisa ser calculado, e nao guardado. E sim, super.title aqui chama o getter base.

Em projetos reais, isso aparece bastante para “estender uma descricao”. A classe base guarda um trecho comum; a derivada adiciona um sufixo. E se, na classe base, a descricao tambem for calculada (e nao apenas armazenada), super ajuda a reutilizar essa logica base.

4. Ordem de inicializacao na heranca

Antes de chegarmos aos pontos traiçoeiros, vamos entender em que ordem um objeto e criado quando existe uma classe base e uma derivada. Muitos bugs de heranca parecem “por que meu campo ainda e 0/null, se eu ja atribui?!”, e quase sempre a resposta e a ordem de inicializacao.

A regra intuitiva e: ao criar um objeto da classe derivada, primeiro e criada a “parte base” do objeto, e so depois a “parte da derivada”. Isso faz sentido, mas ha um detalhe importante: antes da inicializacao da classe base, ainda sao avaliados os argumentos para chamar o construtor dela. Em Kotlin, isso e descrito de forma direta: a inicializacao base acontece primeiro (tirando a avaliacao dos argumentos do construtor da superclasse), e isso significa que as propriedades sobrescritas na derivada ainda nao foram inicializadas quando o codigo do construtor base ou do init e executado.

Vamos ver um exemplo demonstrativo imprimindo as etapas. Aqui, de proposito, usamos also { println(...) } para mostrar “em que momento cada coisa foi avaliada”:

open class Base(name: String) {
    init {
        println("Base.init, name=$name")
    }

    open val size: Int = name.length.also {
        println("Base.size initialized: $it")
    }
}

class Derived(name: String, private val lastName: String) : Base(
    name.uppercase().also { println("Argument for Base: $it") }
) {
    init {
        println("Derived.init, lastName=$lastName")
    }

    override val size: Int = (super.size + lastName.length).also {
        println("Derived.size initialized: $it")
    }
}

fun main() {
    println("Constructing...")
    Derived("hello", "world")
}

Saida esperada (mais ou menos assim; a ordem importa mais do que o texto exato):

Constructing...
Argument for Base: HELLO
Base.init, name=HELLO
Base.size initialized: 5
Derived.init, lastName=world
Derived.size initialized: 10

Se tirarmos um “esqueleto” do que esta acontecendo, fica este fluxo:

flowchart TD
    A["Criamos Derived(...)"] --> B["Avaliamos os argumentos para Base(...)"]
    B --> C["Inicializacao de Base: propriedades + blocos init"]
    C --> D["Inicializacao de Derived: propriedades + blocos init"]

E por isso que a classe base precisa ser cautelosa: durante a inicializacao dela, a derivada ainda nao esta “montada”.

Alias, se voce quiser checar rapidamente a ordem no seu proprio codigo, um println dentro de init { } e uma ferramenta de debug honesta. O Kotlin nao julga (o seu futuro lider de equipe vai julgar um pouco, mas isso ja e outra historia).

5. Por que chamadas open em init sao perigosas

Agora chegamos ao ponto em que heranca deixa de ser um gatinho fofo e vira um gatinho que derruba a caneca da mesa porque “e o que voce vai fazer a respeito”. O problema e o seguinte: se a classe base, dentro de init, chama um metodo open ou acessa uma propriedade open, o Kotlin pode chamar a sobrescrita na classe derivada, mesmo que a derivada ainda nao tenha inicializado seus campos.

Isso nao e uma “peculiaridade do compilador”; e uma consequencia logica do polimorfismo: a chamada precisa ir para a implementacao mais especifica. A documentacao alerta explicitamente: ao projetar uma classe base, vale evitar usar membros open em construtores, inicializadores de propriedades e blocos init.

Vamos ver um exemplo que parece inocente, mas pode quebrar:

open class BaseGreeter {
    init {
        // Bad idea: the open method will be called at a time
        // when the derived class may not be ready yet.
        println(buildGreeting())
    }

    open fun buildGreeting(): String = "Hello from Base"
}

class FancyGreeter : BaseGreeter() {
    private val prefix: String = "[Fancy]"

    override fun buildGreeting(): String = "$prefix Hello from Fancy"
}

fun main() {
    FancyGreeter()
}

A primeira vista, parece tudo ok: prefix foi declarado. Mas a ordem faz com que BaseGreeter.init rode antes da inicializacao de FancyGreeter (suas propriedades e init). Em alguns cenarios, isso faz com que a sobrescrita tente usar um estado que ainda nao esta pronto.

Para deixar o problema o mais claro possivel e garantir o erro, vamos usar lateinit. Isso e uma situacao real: voce adia a inicializacao, mas, se houver uma chamada precoce, vai receber uma excecao.

open class BaseFormatter {
    init {
        println(format()) // risk
    }

    open fun format(): String = "(base)"
}

class UserFormatter : BaseFormatter() {
    private lateinit var userName: String

    init {
        userName = "Alice"
    }

    override fun format(): String = "User=$userName"
}

fun main() {
    UserFormatter()
    // UninitializedPropertyAccessException: lateinit property userName has not been initialized
}

Esse e exatamente o momento “por que isso foi parar na derivada?”. Foi parar porque o metodo e open, e o objeto real e UserFormatter.

Como corrigir? A ideia e simples: a classe base nao deve depender de sobrescritas para conseguir se inicializar. Um padrao seguro e ter, na classe base, um metodo final chamado no init, e colocar a extensao em um ponto separado que voce chama depois que o objeto com certeza esta pronto.

Por exemplo: o init base imprime apenas a parte base; e o “extra” voce pode chamar depois de criar o objeto (a partir de codigo externo ou do init da derivada, quando ela ja esta pronta).

open class SafeBaseFormatter {
    init {
        println(baseFormat()) // base part, not open
    }

    fun baseFormat(): String = "(base safe)"
}

class SafeUserFormatter : SafeBaseFormatter() {
    private val userName: String = "Alice"

    fun fullFormat(): String = "User=$userName"
}

fun main() {
    val f = SafeUserFormatter()
    println(f.fullFormat()) // User=Alice
}

Sim, isso e um pouco menos “magico”, mas e previsivel. E previsibilidade e praticamente a principal moeda na programacao (depois do cafe).

6. Pequeno passo no projeto: formatacao de linhas do relatorio

Para fixar o tema sem ficar em “retangulos abstratos”, vamos encaixar super em um pequeno trecho do nosso aplicativo de console de controle de gastos. Imagine que ja temos uma lista de despesas e imprimimos um relatorio. Queremos que todas as linhas do relatorio tenham um estilo comum: identacao, marcador, limite de comprimento, e que as derivadas adicionem detalhes sem copiar esse estilo manualmente.

Vamos fazer uma classe base ReportLine que sabe gerar o “cabecalho da linha”, e as derivadas adicionam o conteudo:

open class ReportLine {
    protected fun bullet(): String = "•"

    open fun render(): String = "${bullet()} "
}

class TextReportLine(private val text: String) : ReportLine() {
    override fun render(): String = super.render() + text
}

fun main() {
    println(TextReportLine("Expenses").render())
    // • Expenses
}

Aqui ja da para ver duas ideias do dia: super.render() adiciona a parte comum, e protected deixa o metodo “de servico” bullet() acessivel a derivadas, mas nao ao mundo externo.

Agora vamos adicionar uma linha de “total”, em que queremos reforcar o formato: a parte base continua igual para todos, mas a palavra "TOTAL" e adicionada. Aqui eu mostro de proposito que da para chamar super ate no meio da string — e uma funcao como qualquer outra.

class TotalReportLine(private val total: Int) : ReportLine() {
    override fun render(): String = super.render() + "TOTAL = $total"
}

fun main() {
    println(TotalReportLine(1200).render())
    // • TOTAL = 1200
}

E agora um exemplo em que super e usado em propriedades, porque “titulo” tambem fica bom como propriedade:

open class TitledLine {
    open val title: String = "line"

    open fun render(): String = "[$title]"
}

class CategoryLine(private val category: String) : TitledLine() {
    override val title: String
        get() = super.title + ":" + category
}

fun main() {
    println(CategoryLine("Food").render())
    // [line:Food]
}

Aqui, super.title acessa o getter base. Mesmo que o title base passe a ser calculado, o codigo da derivada nao precisa ser reescrito.

E a moral pratica: super ajuda a garantir que o estilo comum do relatorio viva em um lugar so. Quando voce decidir trocar o marcador "•" por "-", nao vai precisar procurar 15 copias de formatacao espalhadas pelo projeto.

Quando chamar super e quando nao

Na pratica, “sempre chame super” e um conselho ruim, e “nunca chame super” tambem. Voce precisa de um criterio: a implementacao base faz parte de um contrato obrigatorio, ou voce realmente esta substituindo a logica?

E util manter esta tabelinha na cabeca:

Situacao O que geralmente significa O que fazer com mais frequencia
O metodo base valida dados, registra log, atualiza um contador Infraestrutura importante; sem isso, a classe pode ficar “quebrada” Chamar super e adicionar a sua parte antes/depois
O metodo base retorna um “placeholder” ou um padrao A base nao carrega significado; e apenas “para existir” Voce pode substituir totalmente sem super
A sobrescrita muda o formato, mas o esqueleto geral e o mesmo A parte comum e necessaria para todas as derivadas super + complemento
A classe base chama um metodo open dentro de init Risco de chamada precoce na derivada Refatorar o design: super nao resolve aqui

E uma regra separada, que vale escrever em um post-it: se a classe base faz algo importante para a corretude e voce nao chamou super, seu codigo pode funcionar “quase sempre” — e esse e o modo de falha mais traiçoeiro.

7. Erros tipicos

Erro nº 1: “Eu sobrescrevi o metodo — entao a logica base nao e mais necessaria”.
Esse tipo de abordagem frequentemente quebra invariantes da classe base: por exemplo, ela normaliza dados de entrada, mantem um contador, preenche campos auxiliares ou imprime uma parte obrigatoria do relatorio. Se voce nao chama super, voce nao esta apenas “mudando o comportamento”; voce pode cancelar sem querer uma parte importante do contrato.

Erro nº 2: chamar super “no lugar errado”, e a logica fica estranha.
As vezes a ordem importa: o metodo base pode assumir que sua chamada e o primeiro passo (por exemplo, validacao de argumentos) ou, ao contrario, o ultimo passo (por exemplo, finaliza o estado). Se voce chama super depois de ja ter feito acoes que dependem da preparacao base, voce ganha bugs do tipo “parece tudo certo, mas o resultado fica torto”.

Erro nº 3: acessar super.someProperty esperando que seja apenas um campo, e nao um calculo.
Em Kotlin, propriedade e, na pratica, metodos get()/set(). super.title pode ser calculado, pode acessar outros campos e ate ter efeitos colaterais (embora isso nao seja recomendado). Por isso, super.property deve ser visto como chamada de logica, e nao como leitura de memoria.

Erro nº 4: chamar metodos/propriedades open a partir do init da classe base.
Esse e um dos bugs “profissionais” mais desagradaveis: o codigo compila, as vezes ate roda, mas quando a classe derivada fica mais complexa, comeca a falhar porque os campos “ainda nao estao prontos”. O Kotlin alerta separadamente sobre esse padrao: ao projetar a classe base, e melhor evitar chamadas open no construtor e no init.

Erro nº 5: tentar resolver a ordem de inicializacao com “mais um lateinit”.
lateinit e uma ferramenta util, mas nao torna a ordem de inicializacao mais segura. Pelo contrario: se a chamada open acontecer cedo demais, voce vai receber uma excecao. O tratamento correto nao e “adiar ainda mais a inicializacao”, e sim garantir que a classe base nao dependa de sobrescritas para ser criada, levando em conta a ordem de inicializacao na hierarquia.

1
Tarefa
Kotlin SELF, nível 35, lição 2
Bloqueado
Saudação de amigo
Saudação de amigo
1
Tarefa
Kotlin SELF, nível 35, lição 2
Bloqueado
Título da nota
Título da nota
1
Tarefa
Kotlin SELF, nível 35, lição 2
Bloqueado
Rastro de inicialização
Rastro de inicialização
1
Tarefa
Kotlin SELF, nível 35, lição 2
Bloqueado
Formatador seguro
Formatador seguro
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION