O que é Delegation em Kotlin?

Delegation (delegação) é um padrão de design onde um objeto repassa responsabilidades para outro. Em Kotlin, a delegação é suportada nativamente com a palavra-chave by, tanto para classes quanto para propriedades.

Em vez de herdar de uma classe, você delega a implementação para uma instância. É o famoso princípio “composição sobre herança” que fica muito fácil de aplicar em Kotlin.

Pense na delegação como um gerente que distribui tarefas. O gerente (classe principal) não faz tudo sozinho – ele delega para especialistas (objetos delegados). Quando alguém pede um relatório financeiro, o gerente repassa para o contador. O gerente sabe a quem delegar, mas não precisa saber fazer cada tarefa.

Delegação de classe

interface Repositorio {
    fun salvar(dados: String)
    fun buscar(): String
}

class RepositorioBanco : Repositorio {
    override fun salvar(dados: String) = println("Salvando no banco: $dados")
    override fun buscar() = "Dados do banco"
}

class RepositorioComLog(repo: Repositorio) : Repositorio by repo {
    override fun salvar(dados: String) {
        println("[LOG] Operação de salvar iniciada")
        // Delega para o repositório original, mas nao automaticamente neste override
    }
    // buscar() é delegado automaticamente
}

fun main() {
    val repo = RepositorioComLog(RepositorioBanco())
    println(repo.buscar()) // Dados do banco
    repo.salvar("teste")   // [LOG] Operação de salvar iniciada
}

O by repo faz com que todos os métodos da interface sejam delegados automaticamente. Você só sobrescreve o que quiser customizar.

Delegação de propriedade

Kotlin oferece delegates prontos pra uso:

import kotlin.properties.Delegates

class Configuração {
    // Inicialização preguiçosa
    val conexao: String by lazy {
        println("Conectando ao banco...")
        "Conexão estabelecida"
    }

    // Observável — executa bloco quando muda
    var tema: String by Delegates.observable("claro") { _, antigo, novo ->
        println("Tema mudou de '$antigo' para '$novo'")
    }

    // Não pode ser lido antes de ser atribuído
    var usuario: String by Delegates.notNull()
}

fun main() {
    val config = Configuração()

    println(config.conexao) // "Conectando..." + "Conexão estabelecida"
    println(config.conexao) // "Conexão estabelecida" (já inicializou)

    config.tema = "escuro"  // Tema mudou de 'claro' para 'escuro'

    config.usuario = "Karina"
    println(config.usuario) // Karina
}

Criando seu próprio delegate

import kotlin.reflect.KProperty

class FormatadoDelegate {
    private var valor = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = valor
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        valor = value.trim().lowercase()
    }
}

class Formulario {
    var email: String by FormatadoDelegate()
}

fun main() {
    val form = Formulario()
    form.email = "  KARINA@Email.COM  "
    println(form.email) // karina@email.com
}

Delegation em Kotlin é poderoso pra reduzir boilerplate, adicionar comportamentos transversais e manter o código organizado.

Delegação com múltiplas interfaces

Você pode delegar múltiplas interfaces para diferentes objetos, compondo comportamentos de forma flexível:

interface Notificador {
    fun notificar(mensagem: String)
}

interface Auditor {
    fun registrar(acao: String)
}

class NotificadorEmail : Notificador {
    override fun notificar(mensagem: String) {
        println("Email enviado: $mensagem")
    }
}

class AuditorLog : Auditor {
    override fun registrar(acao: String) {
        println("Auditoria: $acao em ${System.currentTimeMillis()}")
    }
}

class ServicoCompleto(
    notificador: Notificador,
    auditor: Auditor
) : Notificador by notificador, Auditor by auditor

fun main() {
    val servico = ServicoCompleto(NotificadorEmail(), AuditorLog())
    servico.notificar("Pedido confirmado")
    servico.registrar("PEDIDO_CRIADO")
}

Delegate para válidação de propriedades

Um caso prático é criar delegates que validam valores automaticamente:

import kotlin.reflect.KProperty

