45 á 54
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, pois 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.
Caso contrário, esse método poderia, e deveria, receber uma referência a 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.
Classes abstratas possuem a vantagem de se poder adicionar-lhes 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.
Programe voltado à interface, não à implementação Casa do Código 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.
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.
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.
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, diminuindo bastante a complexidade e aumentando a manutenibilidade, além de algum ganho de performance. Apesar de simples, existem seis possibilidades diferentes de execução do método, além de ele misturar diversos comportamentos que não possuem relação, isto é, responsabilidades diferentes para uma única classe: impostos, estoques, transferências e e-mails.
Tal comportamento pode ser composto por diversas partes menores e, para tanto, refatorações pequenas podem ser executadas.
A mais simples seria a extração de quatro métodos, uma solução que simplifica o código atual, mas não aumenta sua coesão.
É possível aumentar a coesão da classe extraindo tais métodos em quatro classes distintas, como RetemImposto, ProcessaEstoque, Transferencia e Confirmacao.
À medida que foi necessário, o código era refatorado.
Após a quebra de um comportamento em diversas partes, é necessário juntar esses comportamentos novamente. Composição (composition) é esta união através da utilização de alguma prática de design, como no caso visto anteriormente, no qual o polimorfismo permite trabalhar de maneira uniforme com partes que executam tarefas distintas.
Um método que apresente um corpo com um número razoável de branches (como ifs e switches), ou que não permita a compreensão imediata de seu comportamento, pode ser refatorado para facilitar sua manutenção de Orientação a Objetos bem o código interno da classe mãe, o que é visto como uma quebra de encapsulamento.
Podemos enxergar este problema em um exemplo encontrado no pacote java.util, com Properties e Hashtable.
A classe Properties herda de Hashtable e, portanto, instâncias de Properties possuem disponíveis métodos de Hashtable.
Mas, muitas vezes, esses métodos não fazem sentido, como no caso da Properties que lida apenas com Strings.
A quebra aparece quando temos uma instância de Properties e invocamos o método put(Object, Object), em vez de setProperty(String, String), podendo passar um Object qualquer como argumento.
Nesse caso, ao invocar posteriormente o método getProperty(String), recebemos um inesperado null.
Não há como evitar que alguém invoque o método que trabalha com Object, a não ser documentando a respeito, como ocorre no javadoc dessa classe.
Ter de garantir condições apenas através do javadoc é um contrato muito fraco, em especial dizendo que tal método não deve ser invocado com determinados tipos de objetos, algo que, em Java, poderia ser feito através da tipagem estática.
Herança é comumente citada em cenários de reúso de código, mas não é a única solução.
Usar composição permite reaproveitamento de código sem o efeito indesejado da quebra de encapsulamento.
Para que a classe Properties reaproveitasse os recursos já existentes em Hashtable com composição bastaria um atributo Hashtable privado, e o método setProperty delegaria a invocação para o hashtable.put.
Dessa maneira, expomos apenas os métodos que desejamos, sem possibilitar que fosse invocado o hashtable.put diretamente, nem que Properties pudesse ser passada como argumento para alguém que espera uma Hashtable.
Melhor ainda, com o surgimento da API de Collections, a implementação poderia ser facilmente trocada por algum Map sem causar problemas aos usuários da classe.
O uso abusivo de herança ocorre em diversos frameworks e APIs.
O Struts 1 é um exemplo deste uso e de seus diversos problemas relacionados; somos instruídos a estender Action e, ao mesmo tempo, a tomar muito cuidado com seu ciclo de vida, principalmente ao sobrescrever alguns métodos-chave dessa classe.
Em linguagens com suporte a mixin, como Ruby, o acoplamento com as classes pode gerar complicações ainda maiores, que só serão percebidas em tempo de execução.
O problema da herança aparece, mesmo que mais sutilmente, no uso de classes abstratas para aplicar patterns, como o template method.
O novo método init(), sem parâmetros, deve ser reescrito; ele é invocado pelo próprio init(ServletConfig) após o ServletConfig ser guardado em um atributo.
Dentro de um init() reescrito podemos obter acesso ao ServletConfig através do getter da classe mãe, que agora estará corretamente atribuído.
Mesmo assim, em razão da tomada de decisão original na API de servlet, o desenvolvedor ainda deve conhecer como esses métodos funcionam na classe mãe, pois só assim saberá de todos os cuidados que são necessários.
Mas nem tudo é negativo. Todo esse design de servlets tem como objetivo principal trazer o polimorfismo e uma interface de uso uniforme para as servlets serem chamadas pelo container. Contudo, em Java, é possível usar interface para polimorfismo, sem os pontos negativos da herança como quebra de encapsulamento e alto acoplamento.
A API de servlets poderia ter usado um design baseado em interfaces com um strategy pattern por exemplo, evitando a herança.
Ao substituir a herança por interface, continuamos com o benefício do polimorfismo.
Mas herança ainda traz outro grande benefício, o reaproveitamento de código, algo que interfaces puras não fazem.
Podemos então usar interfaces com composição, obtendo substitutos para todos os benefícios de herança sem correr o risco de cair na armadilha da quebra de encapsulamento e alto acoplamento.
Repare que, desta forma, o acoplamento é bem menor, nenhuma das classes precisa conhecer o funcionamento interno das outras.
Porém, se for necessário o uso de herança, alguns cuidados são importantes para minimizar a possível quebra de encapsulamento.
Joshua Bloch, no Effective Java, fala de Design for Inheritance, com diversas práticas como evitar invocações entre métodos públicos, para que a sobrescrita de um não mude o comportamento de outro.
Ao mesmo tempo que desejamos evitar herança, temos interesse em permitir mudança no nosso comportamento sem a necessidade de alterar ou copiar o código original.
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, pois 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.
Caso contrário, esse método poderia, e deveria, receber uma referência a 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.
Classes abstratas possuem a vantagem de se poder adicionar-lhes 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.
Programe voltado à interface, não à implementação Casa do Código 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.
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.
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.
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, diminuindo bastante a complexidade e aumentando a manutenibilidade, além de algum ganho de performance. Apesar de simples, existem seis possibilidades diferentes de execução do método, além de ele misturar diversos comportamentos que não possuem relação, isto é, responsabilidades diferentes para uma única classe: impostos, estoques, transferências e e-mails.
Tal comportamento pode ser composto por diversas partes menores e, para tanto, refatorações pequenas podem ser executadas.
A mais simples seria a extração de quatro métodos, uma solução que simplifica o código atual, mas não aumenta sua coesão.
É possível aumentar a coesão da classe extraindo tais métodos em quatro classes distintas, como RetemImposto, ProcessaEstoque, Transferencia e Confirmacao.
À medida que foi necessário, o código era refatorado.
Após a quebra de um comportamento em diversas partes, é necessário juntar esses comportamentos novamente. Composição (composition) é esta união através da utilização de alguma prática de design, como no caso visto anteriormente, no qual o polimorfismo permite trabalhar de maneira uniforme com partes que executam tarefas distintas.
Um método que apresente um corpo com um número razoável de branches (como ifs e switches), ou que não permita a compreensão imediata de seu comportamento, pode ser refatorado para facilitar sua manutenção de Orientação a Objetos bem o código interno da classe mãe, o que é visto como uma quebra de encapsulamento.
Podemos enxergar este problema em um exemplo encontrado no pacote java.util, com Properties e Hashtable.
A classe Properties herda de Hashtable e, portanto, instâncias de Properties possuem disponíveis métodos de Hashtable.
Mas, muitas vezes, esses métodos não fazem sentido, como no caso da Properties que lida apenas com Strings.
A quebra aparece quando temos uma instância de Properties e invocamos o método put(Object, Object), em vez de setProperty(String, String), podendo passar um Object qualquer como argumento.
Nesse caso, ao invocar posteriormente o método getProperty(String), recebemos um inesperado null.
Não há como evitar que alguém invoque o método que trabalha com Object, a não ser documentando a respeito, como ocorre no javadoc dessa classe.
Ter de garantir condições apenas através do javadoc é um contrato muito fraco, em especial dizendo que tal método não deve ser invocado com determinados tipos de objetos, algo que, em Java, poderia ser feito através da tipagem estática.
Herança é comumente citada em cenários de reúso de código, mas não é a única solução.
Usar composição permite reaproveitamento de código sem o efeito indesejado da quebra de encapsulamento.
Para que a classe Properties reaproveitasse os recursos já existentes em Hashtable com composição bastaria um atributo Hashtable privado, e o método setProperty delegaria a invocação para o hashtable.put.
Dessa maneira, expomos apenas os métodos que desejamos, sem possibilitar que fosse invocado o hashtable.put diretamente, nem que Properties pudesse ser passada como argumento para alguém que espera uma Hashtable.
Melhor ainda, com o surgimento da API de Collections, a implementação poderia ser facilmente trocada por algum Map sem causar problemas aos usuários da classe.
O uso abusivo de herança ocorre em diversos frameworks e APIs.
O Struts 1 é um exemplo deste uso e de seus diversos problemas relacionados; somos instruídos a estender Action e, ao mesmo tempo, a tomar muito cuidado com seu ciclo de vida, principalmente ao sobrescrever alguns métodos-chave dessa classe.
Em linguagens com suporte a mixin, como Ruby, o acoplamento com as classes pode gerar complicações ainda maiores, que só serão percebidas em tempo de execução.
O problema da herança aparece, mesmo que mais sutilmente, no uso de classes abstratas para aplicar patterns, como o template method.
O novo método init(), sem parâmetros, deve ser reescrito; ele é invocado pelo próprio init(ServletConfig) após o ServletConfig ser guardado em um atributo.
Dentro de um init() reescrito podemos obter acesso ao ServletConfig através do getter da classe mãe, que agora estará corretamente atribuído.
Mesmo assim, em razão da tomada de decisão original na API de servlet, o desenvolvedor ainda deve conhecer como esses métodos funcionam na classe mãe, pois só assim saberá de todos os cuidados que são necessários.
Mas nem tudo é negativo. Todo esse design de servlets tem como objetivo principal trazer o polimorfismo e uma interface de uso uniforme para as servlets serem chamadas pelo container. Contudo, em Java, é possível usar interface para polimorfismo, sem os pontos negativos da herança como quebra de encapsulamento e alto acoplamento.
A API de servlets poderia ter usado um design baseado em interfaces com um strategy pattern por exemplo, evitando a herança.
Ao substituir a herança por interface, continuamos com o benefício do polimorfismo.
Mas herança ainda traz outro grande benefício, o reaproveitamento de código, algo que interfaces puras não fazem.
Podemos então usar interfaces com composição, obtendo substitutos para todos os benefícios de herança sem correr o risco de cair na armadilha da quebra de encapsulamento e alto acoplamento.
Repare que, desta forma, o acoplamento é bem menor, nenhuma das classes precisa conhecer o funcionamento interno das outras.
Porém, se for necessário o uso de herança, alguns cuidados são importantes para minimizar a possível quebra de encapsulamento.
Joshua Bloch, no Effective Java, fala de Design for Inheritance, com diversas práticas como evitar invocações entre métodos públicos, para que a sobrescrita de um não mude o comportamento de outro.
Ao mesmo tempo que desejamos evitar herança, temos interesse em permitir mudança no nosso comportamento sem a necessidade de alterar ou copiar o código original.
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, pois 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.
Caso contrário, esse método poderia, e deveria, receber uma referência a 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.
Classes abstratas possuem a vantagem de se poder adicionar-lhes 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.
Programe voltado à interface, não à implementação Casa do Código 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.
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.
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.
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, diminuindo bastante a complexidade e aumentando a manutenibilidade, além de algum ganho de performance. Apesar de simples, existem seis possibilidades diferentes de execução do método, além de ele misturar diversos comportamentos que não possuem relação, isto é, responsabilidades diferentes para uma única classe: impostos, estoques, transferências e e-mails.
Tal comportamento pode ser composto por diversas partes menores e, para tanto, refatorações pequenas podem ser executadas.
A mais simples seria a extração de quatro métodos, uma solução que simplifica o código atual, mas não aumenta sua coesão.
É possível aumentar a coesão da classe extraindo tais métodos em quatro classes distintas, como RetemImposto, ProcessaEstoque, Transferencia e Confirmacao.
À medida que foi necessário, o código era refatorado.
Após a quebra de um comportamento em diversas partes, é necessário juntar esses comportamentos novamente. Composição (composition) é esta união através da utilização de alguma prática de design, como no caso visto anteriormente, no qual o polimorfismo permite trabalhar de maneira uniforme com partes que executam tarefas distintas.
Um método que apresente um corpo com um número razoável de branches (como ifs e switches), ou que não permita a compreensão imediata de seu comportamento, pode ser refatorado para facilitar sua manutenção de Orientação a Objetos bem o código interno da classe mãe, o que é visto como uma quebra de encapsulamento.
Podemos enxergar este problema em um exemplo encontrado no pacote java.util, com Properties e Hashtable.
A classe Properties herda de Hashtable e, portanto, instâncias de Properties possuem disponíveis métodos de Hashtable.
Mas, muitas vezes, esses métodos não fazem sentido, como no caso da Properties que lida apenas com Strings.
A quebra aparece quando temos uma instância de Properties e invocamos o método put(Object, Object), em vez de setProperty(String, String), podendo passar um Object qualquer como argumento.
Nesse caso, ao invocar posteriormente o método getProperty(String), recebemos um inesperado null.
Não há como evitar que alguém invoque o método que trabalha com Object, a não ser documentando a respeito, como ocorre no javadoc dessa classe.
Ter de garantir condições apenas através do javadoc é um contrato muito fraco, em especial dizendo que tal método não deve ser invocado com determinados tipos de objetos, algo que, em Java, poderia ser feito através da tipagem estática.
Herança é comumente citada em cenários de reúso de código, mas não é a única solução.
Usar composição permite reaproveitamento de código sem o efeito indesejado da quebra de encapsulamento.
Para que a classe Properties reaproveitasse os recursos já existentes em Hashtable com composição bastaria um atributo Hashtable privado, e o método setProperty delegaria a invocação para o hashtable.put.
Dessa maneira, expomos apenas os métodos que desejamos, sem possibilitar que fosse invocado o hashtable.put diretamente, nem que Properties pudesse ser passada como argumento para alguém que espera uma Hashtable.
Melhor ainda, com o surgimento da API de Collections, a implementação poderia ser facilmente trocada por algum Map sem causar problemas aos usuários da classe.
O uso abusivo de herança ocorre em diversos frameworks e APIs.
O Struts 1 é um exemplo deste uso e de seus diversos problemas relacionados; somos instruídos a estender Action e, ao mesmo tempo, a tomar muito cuidado com seu ciclo de vida, principalmente ao sobrescrever alguns métodos-chave dessa classe.
Em linguagens com suporte a mixin, como Ruby, o acoplamento com as classes pode gerar complicações ainda maiores, que só serão percebidas em tempo de execução.
O problema da herança aparece, mesmo que mais sutilmente, no uso de classes abstratas para aplicar patterns, como o template method.
O novo método init(), sem parâmetros, deve ser reescrito; ele é invocado pelo próprio init(ServletConfig) após o ServletConfig ser guardado em um atributo.
Dentro de um init() reescrito podemos obter acesso ao ServletConfig através do getter da classe mãe, que agora estará corretamente atribuído.
Mesmo assim, em razão da tomada de decisão original na API de servlet, o desenvolvedor ainda deve conhecer como esses métodos funcionam na classe mãe, pois só assim saberá de todos os cuidados que são necessários.
Mas nem tudo é negativo. Todo esse design de servlets tem como objetivo principal trazer o polimorfismo e uma interface de uso uniforme para as servlets serem chamadas pelo container. Contudo, em Java, é possível usar interface para polimorfismo, sem os pontos negativos da herança como quebra de encapsulamento e alto acoplamento.
A API de servlets poderia ter usado um design baseado em interfaces com um strategy pattern por exemplo, evitando a herança.
Ao substituir a herança por interface, continuamos com o benefício do polimorfismo.
Mas herança ainda traz outro grande benefício, o reaproveitamento de código, algo que interfaces puras não fazem.
Podemos então usar interfaces com composição, obtendo substitutos para todos os benefícios de herança sem correr o risco de cair na armadilha da quebra de encapsulamento e alto acoplamento.
Repare que, desta forma, o acoplamento é bem menor, nenhuma das classes precisa conhecer o funcionamento interno das outras.
Porém, se for necessário o uso de herança, alguns cuidados são importantes para minimizar a possível quebra de encapsulamento.
Joshua Bloch, no Effective Java, fala de Design for Inheritance, com diversas práticas como evitar invocações entre métodos públicos, para que a sobrescrita de um não mude o comportamento de outro.
Ao mesmo tempo que desejamos evitar herança, temos interesse em permitir mudança no nosso comportamento sem a necessidade de alterar ou copiar o código original.
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, pois 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.
Caso contrário, esse método poderia, e deveria, receber uma referência a 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.
Classes abstratas possuem a vantagem de se poder adicionar-lhes 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.
Programe voltado à interface, não à implementação Casa do Código 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.
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.
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.
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, diminuindo bastante a complexidade e aumentando a manutenibilidade, além de algum ganho de performance. Apesar de simples, existem seis possibilidades diferentes de execução do método, além de ele misturar diversos comportamentos que não possuem relação, isto é, responsabilidades diferentes para uma única classe: impostos, estoques, transferências e e-mails.
Tal comportamento pode ser composto por diversas partes menores e, para tanto, refatorações pequenas podem ser executadas.
A mais simples seria a extração de quatro métodos, uma solução que simplifica o código atual, mas não aumenta sua coesão.
É possível aumentar a coesão da classe extraindo tais métodos em quatro classes distintas, como RetemImposto, ProcessaEstoque, Transferencia e Confirmacao.
À medida que foi necessário, o código era refatorado.
Após a quebra de um comportamento em diversas partes, é necessário juntar esses comportamentos novamente. Composição (composition) é esta união através da utilização de alguma prática de design, como no caso visto anteriormente, no qual o polimorfismo permite trabalhar de maneira uniforme com partes que executam tarefas distintas.
Um método que apresente um corpo com um número razoável de branches (como ifs e switches), ou que não permita a compreensão imediata de seu comportamento, pode ser refatorado para facilitar sua manutenção de Orientação a Objetos bem o código interno da classe mãe, o que é visto como uma quebra de encapsulamento.
Podemos enxergar este problema em um exemplo encontrado no pacote java.util, com Properties e Hashtable.
A classe Properties herda de Hashtable e, portanto, instâncias de Properties possuem disponíveis métodos de Hashtable.
Mas, muitas vezes, esses métodos não fazem sentido, como no caso da Properties que lida apenas com Strings.
A quebra aparece quando temos uma instância de Properties e invocamos o método put(Object, Object), em vez de setProperty(String, String), podendo passar um Object qualquer como argumento.
Nesse caso, ao invocar posteriormente o método getProperty(String), recebemos um inesperado null.
Não há como evitar que alguém invoque o método que trabalha com Object, a não ser documentando a respeito, como ocorre no javadoc dessa classe.
Ter de garantir condições apenas através do javadoc é um contrato muito fraco, em especial dizendo que tal método não deve ser invocado com determinados tipos de objetos, algo que, em Java, poderia ser feito através da tipagem estática.
Herança é comumente citada em cenários de reúso de código, mas não é a única solução.
Usar composição permite reaproveitamento de código sem o efeito indesejado da quebra de encapsulamento.
Para que a classe Properties reaproveitasse os recursos já existentes em Hashtable com composição bastaria um atributo Hashtable privado, e o método setProperty delegaria a invocação para o hashtable.put.
Dessa maneira, expomos apenas os métodos que desejamos, sem possibilitar que fosse invocado o hashtable.put diretamente, nem que Properties pudesse ser passada como argumento para alguém que espera uma Hashtable.
Melhor ainda, com o surgimento da API de Collections, a implementação poderia ser facilmente trocada por algum Map sem causar problemas aos usuários da classe.
O uso abusivo de herança ocorre em diversos frameworks e APIs.
O Struts 1 é um exemplo deste uso e de seus diversos problemas relacionados; somos instruídos a estender Action e, ao mesmo tempo, a tomar muito cuidado com seu ciclo de vida, principalmente ao sobrescrever alguns métodos-chave dessa classe.
Em linguagens com suporte a mixin, como Ruby, o acoplamento com as classes pode gerar complicações ainda maiores, que só serão percebidas em tempo de execução.
O problema da herança aparece, mesmo que mais sutilmente, no uso de classes abstratas para aplicar patterns, como o template method.
O novo método init(), sem parâmetros, deve ser reescrito; ele é invocado pelo próprio init(ServletConfig) após o ServletConfig ser guardado em um atributo.
Dentro de um init() reescrito podemos obter acesso ao ServletConfig através do getter da classe mãe, que agora estará corretamente atribuído.
Mesmo assim, em razão da tomada de decisão original na API de servlet, o desenvolvedor ainda deve conhecer como esses métodos funcionam na classe mãe, pois só assim saberá de todos os cuidados que são necessários.
Mas nem tudo é negativo. Todo esse design de servlets tem como objetivo principal trazer o polimorfismo e uma interface de uso uniforme para as servlets serem chamadas pelo container. Contudo, em Java, é possível usar interface para polimorfismo, sem os pontos negativos da herança como quebra de encapsulamento e alto acoplamento.
A API de servlets poderia ter usado um design baseado em interfaces com um strategy pattern por exemplo, evitando a herança.
Ao substituir a herança por interface, continuamos com o benefício do polimorfismo.
Mas herança ainda traz outro grande benefício, o reaproveitamento de código, algo que interfaces puras não fazem.
Podemos então usar interfaces com composição, obtendo substitutos para todos os benefícios de herança sem correr o risco de cair na armadilha da quebra de encapsulamento e alto acoplamento.
Repare que, desta forma, o acoplamento é bem menor, nenhuma das classes precisa conhecer o funcionamento interno das outras.
Porém, se for necessário o uso de herança, alguns cuidados são importantes para minimizar a possível quebra de encapsulamento.
Joshua Bloch, no Effective Java, fala de Design for Inheritance, com diversas práticas como evitar invocações entre métodos públicos, para que a sobrescrita de um não mude o comportamento de outro.
Ao mesmo tempo que desejamos evitar herança, temos interesse em permitir mudança no nosso comportamento sem a necessidade de alterar ou copiar o código original.