Na Programação Orientada a Objetos em Java, quando utilizamos interfaces, é obrigatório adicionar os métodos da mesma em cada classe que a implemente. Mesmo que um deles não precise executar nada, é necessário manter ao menos a sua assinatura na classe. Respeitando o contrato, chega um momento em que os métodos implementados da interface em várias classes tornam-se idênticos e repetitivos, levando o desenvolvedor a copiar e colar código para ganhar tempo de codificação, replicando os métodos iguais de uma classe para outra.
Até o Java 7, a melhor solução para não cair nessa prática era o uso de padrões de projetos, como o Strategy, que permitem reaproveitar métodos já implementados. Pensando nesse problema, na versão 8 da linguagem foi introduzido o recurso Default Methods. Com ele, deixa de ser necessário implementar todos os métodos da interface, o que possibilita redução no tempo gasto com o desenvolvimento e simplifica o uso desse tipo de estrutura.
Saiba mais sobre: Java 8
Opções antes dos Default Methods
Para evitar lidar com a necessidade de implementar todos os métodos de uma interface em todas as classes que a estendem, os desenvolvedores geralmente adotam uma das seguintes “técnicas”:
- Copiar e colar;
- Desistir de utilizar interfaces e utilizar classes abstratas;
- Usar o padrão de projeto Strategy.
Nas Listagens 1, 2 e 3 veremos como cada uma dessas opções se aplica a um sistema desenvolvido para uma autoescola, o qual consiste de um simulador que pode conter vários tipos de veículos para o aluno selecionar, por exemplo, quando desejar fazer uma aula.
Nos códigos que serão analisados, trabalharemos com três opções de veículos, representadas por três classes. Dessas, duas são idênticas, fato que permite a má prática de copiar e colar código.
public interface IAceleracao { void acelerar(); void desacelerar(); } public abstract class Veiculo implements IAceleracao{ Motor motor = new Motor(); } public class Gol extends Veiculo { @Override public void acelerar() { System.out.println("Aceleração Constante"); } @Override public void desacelerar() { System.out.println("Desaceleração Constante"); } } public class Fiesta extends Veiculo { @Override public void acelerar() { System.out.println("Aceleração Constante"); } @Override public void desacelerar() { System.out.println("Desaceleração Constante"); } } public class Maverick extends Veiculo { public Maverick() { motor = new Motor("V8"); } @Override public void acelerar() { System.out.println("Aceleração Super Rápida"); } @Override public void desacelerar() { System.out.println("Desaceleração Lenta"); } }
- Linhas 1 a 4: Código da interface IAceleracao com os métodos acelerar() e desacelerar();
- Linhas 6 a 8: Código da classe abstrata, que, além de implementar a interface, adiciona também um atributo, referente ao motor do veículo;
- Linhas 10 a 20: Código da classe Gol, que estende a classe Veiculo. Note que aqui implementamos os métodos acelerar() e desacelerar() da interface IAceleracao;
- Linhas 22 a 32: Implementação da classe Fiesta. Observe que ela é idêntica à da classe Gol, o que permitiu ao desenvolvedor copiar e colar o código sem fazer qualquer revisão;
- Linhas 34 a 49: Código da classe Maverick. Note que ela contém uma implementação diferente das classes Gol e Fiesta, assim, não é possível apenas copiar o código.
Apesar de muitas vezes esse se mostrar o caminho mais rápido, copiar e colar código é considerado uma das piores práticas de desenvolvimento, pois se torna um multiplicador de erros e vai de encontro a um dos objetivos básicos da Orientação a Objetos: o reuso.
Como alternativa a essa prática, a Listagem 2 apresenta uma segunda solução para a implementação do mesmo simulador: desistir do uso de interface e aplicar apenas herança. Nesse novo modelo deixamos os métodos com implementação padrão na superclasse e sobrescrevemos as variações dele somente naquelas que precisarem.
public abstract class Veiculo{ Motor motor = new Motor(); public void acelerar(){ System.out.println("Aceleração Constante"); } public void desacelerar(){ System.out.println("Desaceleração Constante"); } } public class Gol extends Veiculo { } public class Fiesta extends Veiculo { } public class Maverick extends Veiculo { public Maverick(){ motor = new Motor("V8"); } @Override public void acelerar() { System.out.println("Aceleração Super Rápida"); } @Override public void desacelerar() { System.out.println("Desaceleração Lenta"); } }
- Linhas 1 a 11: Código da classe abstrata Veiculo, na qual se pode notar o atributo motor e a implementação dos métodos acelerar() e desacelerar(), com a finalidade de dispensar o uso de interfaces;
- Linhas 13 e 15: Código das classes Gol e Fiesta. A ausência de conteúdo aqui indica que elas herdarão os métodos implementados da classe abstrata;
- Linha 17 a 31: Código da classe Maverick, que utiliza polimorfismo para sobrescrever os métodos herdados da classe Veiculo.
Nesse exemplo, embora pareça que tenhamos resolvido o problema e o código tenha ficado mais limpo e de fácil manutenção, a solução perdeu em flexibilidade, em relação à opção anterior (com interfaces). Além disso, apesar de nesse caso ter funcionado, em outras situações se torna inviável resolver o problema apenas com herança. Por exemplo, se houvesse a necessidade de aumentar o número de classes que herdam de Veiculo, contendo cada uma implementações diferentes para os métodos, o uso de polimorfismo em relação aos métodos seria frequente, aumentando proporcionalmente a complexidade e a manutenção do sistema. Isso acaba prejudicando também a aplicação de boas práticas da Programação Orientação a Objetos, que presam por reusabilidade e fraco acoplamento entre objetos (o que se obtém pelo uso correto de interfaces).
A próxima alternativa, e que se mostrará a melhor opção até aqui, se baseia na utilização de padrões de projeto. Observe na Listagem 3 a solução proposta para o mesmo simulador, mas dessa vez aplicando o design pattern Strategy.
public interface IAceleracao { void acelerar(); void desacelerar(); } public class AceleracaoMotorComum implements IAceleracao { @Override public void acelerar() { System.out.println("Aceleração Constante"); } @Override public void desacelerar() { System.out.println("Desaceleração Constante"); } } public class AceleracaoMotorV8 implements IAceleracao{ @Override public void acelerar() { System.out.println("Aceleração Super Rápida"); } @Override public void desacelerar() { System.out.println("Desaceleração Lenta"); } } public abstract class Veiculo{ Motor motor = new Motor(); IAceleracao acelerador = new AceleracaoMotorComum(); public void acelerar(){ acelerador.acelerar(); } public void desacelerar(){ acelerador.desacelerar(); } } public class Gol extends Veiculo {} public class Fiesta extends Veiculo {} public class Maverick extends Veiculo { public Maverick() { acelerador = new AceleracaoMotorV8(); motor = new Motor("V8"); } }
- Linhas 1 a 4: Código da interface IAceleracao, que representa o comportamento de aceleração do motor, com os métodos acelerar() e desacelerar();
- Linhas 6 a 28: Código das classes de comportamento AceleracaoMotorComum e AceleracaoMotorV8 que implementam a interface IAceleracao e, consequentemente, seus métodos. O termo “classes de comportamento” não é um padrão da POO, ele apenas indica que a classe não representa uma abstração de um objeto, mas sim a abstração de um comportamento;
- Linha 30: Código da classe abstrata Veiculo, a partir da qual serão geradas as demais classes;
- Linha 32: Declaração do atributo acelerador, do tipo IAceleracao, que recebe como default uma instância da classe de comportamento AceleracaoMotorComum;
- Linhas 34 a 40: Declaração de dois métodos para a classe Veiculo, que chamam internamente os métodos de mesmo nome da instância atribuída a acelerador;
- Linha 43 a 45: Código das classes Gol e Fiesta, que estendem a classe Veiculo. Como essas classes mantêm o mesmo funcionamento da sua classe pai, seus métodos acelerar() e desacelerar() executarão os métodos que estão na classe de comportamento AceleracaoMotorComum (acionados nas linhas 34 e 39);
- Linhas 47 a 53: A classe Maverick atribui uma nova instância para o atributo de interface acelerador. Ao fazer isso, os métodos acelerar() e desacelerar() passam a apresentar outro comportamento e executam as rotinas de mesmo nome implementadas na classeAceleracaoMotorV8.
Essa é considerada uma solução eficiente, flexível e que aplica corretamente as práticas da Orientação a Objetos, sendo útil principalmente em situações em que há um número considerável de classes que implementam a interface, já que permite a reusabilidade de código para executar comportamentos semelhantes. Porém, ela se torna pouco eficaz quando utilizada em um contexto onde há poucas classes, já que gera um código extenso. Nesse caso, o esforço de desenvolvimento despendido não terá valido a pena, pois o uso da estrutura gerada será mínimo.
Saiba mais: Padrão de Projeto Strategy em Java
Solução com Default Methods
Com a introdução desse recurso na linguagem Java, obteve-se uma solução simples e que reduz bastante o esforço de programação, simplificando o código ao possibilitar a criação de interfaces que já possuam uma implementação padrão para alguns de seus métodos (mesmo que sejam para lançar apenas uma exceção, por exemplo). Assim, quando uma classe implementar uma interface, mas não sobrescrever seus métodos, será utilizado exatamente o código que foi definido como implementação padrão na interface. Na Listagem 4 apresentamos o mesmo exemplo do simulador, agora com Default Methods.
public interface IAceleracao{ default void acelerar(){ System.out.println("Aceleração Constante"); } default void desacelerar(){ System.out.println("Desaceleração Constante"); } } public abstract class Veiculo implements IAceleracao{ Motor motor = new Motor(); } public class Maverick extends Veiculo { public Maverick(){ motor = new Motor("V8"); } @Override public void acelerar() { System.out.println("Aceleração Super Rápida"); } @Override public void desacelerar() { System.out.println("Desaceleração Lenta"); } } public class Gol extends Veiculo {} public class Fiesta extends Veiculo {}
- Linhas 2 e 6: Ao declarar as assinaturas dos métodos da interface IAceleracao, a cláusula default foi adicionada. Isso sinaliza o uso do recurso Default Methods, permitindo adicionar uma implementação padrão dentro da própria interface para cada método;
- Linhas 15 a 29: Ao declarar a classe Maverick, optamos por seguir o padrão conhecido na linguagem Java, implementando os métodos da interface;
- Linhas 31 e 33: Diferentemente do que é de costume, nas classes Gol e Fiesta os métodos da interface não foram implementados. Contudo, como esses métodos estão marcados como default, isso não irá gerar erro durante a compilação.
Nesse exemplo é perceptível a facilidade obtida ao acrescentar a cláusula default antes da assinatura dos métodos da interface. Isso possibilita acrescentar uma rotina padrão que normalmente só seria definida nas classes que a implementassem. Esse código padrão será executado sempre que o compilador verificar, na classe, que esses métodos não foram implementados.