Android/Java Interfaces like a Boss!

Compartilhe

Compartilhar no facebook
Compartilhar no google
Compartilhar no twitter
Compartilhar no linkedin

Durante o meu tempo como monitor na UNICAMP, percebi que as Interfaces e classes Abstratas eram (e são) os assuntos que mais deixavam dúvidas nos alunos

Absolutamente normal… Eu também passei por essa curva (longa, diga-se de passagem) de aprendizado. Porém, quando realmente entendi o conceito em si das Interfaces, a minha mente quase explodiu de tantas ideias de implementações. 🚀

Neste artigo, irei compartilhar as principais ideias, técnicas e seus conceitos, mas antes vou refrescar alguns tópicos sobre o uso de Interfaces.

Nota: Assume-se que o desenvolvimento do código no Android está sendo feito com Java 8. 

Relembrando Interfaces…

1. Interfaces são classes Abstratas

  • São classes que não podem ser instanciadas.
  • Podem ter métodos abstratos, que deverão ser sobrescritos nas classes que implementarem a Interface.

Legenda: Exemplo simples de como obter a interface através da classe que a implementa

2. Possuem modificadores específicos

  • Podem ser implementados métodos abstratos, padrões e estáticos.
  • Seus métodos podem ter apenas a visibilidade public, ou seja, seus métodos não são restritos apenas para as classes que a implementa, e sim para quem as utiliza também.

Legenda: Exemplo de modificadores nos métodos da Interface

3. Atributos são permitidos 

  • Seus atributos são estáticos e finais, mais conhecidos como constantes.

Legenda: Exemplo de declaração de constantes na Interface

4. Podem ter classes internas

  • São permitidas implementações de qualquer tipo de classe, como Enums, Interfaces, Classes, etc..
  • As classes internas são estáticas.

Legenda: Exemplo de Nested Classes na Interface

5. É possível extender de outras interfaces

  • A herança nas Interfaces ocorrem apenas através da palavra reservada extends.

Like a Boss!

Assumindo as informações passadas, agora serão apresentadas algumas técnicas e conceitos da utilização das Interfaces. 

Alguns assuntos ainda provocam muitas discussões mundo afora — como o conceito de Callback; a relação entre Callback x Listeners; a aplicação de métodos defaults em confronto com o conceito original de Interfaces em Default Functionalities.

Callbacks

Muito utilizado em bibliotecas que auxiliam na comunicação com APIs, como Volley e Retrofit, o conceito de Callback se aproxima bastante do Design Pattern Command. Para explicar, nos serviremos do seguinte exemplo “simplificado”:

Com o objetivo de abrir um arquivo, criamos uma classe chamada OpenFileHelper que contém todo o código que fará tal operação. 

Para que a classe que for utilizar o OpenFileHelper receba o arquivo aberto ou receba um erro, criamos a Interface OpenFileHelperCallback com os métodos necessários.

Neste exemplo, recebemos o Callback do OpenFileHelper no construtor e para uso posterior, atribuímos seu valor a uma variável de instância. Ficaria algo assim:

Legenda: Callbacks — OpenFileHelper e seu Callback

Assim, a classe que for utilizar o OpenFileHelper deverá instanciá-lo já com o Callback que receberá o arquivo ou o erro. Para isso, a classe poderá passar uma classe anônima, ou implementar o Callback criado passando this. Por exemplo:

Legenda: Callbacks — Dois exemplos de receber o retorno do OpenFileHelper

Em resumo…

Muito debatido no mundo da programação, para ser um Callback, os retornos feitos pela classe que executa a operação devem ser únicos para cada execução, ou seja, um para um. Utilizando-se do exemplo acima, para cada arquivo que for aberto, existirá uma chamada de retorno.

Listeners

Encontrado principalmente na captação de eventos em botões, se aproxima muito do Design Pattern Observer. Veja um exemplo, no clássico método onClick provenientes da classe View.OnClickListener do Android:

Legenda: Listeners — Muito usado no setOnClickListener de Views no Android

É comum confundirem o conceito dos Listeners com os Callbacks, pois ambos possuem o comportamento de “avisar alguém que algo ocorreu”, porém, existem diferenças.

Como é possível ver no exemplo do botão, ao configurar o clique dele, precisamos passar a implementação que receberá os eventos de clique. Do outro lado, na classe Button, o Listener recebido fará o papel de “observador”, avisando a classe que criou o Listener sobre os eventos ocorridos. 

No Callback, a classe quem a criou pede pela sua execução e aguarda seu retorno, já o Listener, assim que registrado, pode responder várias vezes sem que seja pedido sua execução.

Além disso, é muito comum que a classe responsável por disparar eventos tenha uma função para remover o Listener, para quando não for mais desejado ouvir seus eventos.

Contratos

Muito utilizado em arquiteturas que se utilizam de Inversão de Controle e Injeção de Dependências, os contratos são comportamentos que uma determinada classe precisa para executar suas operações

O objetivo dos contratos não é descrever uma classe, e sim seu comportamento.

