No livro Introdução à arquitetura e design de software, há um grande destaque para práticas de design e orientação a objetos, assim como no conteúdo da Alura. Nesse capítulo do livro, eu e outros autores debatemos sobre a priorização do uso de interfaces no momento de modelagem e de criação dos testes.
Ao trabalhar com coleções, escolher a implementação certa para cada caso é uma tarefa difícil. Cada uma delas, como ArrayList
, LinkedList
ou HashSet
, é melhor para resolver determinas categorias de problemas. Pode ser muito arriscado escrever todo o código da aplicação dependendo de uma decisão antecipada. Apesar disso, grande parte dos desenvolvedores opta por sempre utilizar ArrayList
desde o início, sem critério algum.
Considere um DAO
de funcionários que pode listar o nome de todos que trabalham em determinado turno, devolvendo um ArrayList
com os nomes:
public class FuncionarioDao {
public ArrayList<String> buscaPorTurno(Turno turno) { ... }
}
E um código que precisa saber se um determinado funcionário esteve presente, efetuando uma busca simples na lista devolvida:
FuncionarioDao dao = new FuncionarioDao();
ArrayList<String> nomes = dao.buscaPorTurno(Turno.NOITE);
boolean presente = nomes.contains("Anton Tcheckov");
Mas a busca com contains
em um ArrayList
é, em termos computacionais, bastante custosa. Poderíamos utilizar outras alternativas de coleção, trocando o retorno de ArrayList
para HashSet
, por exemplo, pois sua operação contains
é muito mais eficiente computacionalmente, usando internamente uma tabela de espalhamento (hash table).
Para poucos funcionários, a diferença é imperceptível. Entretanto, à medida que a lista aumenta, a diferença de desempenho entre um ArrayList
e um HashSet
torna-se mais clara, e até mesmo um gargalo de performance.
O problema em realizar uma mudança de implementação como esta é que todo código que usava o retorno do método como ArrayList
quebra, mesmo que só usássemos métodos que também estão definidos em HashSet
. Seria preciso alterar todos os lugares que dependem de alguma forma desse método. Além de trabalhoso, tarefas do tipo search/replace
são um forte sinal de código ruim.
Há esse acoplamento sintático com a assinatura do método, que conseguimos resolver olhando os erros do compilador. Mas sempre existem também informações semânticas implícitas na utilização desse método, e que não são expostas pela assinatura.
Um exemplo de acoplamento semântico está em depender da
informação de que uma List
permite dados duplicados, enquanto um Set
garante unicidade dos elementos. Como problemas no acoplamento sintático são
encontrados em tempo de compilação, os semânticos somente são em execução,
daí um motivo da importância de testes que garantam o comportamento esperado.
O grande erro do método buscaPorTurno
da classe FuncionarioDao
foi atrelar todos os usuários do método a uma implementação específica de Collection
. Desta forma, alterar a implementação torna-se sempre muito mais custoso, caracterizando o alto acoplamento que tanto se procura evitar.
Para minimizar esse problema, é possível usar um tipo de retorno de método mais genérico, que contemple diversas implementações possíveis, fazendo com que os usuários do método não dependam em nada de uma implementação específica. A interface Collection
é uma boa candidata:
public class FuncionarioDao {
public Collection<String> buscaPorTurno(Turno turno) { ... }
}
Com o método desta forma, podemos trocar a implementação retornada sem receio de quebrar nenhum código que esteja invocando buscaPorTurno
, já que ninguém depende de uma implementação específica. Usar interfaces Java é um grande benefício nestes casos, pois ajuda a garantir que nenhum código dependa de uma implementação específica, pois interfaces não carregam nenhum detalhe de implementação.
Repare que é possível optar ainda por outras interfaces, como List
(mais específica) e Iterable
(menos específica). A escolha da interface ideal vai depender do que você quer permitir que o código invocador possa utilizar e realizar na referência retornada. Quanto menos específica, menor o acoplamento e mais possibilidades de diferentes implementações. Em contrapartida, o código cliente tem uma gama menor de métodos que podem ser invocados.
Algo similar também ocorre para receber argumentos. Considere um método que grava diversos funcionários em lote no nosso DAO
:
public class FuncionarioDao {
public void gravaEmLote(ArrayList<Funcionario> funcionarios)
{ ... }
}
Receber precisamente ArrayList
como argumento tem pouca utilidade; raramente necessitamos que uma coleção seja tão específica assim. Receber aqui um List
provavelmente basta para o nosso método, e permite que o código invocador passe outras coleções como argumento.
Podemos ir além e receber Collection
ou, ainda, Iterable
, caso nossa necessidade seja apenas percorrer os elementos. Neste caso, a escolha de Iterable
permitiria o maior desacoplamento possível, mas limitaria o uso dentro do método. Por exemplo, não seria possível acessar a quantidade de elementos que essa coleção possui, nem os elementos de maneira aleatória por meio de um índice.
Devemos procurar um balanço entre o desacoplamento e a necessidade do nosso código. Esta é a ideia do Princípio de Segregação de Interfaces: clientes não devem ser forçados a depender de interfaces que não usam (MARTIN, 1996b).
O desenvolvedor deve ter em mente que acoplar uma classe, que possui menos chances de alterações em sua estrutura, com outra menos estável pode ser perigoso (MARTIN, 1997). Considere a interface List
, que possui muitas razões para não mudar, afinal, ela é implementada por várias outras classes. Se ela sofresse alterações, todas as classes que a implementam teriam de ser alteradas também.
Então, consideramos que ela é altamente estável, o que significa que essa interface raramente obrigará uma mudança nas classes que a utilizam.
Uma implementação de lista (MeuProprioArrayList
), feita pelo desenvolvedor, é provavelmente mais instável do que a interface List
, já que as forças que a impedem de mudar são fracas (não há outras classes usando essa implementação). Ou seja, uma classe acoplada a essa implementação de lista eventualmente pode ser obrigada a mudar por causa de alguma alteração em MeuProprioArrayList
.
Os frameworks e as bibliotecas consagrados sempre tiram proveito do uso de interfaces, desacoplando-se o máximo possível de implementações. Tanto a Session
do Hibernate quanto a EntityManager
da JPA devolvem List
nos métodos que envolvem listas de resultados. Ao analisar a fundo as implementações atuais de Session
e EntityManager
do Hibernate, elas não retornam nem ArrayList
, nem LinkedList
, nem nenhuma coleção do java.util
, e, sim, implementações de listas persistentes de pacotes do próprio Hibernate.
Isto é possível, novamente, pelo desacoplamento provido pelo uso das interfaces. Além disso, o retorno das consultas com JPA e Hibernate são List
, para deixar claro ao usuário que a ordem é importante.
Manter a ordem de inserção e permitir acesso aleatório são características do contrato de List
, e são importantes para o resultado de consultas. Isso porque podem definir uma ordenação (order by
), uma ótima justificativa para optar por uma interface mais específica, e não usar Iterable
ou Collection
.
Nas principais APIs do Java, é fundamental programar voltado à interface. Ao usar java.io
, evitamos ao máximo nos referenciar a FileInputStream
, SocketInputStream
, entre outras. O código a seguir aceita apenas arquivos como streams:
public class ImportadoraDeDados {
public void carrega(FileInputStream stream) { ... }
}
Desta forma, não é possível passar qualquer tipo de InputStream
para a ImportadoraDeDados
. Adotar esta limitação depende do código dentro do método carrega
. Ao utilizar algum método específico de FileInputStream
, que não esteja definido em InputStream
, não há o que fazer para desacoplar o código. Caso contrário, esse método poderia (e deveria) receber uma referência na InputStream
, ficando mais flexível e podendo receber os mais diferentes tipos de streams, como argumento, que provavelmente não foram previamente imaginados. Utilize sempre o tipo menos específico possível.
Repare que, muitas vezes, classes abstratas trabalham como interfaces, no sentido conceitual de Orientação a Objetos (VENNERS, 2005). Classes abstratas possuem a vantagem de poder adicionar um novo método não abstrato, sem quebrar o código já existente. Já com o uso de interfaces (aqui, pensando na palavra-chave do Java), a adição de qualquer método acarretará a quebra das classes que a implementam.
Como interfaces nunca definem nenhuma implementação, a vantagem é que o código está sempre desacoplado de qualquer outro que as utilize. Isso muda com os polêmicos extension methods do Java 8, permitindo escrever uma implementação padrão nas interfaces, possibilitando suas evoluções ao mesmo tempo que minimiza a quebra de compatibilidade.
Os métodos load(InputStream)
(da classe Properties
) e fromXML(InputStream)
(do XStream) são ótimos exemplos de código que não dependem de implementação. Podem receber arquivos dos mais diferentes streams: rede (SocketInputStream
), upload HTTP (ServletInputStream
), arquivos (FileInputStream
), de dentro de JARs (JarInputStream
) e arrays de byte genéricos (ByteArrayInputStream
).
A Java Database Connectivity (JDBC) é outra API firmemente fundada no uso de interfaces. O pacote java.sql
possui pouquíssimas classes concretas. Sempre que encontramos um código trabalhando com conexões de banco de dados, vemos referências à interface Connection
, e nunca diretamente a MySQLConnection
, OracleConnection
, PostGreSQLConnection
ou qualquer outra implementação de um driver específico, apesar de esta possibilidade existir.
Referindo-se sempre a Connection
, deixamos a escolha da implementação centralizada em um ou poucos locais. Desta forma, fica muito fácil trocar a implementação sem modificar todo o restante do código. Isso ocorre graças ao desacoplamento provido pelo uso de interfaces.
No caso do JDBC, essa escolha por uma implementação está centralizada na classe concreta DriverManager
, que aqui age como uma factory. Ela decide por instanciar uma implementação específica de Connection
de acordo com os parâmetros passados como argumentos ao método getConnection
e conforme os possíveis drivers previamente carregados.
Connection connection =
DriverManager.getConnection("jdbc:mysql://192.168.0.33/banco");
Pode ser fácil enxergar as vantagens do uso das interfaces, mas é bem mais difícil começar a utilizá-las extensivamente no seu próprio domínio. O uso exagerado de reflection para invocar métodos – dependendo de algumas condições – pode ser muitas vezes substituído por interfaces. Assim, a decisão de qual método invocar é deixada para a invocação virtual de método que o polimorfismo promove. Isso diminui bastante a complexidade e aumenta a manutenibilidade, além de termos algum ganho de performance (BLOCH, 2008).
Isto é ainda mais gritante com o uso da instrução switch
, ou mesmo em um excessivo número de ifs
encadeados. Essa abordagem pode acoplar totalmente seu modelo, tornando necessárias mudanças frequentes nele toda vez que uma nova entidade for adicionada ao domínio (FOWLER, 1999).
Programe voltado à interface, não à implementação é outro dos príncipios de Orientação a Objetos do livro Design Patterns (VENNERS, 2005; GAMMA; HELM; JOHNSON; VLISSIDES, 1994), abordado por meio de outros exemplos no artigo Dependency Inversion Principle, de Bob Martin (1996a).
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.