Guerra contra Singletons no Android

TL, DR: Singletons são do mal. Embora instâncias únicas sejam necessárias, existem outras maneiras de implementar sem usar Singleton, e especialmente sem usar classes estáticas.


Comecemos pelo início: objetos únicos na aplicação são uma necessidade. Por exemplo, OkHTTP gerencia internamente o controle de threads e recursos alocados para requests. Se tivermos mais de uma instância, teremos problemas de performance. O mesmo é válido para classes que lidam com banco de dados, e alguns casos onde ter mais de uma instância podem gerar problemas de concorrência.

Digamos que você tenha uma funcionalidade que acesse o banco de dados, e você quer que exista apenas uma instância dessa classe na sua aplicação. Perfeitamente válido. E no mais, parece um lugar ótimo para usar um Singleton correto? Ai escrevemos:

Simples, conciso, maroto. O que poderia ter de errado com isso?


Aparentemente, muita coisa:

  • SongRepository tem uma referência estática para a Database …

… que muito provavelmente tem uma referência a um Context. Isso é pedir por um memory leak. Objetos estáticos nunca são destruídos, logo a referência ao Context nunca é removida. BOOM: memory leak.

  • E se quisermos testar o método MyFunctionality#execute?

MyFunctionality agora usa SongRepository, que por sua vez é fortemente acoplado à plataforma do Android. Se quisermos fazer testes unitários, ou usamos Roboeletric para mockar toda a plataforma Android, ou temos que rodar os testes no emulador, o que faz com que os testes deixem de ser unitários por definição.

Além disso, é bem comum que tentemos fornecer dados prontos para que nosso teste cubra só o método execute. Porém, como o método getSong é estático, não podemos sobre-escrever ele. A única opção que nos resta é adicionar objetos no banco de dados diretamente, para que eles sejam carregados. Isso deixa os testes bem mais lentos de rodar, e parece trabalho demais. E novamente faz com que deixem de ser unitários, por causa da dependência do banco de dados.

  • Quem está modificando o banco de dados?

Nesse caso, o banco de dados pode ser modificado por qualquer dev em qualquer lugar, mesmo sem querer. Pode ser que você tenha usado o repositório com sucesso uma vez, mas logo em sequência ele virou nulo, ou foi modificado, ou qualquer outra coisa. O comportamento do estado de um singleton é muito imprevisível, e pode gerar bugs bem chatos.

Com tantos problemas, deve existir maneira melhores certo?


Passo 1: Instância estática

E se o SongRepository, em vez de uma classe estática, fosse um objeto passado à MyFunctionality no construtor? Algo como:

Agora, se eu quiser testar MyFuntionality.doStuff(), eu posso fazer o seguinte:

Conseguimos desacoplar completamente a funcionalidade da implementação da persistência, o que fez rodar testes unitários normais muito mais fácil. Além de obviamente agora eu ter um código testado, isso significa que eu posso desenvolver MyFunctionality sem precisar compilar o projeto inteiro. Isso é uma das maiores vantagens: em vez de esperar 1 minuto por uma compilação, dando tempo para entrar no AndroidDevBr e perder horas lendo artigos e discussões, podemos iterar essa parte em segundos, e deixar o AndroidDevBR pra mais tarde (mas não muito).

Mas um observador atento observou um grande problema com esse approach, que já havíamos discutido antes. Um problema que está sempre a nossa volta, esperando o nosso mínimo descuido para quebrar nosso lindo e perfeito app. 5 segundos para você encontrar o problema.

5

4

3

2

1

Ding Ding Ding! Se você falou "Memory Leak" acertou! Essa solução ainda pode causar leaks, leaks malvados, horríveis, mais feios que minha cara quando acordo. A instância estática de SqliteSongRepository ainda mantém uma referência para o contexto, e Singletons nunca são limpos da memória. Como resolvemos isso?


Instâncias da Aplicação

E se, em vez do meu SqliteSongRepository manter uma referência estática para o contexto, mantivesse uma referência normal, garantindo que essa referência seja para o contexto de uma Application, e não de uma Activity? Isso pode ser resolvido através de de instâncias que pertencem à Aplicação:

Nenhum singleton, nenhuma chance de passar uma atividade como contexto para o banco de dados, sem chance de memory leak. Apenas uma forma organizada, segura, desacoplada, testável de ter uma instância única de um objeto em sua aplicação.

Só tem um probleminha: está faltando uma qualidade altamente desejável nessa solução. Escalabilidade. O que aconteceria se tivéssemos 200 instâncias únicas de classes diferentes em nossa aplicação? Adicionaríamos getters e a lógica de criação dos objetos de cada uma delas? A classe Application ficaria enorme.

A boa notícia é que existem diversas maneiras de resolver esse problema, mas a ideia de instância de aplicação permanece a mesma.

Primeira Solução: Service Locator

Service Locator é um padrão bem simples, e é uma das soluções para a Inversão de Dependência: em vez de adicionar um monte de dependência através do construtor, podemos passar um único objeto que sabe como retornar as dependências certas. Devs Android já são bem familiares com esse conceito: a classe Context, por exemplo, funciona como um Service Locator para o que a plataforma Android provê. Os métodos Context#getSystemService recebem uma classe ou um nome de um objeto que desejamos ter, e ele retorna a dependência criada.

Usando um Service Locator, ou Provider, ou Registry, ou diversos outros similares, em vez de termos várias dependências sendo inicializadas na Application, teremos apenas uma variável da aplicação (o service Locator), que seria responsável por fornecer os objetos desejados. Esse Service Locator seria passado por construtores.

A maior vantagem de mover as variáveis de instância única para o Service Locator está no fator escalabilidade: enquanto só se pode ter uma Application (que já possui responsabilidades demais por sinal), Service Locators podem ser modularizados, compostos, divididos. Em um time grande, por exemplo, cada time pode ter seu próprio Service Locator, e a aplicação sabe como compor todos eles.

Segunda Solução: Dagger2

Acredito que vários desenvolvedores leram o artigo brigando comigo por que eu não estou usando Dagger2. Afinal, Dagger2 é a solução clara para o problema dos Singletons (e todos os outros problemas que eu mencionei nesse post).

Não mencionamos Dagger2 até agora propositalmente. Não mencionamos porque Dagger2 não é a solução. Dagger2 é uma implementação da solução. A solução real por trás são os conceitos de Instância Única em vez de Singleton, Inversão de Dependência e Injeção de Dependência. Não falamos até agora sobre a biblioteca para dar mais atenção ao que está por trás dela, e de outras como Kodein, injekt e Roboguice.

Mas Dagger2 é uma solução excelente para escalar a ideia de Instâncias da Aplicação. Permite que você crie componentes modularizados responsáveis pela criação desses objetos, com um mecanismo de criação de Singletons semelhante ao descrito aqui. É uma biblioteca madura e muito utilizada pela comunidade. Se você quiser saber mais sobre como usar Dagger2, existem diversos posts explicando detalhadamente (como esse e esse publicados no AndroidDevBR).


Sobre guardar referência estática para o contexto da aplicação

Um último detalhe que gostaria de mencionar é a questão de guardar uma referência estática para a o contexto da aplicação. Apesar de não ser tão perigoso (afinal, sua aplicação vive durante a existência da aplicação, não teria perigo de memory leak), é uma desnecessária. Primeiro, não resolveria a questão do acoplamento do seu código com o Android, atrapalhando os testes. Segundo, ainda a possibilidade de passar uma Activity sem querer para um contexto. E finalmente, seguindo as opções descritas aqui, não fica mais necessário.


Concluindo, Singletons são perigosos, mas instâncias únicas são importantes na aplicação. Tomando alguns cuidados, e fácil manter um código testável e desacoplado mesmo com objetos semelhante a singletons.