Como exemplo, irei utilizar a arquitetura Model-View-Presenter (de forma bem simplificada e com foco nos contratos) em uma funcionalidade de gerenciamento de conta de um usuário (Account Manager).

A nossa View tem a função de: 

  • Registrar os componentes (TextView, EditText, etc.)
  • Definir as ações dos componentes (button clicks, show loadings, etc.)
  • Abrir outras telas

O nosso Presenter tem a função de:

  • Validar os erros das chamadas da API
  • Intermediar as chamadas da API com a View

Em nosso Account Manager, a View possui o comportamento de avisar o usuário sobre o que ocorreu e está ocorrendo, e o Presenter possui o comportamento de atualizar e remover, através de chamadas da API, a conta do usuário. 

Para a nossa implementação, o Presenter precisa obrigatoriamente de um contrato para funcionar, que é o meio que ele irá avisar a View sobre os resultados de suas operações. Já a View não precisa de alguém para funcionar, e sim de alguém que execute determinadas operações para si. Sendo assim, nossas interfaces ficariam:

A classe concreta que implementa o contrato da View:

E a classe concreta que implementa o Presenter:

Observe que o Presenter só pode ser usado se ele receber em seu construtor o contrato que ele precisa para fazer suas operações (ViewContract).

De forma sucinta, o contrato define uma dependência de determinado comportamento para alguém funcionar.

Markers

Os marcadores são Interfaces que não possuem qualquer método. Seu uso é raro, porém, muito útil para determinadas tarefas. Um exemplo de fácil acesso é a classe do próprio java.io, Serializable.

A serialização das classes que se utilizam dessa interface ocorrem através de reflections. Ou seja, durante a criação do objeto, caso a classe implemente a interface Serializable é executado a serialização da mesma.

Outro caso é quando queremos separar a responsabilidade de determinadas classes. Por exemplo, supondo que queiramos ter uma classe que dispare logs através do System.Out e do LogCat. O comportamento das duas estratégias é o mesmo, porém, o LogCat precisa de uma TAG para executar seu log. Sendo assim, teríamos uma Interface comum, e outras duas que derivam dessa interface comum:

Logger é nossa interface comum. SystemLogger e LogCatLogger são as Interfaces que estão servindo de marcação. Isso nos indica que quem for usá-las não precisa saber se será feito um log através do System.Out ou LogCat, apenas que precisa ser executado um log. Vejamos:

A classe LoggerDispatcher é quem coordena os logs. Reforçando, essa classe não precisa saber qual estratégia está sendo tomada para executar qualquer log. Quem for usar o LoggerDispatcher, usará diretamente sua interface:

Neste caso, está sendo efetuado os logs nas duas maneiras. Isso seria mais útil se, por exemplo, fossem realmente duas plataformas diferentes para os logs.

Existem diversos casos de uso para os marcadores, principalmente quando buscamos a generalização e reutilização de código.

Lambdas & Functional Interfaces

De forma simples, as Interfaces Funcionais são Interfaces que possuem apenas um método abstrato. O Java possui ainda, a notação @FuncionalInterface disponível para Interfaces que cumprem tal objetivo.

Usando-se do exemplo dos logs, a interface Logger é uma Interface funcional, e pode ser anotada com @FunctionalInterface.

A grande vantagem de uma Interface Funcional é sua utilização em Lambdas.

A grosso modo, os Lambdas são classes anônimas que possuem apenas um método, que para serem usadas, não necessitam da criação de sua classe anônima, pois, são criadas implicitamente!

Com exemplo fica mais claro:

O Lambda simplificou a leitura da nossa implementação, pois, implicitamente é criado a classe anônima. Vejamos algumas chamadas de exemplo:

// Não recebe nada e sempre retorna “69”

(() -> 69)

// Recebe algo e retorna seu quadrado

(x -> x * x)

// Recebe dois valores e retorna sua soma

((x,y) -> x + y)

((int x, int y) -> x + y)

((x, y) -> { return x + y; })

((int x, int y) -> {

   System.out.println(x + y);

   return x + y;

})

 

Reforçando, apenas podemos ter funções Lambdas quando é implementado apenas um método na Interface!

Retro & Compatibilidade

É comum a necessidade de adicionarmos novas funcionalidades em classes que já possuem seu escopo fechado  —  como classes abstratas com heranças profundas, em que é arriscada sua alteração  —  ou adaptar novas funcionalidades em classes depreciadas, porém, ainda utilizadas. Foi com este objetivo que os métodos default surgiram a partir do Java 8.

Para a Retrocompatibilidade, vamos nos aproveitar do exemplo usado na seção dos Contratos.

Vamos supor que os métodos onShowLoading(), onHideLoading() e onShowError(String error) são métodos herdados de uma BaseViewContract:

A nossa MainActivity permanece da mesma forma, com as mesmas implementações. Porém, vamos supor que todos os contratos de View estendem de BaseViewContract. Isso faz com que, se adicionarmos um método ao BaseViewContract, ou removermos um método do mesmo, implicará em alterações de todas as classes concretas que implementam os contratos de View!

Caso quiséssemos, em novas implementações, permitir a exibição de um Toast nas classes que usassem os contratos da View, teria que ser feito usando um método default:

Resolvemos o nosso problema de retrocompatibilidade! Porém, agora temos outro problema…

Na arquitetura MVP, não é recomendado o uso do Context pelo Presenter. Ou seja, não deveríamos fazer o uso da chamada 

contractView.onShowToast(context, “someMessage”);

Neste caso, entra em ação a Compatibilidade.

Supondo que todos os contratos de View são implementados por Activities, então todas possuem o método getApplicationContext(). Com isso, se adicionássemos o método getApplicationContext() ao nosso BaseViewContract, não faria diferença para as Activities. Sendo assim, para que os Presenters não precisem do Context para exibir Toasts

Pronto! Nossa arquitetura não foi quebrada! E as nossas Activities nem precisaram ser alteradas com essa nova funcionalidade! Muito útil isso, não?! ❤ 

Funcionalidades Default

Muito próximo da ideia de compatibilidade, as funcionalidades default são Interfaces que possuem o objetivo de prover um determinado comportamento sem precisar de qualquer implementação a mais.

De certa forma, o uso de Interfaces com esse objetivo é questionável, já que as Interfaces foram criadas para intermediarem as abstrações, e não definir as ações de seus comportamentos.

Mas, vamos lá…

Exibir logs no LogCat do Android Studio, durante o debug, pode ser algo muito útil para entender os fluxos. Seria muito útil se durante o impressão do log fosse anexado também o nome do método que está chamando o log. Ou talvez exibir apenas o nome do método que está sendo executado. Porém, seria cansativo para todo log que quiséssemos:

  • Copiar e colar o nome do método que está sendo executado em todo Log.d
  • Adicionar uma validação para exibir ou não o log.
  • Ter que instanciar um novo objeto toda vez, caso nos utilizássemos de composição.

Entre outras coisas…

Uma possível solução seria criar uma Interface que possua métodos default que execute as tarefas que queremos:

Seria possível melhorar muito o código caso tivéssemos métodos privados, pois queremos que as classes que implementam essa interface, tenham contato apenas com os métodos corretos.

Porém, mesmo sem os métodos privados, podemos criar uma classe privada no package com métodos estáticos. Isso solucionaria totalmente nosso problema:

E seu uso seria simples assim:

O uso desta estratégia é interessante apenas quando nossos métodos são puros, ou seja, não possuem efeitos colaterais, tendo seu código estrito ao escopo do seu respectivo método. 

Em outras palavras, se o código a ser adicionado no escopo do método não sofrer alterações externas, essa estratégia é válida.

Bibliografia:

Carlos Eduardo Zanco Batistão — Design Pattern Command (https://cezbatistao.wordpress.com/2016/05/21/design-pattern-command/)

Daniel Dee — Callback x Listener (http://blog.danieldee.com/2009/06/callback-vs-listener.html)

StackOverFlow — Como funciona e como implementa o design pattern Observer (https://pt.stackoverflow.com/questions/36655/como-funciona-e-como-implementar-o-design-pattern-observer)

DevMedia — Inversão de Controle (https://www.devmedia.com.br/introducao-a-inversao-de-controle/29698)

TriadWorks — Interfaces Funcionais (http://blog.triadworks.com.br/interfaces-funcionais)

TriadWorks — Lambdas (http://blog.triadworks.com.br/lambda-lambda-lambda-java)

Quora — Should I avoid using default methods in Java interfaces? (https://www.quora.com/Should-I-avoid-using-default-methods-in-Java-interfaces)

Oracle — Default Methods (https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)

StackOverFlow — What exactly is in the contract (https://stackoverflow.com/questions/9948211/java-interfaces-what-exactly-is-in-the-contract/9948302#9948302)

Sarath Manchu — Java 8 static default methods (https://medium.com/@sarathmanchu_9050/java-8-static-default-methods-dd2221579b2d)

Jeff Okawa — Default Implementations on Interfaces (https://medium.com/@gepphkat/default-implementations-on-interfaces-bd27d3ee168a)

Ben Weidig — Java 8 interfaces: default methods for backwards compatibility (https://medium.com/@benweidig/java-8-interfaces-default-methods-for-backwards-compatibility-2767a6a70947)

 

 

Guilherme Silva

Guilherme Silva

Ex-aluno e precursor do curso de Android na Faculdade de Tecnologia da UNICAMP para o curso de Engenharia de Telecomunicações. Iniciando sua carreira de desenvolvimento há mais de 5 anos, conta com experiência em empreendedorismo, consultorias, entre outros, sendo hoje Engenheiro de Software no iFood. Violão, fotografia e design são seus principais hobbies, mas nunca nega um aprendizado sobre qualquer assunto.

Deixe um comentário

%d blogueiros gostam disto: