JavaRush /Blogue Java /Random-PT /Projetando Classes e Interfaces (Tradução do artigo)
fatesha
Nível 22

Projetando Classes e Interfaces (Tradução do artigo)

Publicado no grupo Random-PT
Projetando Classes e Interfaces (Tradução do artigo) - 1

Contente

  1. Introdução
  2. Interfaces
  3. Marcadores de interface
  4. Interfaces funcionais, métodos estáticos e métodos padrão
  5. Aulas abstratas
  6. Classes imutáveis ​​(permanentes)
  7. Aulas anônimas
  8. Visibilidade
  9. Herança
  10. Herança múltipla
  11. Herança e composição
  12. Encapsulamento
  13. Aulas e métodos finais
  14. Qual é o próximo
  15. 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 SimpleInterfacedeclara 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() {
        // Implementation here
    }
}
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() {
        // Implementation here
    }
}
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. RunnableA 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() {
        // Implementation here
    }

    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(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
Neste exemplo, a implementação da Runnableinterface é 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( () -> { /* Implementation here */ } ).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. Projetando Classes e Interfaces (Tradução do artigo) - 2A 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 Parente sua subclasse Childpara demonstrar a diferença nos níveis de visibilidade e seus efeitos.
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    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 Objectno 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() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
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() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
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() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
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:
// E is not compilable unless it overrides performAction() as well
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:
// E is not compilable unless it overrides performAction() as well
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 Vehicletambé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
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION