Contente
- Introdução
- Interfaces
- Marcadores de interface
- Interfaces funcionais, métodos estáticos e métodos padrão
- Aulas abstratas
- Classes imutáveis (permanentes)
- Aulas anônimas
- Visibilidade
- Herança
- Herança múltipla
- Herança e composição
- Encapsulamento
- Aulas e métodos finais
- Qual é o próximo
- Baixe o código-fonte
1. INTRODUÇÃO
Não importa qual linguagem de programação você usa (e Java não é exceção), seguir bons princípios de design é a chave para escrever código limpo, compreensível e verificável; e também criá-lo para ter vida longa e apoiar facilmente a resolução de problemas. Nesta parte do tutorial, discutiremos os blocos de construção fundamentais que a linguagem Java fornece e apresentaremos alguns princípios de design em um esforço para ajudá-lo a tomar melhores decisões de design. Mais especificamente, discutiremos interfaces e interfaces usando métodos padrão (um novo recurso no Java 8), classes abstratas e finais, classes imutáveis, herança, composição e revisitaremos as regras de visibilidade (ou acessibilidade) que abordamos brevemente em Lição da parte 1
"Como criar e destruir objetos" .
2. INTERFACES
Na programação orientada a objetos , o conceito de interfaces constitui a base para o desenvolvimento de contratos . Resumindo, as interfaces definem um conjunto de métodos (contratos) e cada classe que requer suporte para essa interface específica deve fornecer uma implementação desses métodos: uma ideia bastante simples, mas poderosa. Muitas linguagens de programação possuem interfaces de uma forma ou de outra, mas Java em particular fornece suporte de linguagem para isso. Vamos dar uma olhada em uma definição simples de interface em Java.
package com.javacodegeeks.advanced.design;
public interface SimpleInterface {
void performAction();
}
No trecho acima, a interface que chamamos
SimpleInterface
declara apenas um método chamado
performAction
. A principal diferença entre interfaces e classes é que as interfaces descrevem como deve ser o contato (declaram um método), mas não fornecem sua implementação. No entanto, as interfaces em Java podem ser mais complexas: podem incluir interfaces aninhadas, classes, contagens, anotações e constantes. Por exemplo:
package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefinitions {
String CONSTANT = "CONSTANT";
enum InnerEnum {
E1, E2;
}
class InnerClass {
}
interface InnerInterface {
void performInnerAction();
}
void performAction();
}
Neste exemplo mais complexo, há diversas restrições que as interfaces impõem incondicionalmente ao aninhamento de construções e declarações de métodos, e estas são impostas pelo compilador Java. Primeiro de tudo, mesmo que não seja explicitamente declarado, toda declaração de método em uma interface é
pública (e só pode ser pública). Portanto, as seguintes declarações de método são equivalentes:
public void performAction();
void performAction();
Vale a pena mencionar que cada método em uma interface é declarado implicitamente
abstract , e mesmo essas declarações de método são equivalentes:
public abstract void performAction();
public void performAction();
void performAction();
Quanto aos campos constantes declarados, além de serem
públicos , também são implicitamente
estáticos e marcados
como finais . Portanto, as seguintes declarações também são equivalentes:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
Por fim, classes, interfaces ou contagens aninhadas, além de serem
públicas , também são declaradas implicitamente
como estáticas . Por exemplo, essas declarações também são equivalentes a:
class InnerClass {
}
static class InnerClass {
}
O estilo que você escolhe é uma preferência pessoal, mas conhecer essas propriedades simples das interfaces pode evitar digitações desnecessárias.
3. Marcador de interface
Uma interface de marcador é um tipo especial de interface que não possui métodos ou outras construções aninhadas. Como a biblioteca Java o define:
public interface Cloneable {
}
Os marcadores de interface não são contratos em si, mas são uma técnica útil para "anexar" ou "associar" alguma característica específica a uma classe. Por exemplo, com relação a
Cloneable , a classe é marcada como clonável, mas a forma como isso pode ou deve ser implementado não faz parte da interface. Outro exemplo muito famoso e amplamente utilizado de marcador de interface é
Serializable
:
public interface Serializable {
}
Essa interface marca a classe como adequada para serialização e desserialização e, novamente, não especifica como isso pode ou deve ser implementado. Os marcadores de interface têm seu lugar na programação orientada a objetos, embora não satisfaçam o propósito principal de uma interface ser um contrato.
4. INTERFACES FUNCIONAIS, MÉTODOS PADRÃO E MÉTODOS ESTÁTICOS
Desde o lançamento do Java 8, as interfaces ganharam alguns novos recursos muito interessantes: métodos estáticos, métodos padrão e conversão automática de lambdas (interfaces funcionais). Na seção de interfaces, enfatizamos o fato de que as interfaces em Java só podem declarar métodos, mas não fornecem sua implementação. Com um método padrão, as coisas são diferentes: uma interface pode marcar um método com a palavra-chave
padrão e fornecer uma implementação para ele. Por exemplo:
package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
void performAction();
default void performDefaulAction() {
}
}
Estando no nível da instância, os métodos padrão podem ser substituídos por cada implementação de interface, mas as interfaces agora também podem incluir métodos
estáticos , por exemplo: package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
static void createAction() {
}
}
Pode-se dizer que fornecer a implementação na interface anula todo o propósito da programação do contrato. Mas há muitos motivos pelos quais esses recursos foram introduzidos na linguagem Java e, por mais úteis ou confusos que sejam, eles estão lá para você e seu uso. As interfaces funcionais são uma história completamente diferente e provaram ser adições muito úteis à linguagem. Basicamente, uma interface funcional é uma interface com apenas um método abstrato declarado.
Runnable
A interface da biblioteca padrão é um bom exemplo desse conceito.
@FunctionalInterface
public interface Runnable {
void run();
}
O compilador Java trata as interfaces funcionais de maneira diferente e pode transformar uma função lambda em uma implementação de interface funcional onde fizer sentido. Vamos considerar a seguinte descrição da função:
public void runMe( final Runnable r ) {
r.run();
}
Para chamar esta função em Java 7 e versões anteriores, uma implementação da interface deve ser fornecida
Runnable
(por exemplo, usando classes anônimas), mas em Java 8 é suficiente fornecer uma implementação do método run() usando sintaxe lambda:
runMe( () -> System.out.println( "Run!" ) );
Além disso, a anotação
@FunctionalInterface (as anotações serão abordadas em detalhes na Parte 5 do tutorial) sugere que o compilador pode verificar se uma interface contém apenas um método abstrato, portanto, quaisquer alterações feitas na interface no futuro não violarão essa suposição .
5. AULAS ABSTRATAS
Outro conceito interessante suportado pela linguagem Java é o conceito de classes abstratas. As classes abstratas são um tanto semelhantes às interfaces do Java 7 e estão muito próximas da interface do método padrão do Java 8. Ao contrário das classes regulares, uma classe abstrata não pode ser instanciada, mas pode ser subclassificada (consulte a seção Herança para obter mais detalhes). Mais importante ainda, as classes abstratas podem conter métodos abstratos: um tipo especial de método sem implementação, assim como uma interface. Por exemplo:
package com.javacodegeeks.advanced.design;
public abstract class SimpleAbstractClass {
public void performAction() {
}
public abstract void performAnotherAction();
}
Neste exemplo, a classe
SimpleAbstractClass
é declarada como
abstrata e contém um método abstrato declarado. Classes abstratas são muito úteis; a maioria ou mesmo algumas partes dos detalhes de implementação podem ser compartilhadas entre muitas subclasses. Seja como for, eles ainda deixam a porta entreaberta e permitem customizar o comportamento inerente a cada uma das subclasses por meio de métodos abstratos. Vale ressaltar que diferentemente das interfaces, que só podem conter declarações
públicas , as classes abstratas podem usar todo o poder das regras de acessibilidade para controlar a visibilidade de um método abstrato.
6. AULAS IMEDIATAS
A imutabilidade está se tornando cada vez mais importante no desenvolvimento de software hoje em dia. A ascensão dos sistemas multi-core levantou muitas questões relacionadas ao compartilhamento e paralelismo de dados. Mas definitivamente surgiu um problema: ter pouco (ou mesmo nenhum) estado mutável leva a uma melhor extensibilidade (escalabilidade) e a um raciocínio mais fácil sobre o sistema. Infelizmente, a linguagem Java não fornece suporte decente para imutabilidade de classe. Porém, usando uma combinação de técnicas, torna-se possível projetar classes imutáveis. Primeiramente, todos os campos da classe devem ser finais (marcados como
final ). Este é um bom começo, mas não é garantia.
package com.javacodegeeks.advanced.design;
import java.util.Collection;
public class ImmutableClass {
private final long id;
private final String[] arrayOfStrings;
private final Collection<String> collectionOfString;
}
Em segundo lugar, garanta uma inicialização adequada: se um campo for uma referência a uma coleção ou array, não atribua esses campos diretamente dos argumentos do construtor; em vez disso, faça cópias. Isso garantirá que o estado da coleção ou matriz não seja modificado fora dela.
public ImmutableClass( final long id, final String[] arrayOfStrings,
final Collection<String> collectionOfString) {
this.id = id;
this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
this.collectionOfString = new ArrayList<>( collectionOfString );
}
E por fim, garantir o acesso adequado (getters). Para coleções, a imutabilidade deve ser fornecida como um wrapper
Collections.unmodifiableXxx
: com arrays, a única maneira de fornecer imutabilidade verdadeira é fornecer uma cópia em vez de retornar uma referência ao array. Isto pode não ser aceitável do ponto de vista prático, pois depende muito do tamanho do array e pode exercer uma pressão enorme sobre o coletor de lixo.
public String[] getArrayOfStrings() {
return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Mesmo este pequeno exemplo dá uma boa ideia de que a imutabilidade ainda não é um cidadão de primeira classe em Java. As coisas podem ficar complicadas se uma classe imutável tiver um campo que se refira a um objeto de outra classe. Essas classes também deveriam ser imutáveis, mas não há como garantir isso. Existem vários analisadores de código-fonte Java decentes, como FindBugs e PMD, que podem ajudar muito, verificando seu código e apontando falhas comuns de programação Java. Essas ferramentas são grandes amigas de qualquer desenvolvedor Java.
7. AULAS ANÔNIMAS
Na era pré-Java 8, as classes anônimas eram a única maneira de garantir que as classes fossem definidas dinamicamente e instanciadas imediatamente. O objetivo das classes anônimas era reduzir o clichê e fornecer uma maneira curta e fácil de representar as classes como um registro. Vamos dar uma olhada na maneira típica e antiquada de gerar um novo thread em Java:
package com.javacodegeeks.advanced.design;
public class AnonymousClass {
public static void main( String[] args ) {
new Thread(
new Runnable() {
@Override
public void run() {
}
}
).start();
}
}
Neste exemplo, a implementação da
Runnable
interface é fornecida imediatamente como uma classe anônima. Embora existam algumas limitações associadas às classes anônimas, as principais desvantagens de usá-las são a sintaxe de construção altamente detalhada que o Java como linguagem obriga. Mesmo uma classe anônima que não faz nada requer pelo menos 5 linhas de código toda vez que é escrita.
new Runnable() {
@Override
public void run() {
}
}
Felizmente, com Java 8, lambda e interfaces funcionais, todos esses estereótipos desaparecerão em breve e, finalmente, escrever código Java parecerá verdadeiramente conciso.
package com.javacodegeeks.advanced.design;
public class AnonymousClass {
public static void main( String[] args ) {
new Thread( () -> { } ).start();
}
}
8. VISIBILIDADE
Já falamos um pouco sobre regras de visibilidade e acessibilidade em Java na Parte 1 do tutorial. Nesta parte, revisitaremos esse tópico novamente, mas no contexto da subclasse.
A visibilidade em diferentes níveis permite ou impede que as classes vejam outras classes ou interfaces (por exemplo, se estiverem em pacotes diferentes ou aninhadas umas nas outras) ou que as subclasses vejam e acessem os métodos, construtores e campos de seus pais. Na próxima seção, herança, veremos isso em ação.
9. HERANÇA
Herança é um dos conceitos-chave da programação orientada a objetos, servindo de base para a construção de uma classe de relacionamentos. Combinada com regras de visibilidade e acessibilidade, a herança permite que as classes sejam projetadas em uma hierarquia que pode ser estendida e mantida. Em um nível conceitual, a herança em Java é implementada usando subclasses e a palavra-chave
extends , junto com a classe pai. Uma subclasse herda todos os elementos públicos e protegidos da classe pai. Além disso, uma subclasse herda os elementos package-private de sua classe pai se ambos (subclasse e classe) estiverem no mesmo pacote. Dito isto, é muito importante, não importa o que você esteja tentando projetar, manter o conjunto mínimo de métodos que uma classe expõe publicamente ou às suas subclasses. Por exemplo, vejamos a classe
Parent
e sua subclasse
Child
para demonstrar a diferença nos níveis de visibilidade e seus efeitos.
package com.javacodegeeks.advanced.design;
public class Parent {
public static final String CONSTANT = "Constant";
private String privateField;
protected String protectedField;
private class PrivateClass {
}
protected interface ProtectedInterface {
}
public void publicAction() {
}
protected void protectedAction() {
}
private void privateAction() {
}
void packageAction() {
}
}
package com.javacodegeeks.advanced.design;
public class Child extends Parent implements Parent.ProtectedInterface {
@Override
protected void protectedAction() {
super.protectedAction();
}
@Override
void packageAction() {
}
public void childAction() {
this.protectedField = "value";
}
}
Herança é um tópico muito amplo em si, com muitos detalhes específicos de Java. No entanto, existem algumas regras que são fáceis de seguir e podem ajudar muito a manter a brevidade da hierarquia de classes. Em Java, cada subclasse pode substituir quaisquer métodos herdados de seu pai, a menos que tenha sido declarado final. No entanto, não existe nenhuma sintaxe ou palavra-chave especial para marcar um método como substituído, o que pode causar confusão. É por isso que a anotação
@Override foi introduzida : sempre que seu objetivo for substituir um método herdado, use a anotação
@Override para indicá-lo de forma sucinta. Outro dilema que os desenvolvedores Java enfrentam constantemente no design é a construção de hierarquias de classes (com classes concretas ou abstratas) versus a implementação de interfaces. Recomendamos fortemente favorecer interfaces em vez de classes ou classes abstratas sempre que possível. As interfaces são mais leves, mais fáceis de testar e manter e também minimizam os efeitos colaterais das mudanças de implementação. Muitas técnicas avançadas de programação, como a criação de classes proxy na biblioteca padrão Java, dependem fortemente de interfaces.
10. HERANÇA MÚLTIPLA
Ao contrário do C++ e de algumas outras linguagens, Java não suporta herança múltipla: em Java, cada classe pode ter apenas um pai direto (com a classe
Object
no topo da hierarquia). No entanto, uma classe pode implementar múltiplas interfaces e, portanto, o empilhamento de interfaces é a única maneira de obter (ou simular) herança múltipla em Java.
package com.javacodegeeks.advanced.design;
public class MultipleInterfaces implements Runnable, AutoCloseable {
@Override
public void run() {
}
@Override
public void close() throws Exception {
}
}
A implementação de múltiplas interfaces é, na verdade, bastante poderosa, mas muitas vezes a necessidade de usar a implementação repetidas vezes leva a uma profunda hierarquia de classes como forma de superar a falta de suporte do Java para herança múltipla.
public class A implements Runnable {
@Override
public void run() {
}
}
public class B extends A implements AutoCloseable {
@Override
public void close() throws Exception {
}
}
public class C extends B implements Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
E assim por diante... O lançamento recente do Java 8 resolve um pouco o problema com a injeção de método padrão. Devido aos métodos padrão, as interfaces fornecem não apenas um contrato, mas também uma implementação. Portanto, as classes que implementam essas interfaces também herdarão automaticamente esses métodos implementados. Por exemplo:
package com.javacodegeeks.advanced.design;
public interface DefaultMethods extends Runnable, AutoCloseable {
@Override
default void run() {
}
@Override
default void close() throws Exception {
}
}
public class C implements DefaultMethods, Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
Tenha em mente que a herança múltipla é uma ferramenta muito poderosa, mas ao mesmo tempo perigosa. O conhecido problema do Diamante da Morte é frequentemente citado como uma grande falha na implementação de herança múltipla, forçando os desenvolvedores a projetar hierarquias de classes com muito cuidado. Infelizmente, as interfaces do Java 8 com métodos padrão também são vítimas desses defeitos.
interface A {
default void performAction() {
}
}
interface B extends A {
@Override
default void performAction() {
}
}
interface C extends A {
@Override
default void performAction() {
}
}
Por exemplo, o seguinte trecho de código não será compilado:
interface E extends B, C {
}
Neste ponto, é justo dizer que Java como linguagem sempre tentou evitar os casos extremos da programação orientada a objetos, mas à medida que a linguagem evolui, alguns desses casos começaram a aparecer repentinamente.
11. HERANÇA E COMPOSIÇÃO
Felizmente, a herança não é a única maneira de projetar sua classe. Outra alternativa que muitos desenvolvedores acreditam ser muito melhor que a herança é a composição. A ideia é muito simples: ao invés de criar uma hierarquia de classes, elas precisam ser compostas por outras classes. Vejamos este exemplo:
interface E extends B, C {
}
A aula
Vehicle
é composta por motor e rodas (além de muitas outras peças que são deixadas de lado para simplificar). Porém, pode-se dizer que uma classe
Vehicle
também é um motor, portanto pode ser projetada por meio de herança.
public class Vehicle extends Engine {
private Wheels[] wheels;
}
Qual solução de design seria correta? As diretrizes básicas gerais são conhecidas como princípios IS-A (é) e HAS-A (contém). IS-A é um relacionamento de herança: uma subclasse também satisfaz a especificação de classe da classe pai e uma variação da classe pai. subclasse) estende seu pai. Se você quiser saber se uma entidade estende outra, faça um teste de correspondência - IS -A (é).") Portanto, HAS-A é um relacionamento de composição: uma classe possui (ou contém) um objeto que Na maioria dos casos, o princípio HAS-A funciona melhor que IS-A por uma série de razões:
- O design é mais flexível;
- O modelo é mais estável porque a mudança não se propaga pela hierarquia de classes;
- Uma classe e sua composição são fracamente acopladas em comparação com a composição, que acopla fortemente um pai e sua subclasse.
- A linha lógica de pensamento em uma classe é mais simples, pois todas as suas dependências estão incluídas nela, em um só lugar.
Independentemente disso, a herança tem o seu lugar e resolve vários problemas de design existentes de diversas maneiras, por isso não deve ser negligenciada. Tenha essas duas alternativas em mente ao projetar seu modelo orientado a objetos.
12. ENCAPSULAMENTO.
O conceito de encapsulamento na programação orientada a objetos é ocultar todos os detalhes de implementação (como modo operacional, métodos internos, etc.) do mundo exterior. Os benefícios do encapsulamento são a facilidade de manutenção e a facilidade de mudança. A implementação interna da classe fica oculta, o trabalho com os dados da classe ocorre exclusivamente através dos métodos públicos da classe (um verdadeiro problema se você estiver desenvolvendo uma biblioteca ou framework utilizado por muitas pessoas). O encapsulamento em Java é obtido por meio de regras de visibilidade e acessibilidade. Em Java, é considerada prática recomendada nunca expor campos diretamente, apenas por meio de getters e setters (a menos que os campos sejam marcados como finais). Por exemplo:
package com.javacodegeeks.advanced.design;
public class Encapsulation {
private final String email;
private String address;
public Encapsulation( final String email ) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
}
Este exemplo lembra o que é chamado de
JavaBeans na linguagem Java: classes Java padrão são escritas de acordo com um conjunto de convenções, uma das quais permite que os campos sejam acessados apenas usando métodos getter e setter. Como já enfatizamos na seção de herança, siga sempre o contrato mínimo de publicidade em uma aula, utilizando os princípios do encapsulamento. Tudo o que não deveria ser público deve se tornar privado (ou protegido/pacote privado, dependendo do problema que você está resolvendo). Isso terá retorno no longo prazo, dando-lhe liberdade para projetar sem (ou pelo menos minimizar) alterações significativas.
13. AULAS E MÉTODOS FINAIS
Em Java, existe uma maneira de evitar que uma classe se torne uma subclasse de outra classe: a outra classe deve ser declarada final.
package com.javacodegeeks.advanced.design;
public final class FinalClass {
}
A mesma palavra-chave
final em uma declaração de método evita que as subclasses substituam o método.
package com.javacodegeeks.advanced.design;
public class FinalMethod {
public final void performAction() {
}
}
Não existem regras gerais para decidir se uma classe ou métodos devem ser finais ou não. As classes e métodos finais limitam a extensibilidade e é muito difícil pensar no futuro se uma classe deve ou não ser herdada, ou se um método deve ou não ser substituído no futuro. Isto é especialmente importante para desenvolvedores de bibliotecas, uma vez que decisões de design como essa podem limitar significativamente a aplicabilidade da biblioteca. A Java Standard Library possui vários exemplos de classes finais, sendo a mais famosa a classe String. Numa fase inicial, esta decisão foi tomada para evitar qualquer tentativa dos desenvolvedores de encontrar a sua própria e “melhor” solução para implementar strings.
14. O QUE SE SEGUE
Nesta parte da lição, abordamos os conceitos de programação orientada a objetos em Java. Também demos uma rápida olhada na programação de contratos, abordando alguns conceitos funcionais e vendo como a linguagem evoluiu ao longo do tempo. Na próxima parte da lição, conheceremos os genéricos e como eles mudam a maneira como abordamos a segurança de tipos na programação.
15. BAIXAR CÓDIGO FONTE
Você pode baixar a fonte aqui -
advanced-java-part-3 Fonte:
Como projetar classes e
GO TO FULL VERSION