class Validado<T>(
    private val validacao: (T) -> Boolean,
    private val mensagemErro: String
) {
    private var valor: T? = null

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return valor ?: throw IllegalStateException("${property.name} nao foi inicializado")
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        if (!validacao(value)) {
            throw IllegalArgumentException("$mensagemErro: $value")
        }
        valor = value
    }
}

class Cadastro {
    var idade: Int by Validado({ it in 0..150 }, "Idade invalida")
    var nome: String by Validado({ it.isNotBlank() }, "Nome nao pode ser vazio")
}

fun main() {
    val cadastro = Cadastro()
    cadastro.nome = "Karina"
    cadastro.idade = 28
    println("${cadastro.nome}, ${cadastro.idade} anos")

    try {
        cadastro.idade = -5 // Lança IllegalArgumentException
    } catch (e: IllegalArgumentException) {
        println(e.message) // Idade invalida: -5
    }
}

Delegate com Map para configurações dinâmicas

Kotlin permite usar um Map como delegate, ideal para configurações carregadas de arquivos ou banco de dados:

class AppConfig(propriedades: Map<String, Any?>) {
    val nome: String by propriedades
    val versao: String by propriedades
    val debug: Boolean by propriedades
}

fun main() {
    val props = mapOf(
        "nome" to "MeuApp",
        "versao" to "3.2.1",
        "debug" to false
    )

    val config = AppConfig(props)
    println("${config.nome} v${config.versao} (debug=${config.debug})")
    // MeuApp v3.2.1 (debug=false)
}

Casos de Uso no Mundo Real

  • Decorator pattern: adicionar logging, cache ou métricas a uma implementação existente sem alterar o código original, como no exemplo RepositorioComLog.
  • Injeção de dependência simplificada: delegar implementações de interfaces permite trocar comportamentos facilmente em testes.
  • Propriedades lazy em Android: usar by lazy para inicializar views, adapters e outros componentes pesados somente quando necessário.
  • SharedPreferences com delegates: criar delegates personalizados que leem e gravam valores automaticamente no SharedPreferences do Android.
  • Propriedades observáveis para UI reativa: usar Delegates.observable para notificar mudanças de estado e atualizar a interface automaticamente.

Boas Práticas

  • Prefira delegação sobre herança: quando você precisa reutilizar comportamento, considere delegar para uma instância em vez de criar uma hierarquia de classes.
  • Use by lazy para inicializações caras: propriedades que envolvem leitura de arquivo, conexão de banco ou cálculos pesados devem ser lazy.
  • Mantenha delegates simples: cada delegate deve ter uma única responsabilidade.
  • Use property delegates padrão antes de criar os seus. lazy, observable, notNull e vetoable cobrem a maioria dos casos.

Erros Comuns

  • Esquecer de chamar o método original no override: ao sobrescrever um método delegado, o by não repassa automaticamente. Você precisa manter uma referência ao objeto delegado se quiser chamar a implementação original.
  • Usar by lazy com var: lazy só funciona com val, pois o valor é computado uma única vez. Para propriedades mutáveis, use Delegates.observable ou Delegates.vetoable.
  • Ciclos de dependência em delegates: se dois delegates dependem um do outro, você pode causar um loop infinito ou StackOverflowError.
  • Não considerar thread safety com lazy: por padrão, lazy é sincronizado (LazyThreadSafetyMode.SYNCHRONIZED). Se você tem certeza de que a propriedade será acessada por uma única thread, use lazy(LazyThreadSafetyMode.NONE) para melhor performance.

Perguntas Frequentes

Qual a diferença entre delegation e herança? Herança cria uma relação “é um” (is-a), enquanto delegação cria uma relação “tem um” (has-a). Delegação é mais flexível porque permite trocar o comportamento em tempo de execução e evita os problemas da herança múltipla.

Posso delegar para uma propriedade mutável? Sim, mas cuidado. Se você usa var para o objeto delegado, trocar a referência não atualiza a delegação, pois o by captura a referência no momento da construção.

O que é Delegates.vetoable? É similar ao observable, mas permite rejeitar a mudança de valor. A lambda retorna Boolean: se retornar false, o valor não é atualizado.

Delegação de classe funciona com classes abstratas? Não. A delegação com by só funciona com interfaces. Você não pode usar by para delegar a implementação de uma classe abstrata ou concreta.