JavaRush /Blogue Java /Random-PT /Padrões de projeto em Java
Viacheslav
Nível 3

Padrões de projeto em Java

Publicado no grupo Random-PT
Padrões ou padrões de design são uma parte frequentemente negligenciada do trabalho de um desenvolvedor, tornando o código difícil de manter e adaptar a novos requisitos. Sugiro que você veja o que é e como é usado no JDK. Naturalmente, todos os padrões básicos, de uma forma ou de outra, já existem há muito tempo. Vamos vê-los nesta revisão.
Padrões de Projeto em Java - 1
Contente:

Modelos

Um dos requisitos mais comuns nas vagas é “Conhecimento de padrões”. Em primeiro lugar, vale a pena responder a uma pergunta simples - “O que é um Design Pattern?” O padrão é traduzido do inglês como “modelo”. Ou seja, este é um certo padrão segundo o qual fazemos algo. O mesmo acontece na programação. Existem algumas melhores práticas e abordagens estabelecidas para resolver problemas comuns. Todo programador é um arquiteto. Mesmo quando você cria apenas algumas classes ou mesmo uma, depende de você quanto tempo o código pode sobreviver sob mudanças de requisitos, quão conveniente é ser usado por outros. E é aqui que o conhecimento dos templates vai ajudar, porque... Isso permitirá que você entenda rapidamente a melhor forma de escrever código sem reescrevê-lo. Como você sabe, os programadores são pessoas preguiçosas e é mais fácil escrever algo bem imediatamente do que refazê-lo várias vezes. Os padrões também podem parecer semelhantes aos algoritmos. Mas eles têm uma diferença. O algoritmo consiste em etapas específicas que descrevem as ações necessárias. Os padrões descrevem apenas a abordagem, mas não descrevem as etapas de implementação. Os padrões são diferentes porque... resolver problemas diferentes. Normalmente as seguintes categorias são diferenciadas:
  • Generativo

    Esses padrões resolvem o problema de tornar flexível a criação de objetos

  • Estrutural

    Esses padrões resolvem o problema de construir efetivamente conexões entre objetos

  • Comportamental

    Esses padrões resolvem o problema da interação efetiva entre objetos

Para considerar exemplos, sugiro usar o compilador de código online repl.it.
Padrões de Projeto em Java - 2

Padrões de criação

Vamos começar do início do ciclo de vida dos objetos - com a criação dos objetos. Os modelos generativos ajudam a criar objetos de maneira mais conveniente e fornecem flexibilidade nesse processo. Um dos mais famosos é o “ Construtor ”. Este padrão permite criar objetos complexos passo a passo. Em Java, o exemplo mais famoso é StringBuilder:
class Main {
  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    builder.append("Hello");
    builder.append(',');
    builder.append("World!");
    System.out.println(builder.toString());
  }
}
Outra abordagem bem conhecida para criar um objeto é mover a criação para um método separado. Este método torna-se, por assim dizer, uma fábrica de objetos. É por isso que o padrão é chamado de " Método Fábrica ". Em Java, por exemplo, seu efeito pode ser visto na classe java.util.Calendar. A classe em si Calendaré abstrata e para criá-la é usado o método getInstance:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());
    System.out.println(calendar.getClass().getCanonicalName());
  }
}
Muitas vezes, isso ocorre porque a lógica por trás da criação de objetos pode ser complexa. Por exemplo, no caso acima, acessamos a classe base Calendar, e uma classe é criada GregorianCalendar. Se olharmos para o construtor, podemos ver que diferentes implementações são criadas dependendo das condições Calendar. Mas às vezes um método de fábrica não é suficiente. Às vezes você precisa criar objetos diferentes para que eles se encaixem. Outro modelo nos ajudará nisso - “ Fábrica abstrata ”. E então precisamos criar fábricas diferentes em um só lugar. Ao mesmo tempo, a vantagem é que os detalhes da implementação não são importantes para nós, ou seja, não importa qual fábrica específica adquirimos. O principal é que ele crie as implementações corretas. Superexemplo:
Padrões de Projeto em Java - 3
Ou seja, dependendo do ambiente (sistema operacional), conseguiremos uma determinada fábrica que criará elementos compatíveis. Como alternativa à abordagem de criação através de outra pessoa, podemos usar o padrão “ Protótipo ”. Sua essência é simples - novos objetos são criados à imagem e semelhança de objetos já existentes, ou seja, de acordo com seu protótipo. Todo mundo já encontrou esse padrão em Java - este é o uso de uma interface java.lang.Cloneable:
class Main {
  public static void main(String[] args) {
    class CloneObject implements Cloneable {
      @Override
      protected Object clone() throws CloneNotSupportedException {
        return new CloneObject();
      }
    }
    CloneObject obj = new CloneObject();
    try {
      CloneObject pattern = (CloneObject) obj.clone();
    } catch (CloneNotSupportedException e) {
      //Do something
    }
  }
}
Como você pode ver, o chamador não sabe como o arquivo clone. Ou seja, a criação de um objeto baseado em um protótipo é de responsabilidade do próprio objeto. Isso é útil porque não vincula o usuário à implementação do objeto modelo. Bem, o último desta lista é o padrão “Singleton”. Seu objetivo é simples: fornecer uma única instância de um objeto para todo o aplicativo. Esse padrão é interessante porque geralmente mostra problemas de multithreading. Para uma visão mais aprofundada, confira estes artigos:
Padrões de Projeto em Java - 4

Padrões estruturais

Com a criação dos objetos ficou mais claro. Agora é a hora de examinar os padrões estruturais. Seu objetivo é construir hierarquias de classe fáceis de suportar e seus relacionamentos. Um dos primeiros e mais conhecidos padrões é o “ Deputado ” (Proxy). O proxy possui a mesma interface do objeto real, portanto não faz diferença o cliente trabalhar através do proxy ou diretamente. O exemplo mais simples é java.lang.reflect.Proxy :
import java.util.*;
import java.lang.reflect.*;
class Main {
  public static void main(String[] arguments) {
    final Map<String, String> original = new HashMap<>();
    InvocationHandler proxy = (obj, method, args) -> {
      System.out.println("Invoked: " + method.getName());
      return method.invoke(original, args);
    };
    Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
        original.getClass().getClassLoader(),
        original.getClass().getInterfaces(),
        proxy);
    proxyInstance.put("key", "value");
    System.out.println(proxyInstance.get("key"));
  }
}
Como você pode ver, no exemplo temos o original - é aquele HashMapque implementa a interface Map. A seguir criamos um proxy que substitui o original HashMappara a parte do cliente, que chama os métodos pute get, adicionando nossa própria lógica durante a chamada. Como podemos ver, a interação no padrão ocorre através de interfaces. Mas às vezes um substituto não é suficiente. E então o padrão “ Decorador ” pode ser usado. Um decorador também é chamado de wrapper ou wrapper. Proxy e decorador são muito parecidos, mas se você olhar o exemplo, verá a diferença:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    List<String> list = new ArrayList<>();
    List<String> decorated = Collections.checkedList(list, String.class);
    decorated.add("2");
    list.add("3");
    System.out.println(decorated);
  }
}
Ao contrário de um proxy, um decorador envolve algo que é passado como entrada. Um proxy pode aceitar o que precisa ser proxy e também gerenciar a vida útil do objeto proxy (por exemplo, criar um objeto proxy). Existe outro padrão interessante - “ Adaptador ”. É semelhante a um decorador - o decorador pega um objeto como entrada e retorna um wrapper sobre esse objeto. A diferença é que o objetivo não é alterar a funcionalidade, mas sim adaptar uma interface a outra. Java tem um exemplo muito claro disso:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    String[] array = {"One", "Two", "Three"};
    List<String> strings = Arrays.asList(array);
    strings.set(0, "1");
    System.out.println(Arrays.toString(array));
  }
}
Na entrada temos um array. A seguir, criamos um adaptador que traz o array para a interface List. Ao trabalhar com ele, estamos na verdade trabalhando com um array. Portanto, adicionar elementos não funcionará, porque... A matriz original não pode ser alterada. E neste caso obteremos UnsupportedOperationException. A próxima abordagem interessante para desenvolver a estrutura de classes é o padrão Composite . É interessante que um determinado conjunto de elementos usando uma interface seja organizado em uma certa hierarquia semelhante a uma árvore. Ao chamar um método em um elemento pai, obtemos uma chamada para esse método em todos os elementos filhos necessários. Um excelente exemplo desse padrão é a UI (seja java.awt ou JSF):
import java.awt.*;
class Main {
  public static void main(String[] arguments) {
    Container container = new Container();
    Component component = new java.awt.Component(){};
    System.out.println(component.getComponentOrientation().isLeftToRight());
    container.add(component);
    container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
    System.out.println(component.getComponentOrientation().isLeftToRight());
  }
}
Como podemos ver, adicionamos um componente ao contêiner. E então pedimos ao container para aplicar a nova orientação dos componentes. E o container, sabendo em que componentes consiste, delegou a execução deste comando a todos os componentes filhos. Outro padrão interessante é o padrão “ Ponte ”. É chamado assim porque descreve uma conexão ou ponte entre duas hierarquias de classes diferentes. Uma dessas hierarquias é considerada uma abstração e a outra uma implementação. Isto é destacado porque a abstração em si não executa ações, mas delega essa execução para a implementação. Este padrão é frequentemente usado quando existem classes de “controle” e vários tipos de classes de “plataforma” (por exemplo, Windows, Linux, etc.). Com esta abordagem, uma dessas hierarquias (abstração) receberá uma referência a objetos de outra hierarquia (implementação) e delegará a eles o trabalho principal. Como todas as implementações seguirão uma interface comum, elas poderão ser trocadas dentro da abstração. Em Java, um exemplo claro disso é java.awt:
Padrões de Projeto em Java - 5
Para mais informações, consulte o artigo " Padrões em Java AWT ". Entre os padrões estruturais, gostaria também de destacar o padrão “ Fachada ”. Sua essência é esconder a complexidade do uso das bibliotecas/frameworks por trás desta API por trás de uma interface conveniente e concisa. Por exemplo, você pode usar JSF ou EntityManager do JPA como exemplo. Existe também outro padrão chamado “ Flyweight ”. Sua essência é que, se objetos diferentes tiverem o mesmo estado, ele poderá ser generalizado e armazenado não em cada objeto, mas em um só lugar. E então cada objeto poderá fazer referência a uma parte comum, o que reduzirá os custos de memória para armazenamento. Esse padrão geralmente envolve o pré-armazenamento em cache ou a manutenção de um conjunto de objetos. Curiosamente, também conhecemos esse padrão desde o início:
Padrões de Projeto em Java - 6
Pela mesma analogia, um conjunto de strings pode ser incluído aqui. Você pode ler o artigo sobre este tópico: " Flyweight Design Pattern ".
Padrões de Projeto em Java - 7

Padrões comportamentais

Então, descobrimos como os objetos podem ser criados e como as conexões entre as classes podem ser organizadas. O mais interessante que resta é proporcionar flexibilidade na mudança do comportamento dos objetos. E os padrões comportamentais nos ajudarão nisso. Um dos padrões mencionados com mais frequência é o padrão " Estratégia ". É aqui que começa o estudo dos padrões no livro “ De Cabeça. Padrões de Design ”. Utilizando o padrão “Estratégia”, podemos armazenar dentro de um objeto como iremos realizar a ação, ou seja, o objeto interno armazena uma estratégia que pode ser alterada durante a execução do código. Este é um padrão que costumamos usar quando usamos um comparador:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Comparator<String> comparator = Comparator.comparingInt(String::length);
    Set dataSet = new TreeSet(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Antes de nós - TreeSet. Tem o comportamento de TreeSetmanter a ordem dos elementos, ou seja, os classifica (já que é um SortedSet). Este comportamento possui uma estratégia padrão, que vemos no JavaDoc: ordenação em "ordenação natural" (para strings, esta é a ordem lexicográfica). Isso acontece se você usar um construtor sem parâmetros. Mas se quisermos mudar a estratégia, podemos passar Comparator. Neste exemplo, podemos criar nosso conjunto como new TreeSet(comparator), e então a ordem de armazenamento dos elementos (estratégia de armazenamento) mudará para aquela especificada no comparador. Curiosamente, existe quase o mesmo padrão chamado “ Estado ”. O padrão “Estado” diz que se tivermos algum comportamento no objeto principal que depende do estado desse objeto, então podemos descrever o próprio estado como um objeto e alterar o estado do objeto. E delegue chamadas do objeto principal para o estado. Outro padrão que conhecemos ao estudar os fundamentos da linguagem Java é o padrão “ Command ”. Este padrão de design sugere que comandos diferentes podem ser representados como classes diferentes. Este padrão é muito semelhante ao padrão Estratégia. Mas no padrão Strategy, estávamos redefinindo como uma ação específica seria executada (por exemplo, classificação em TreeSet). No padrão “Comando”, redefinimos qual ação será executada. O comando padrão está conosco todos os dias quando usamos threads:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Runnable command = () -> {
      System.out.println("Command action");
    };
    Thread th = new Thread(command);
    th.start();
  }
}
Como você pode ver, comando define uma ação ou comando que será executado em um novo thread. Também vale a pena considerar o padrão “ Cadeia de responsabilidade ”. Esse padrão também é muito simples. Esse padrão diz que se algo precisar ser processado, você poderá coletar manipuladores em uma cadeia. Por exemplo, esse padrão é frequentemente usado em servidores web. Na entrada, o servidor recebe alguma solicitação do usuário. Essa solicitação passa então pela cadeia de processamento. Esta cadeia de manipuladores inclui filtros (por exemplo, não aceitar solicitações de uma lista negra de endereços IP), manipuladores de autenticação (permitir apenas usuários autorizados), um manipulador de cabeçalho de solicitação, um manipulador de cache, etc. Mas existe um exemplo mais simples e compreensível em Java java.util.logging:
import java.util.logging.*;
class Main {
  public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    ConsoleHandler consoleHandler = new ConsoleHandler(){
		@Override
            public void publish(LogRecord record) {
                System.out.println("LogRecord обработан");
            }
        };
    logger.addHandler(consoleHandler);
    logger.info("test");
  }
}
Como você pode ver, os manipuladores são adicionados à lista de manipuladores do logger. Quando um criador de logs recebe uma mensagem para processamento, cada mensagem passa por uma cadeia de manipuladores (de logger.getHandlers) para esse criador de logs. Outro padrão que vemos todos os dias é o “ Iterador ”. Sua essência é separar uma coleção de objetos (ou seja, uma classe que representa uma estrutura de dados. Por exemplo, List) e percorrer essa coleção.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Iterator<String> iterator = data.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}
Como você pode ver, o iterador não faz parte da coleção, mas é representado por uma classe separada que percorre a coleção. O usuário do iterador pode nem saber em qual coleção ele está iterando, ou seja, que coleção ele está visitando? Vale considerar o padrão “ Visitante ”. O padrão do visitante é muito semelhante ao padrão do iterador. Esse padrão ajuda você a ignorar a estrutura dos objetos e executar ações nesses objetos. Eles diferem bastante no conceito. O iterador percorre a coleção para que o cliente que usa o iterador não se importe com o que a coleção está dentro, apenas os elementos da sequência são importantes. O visitante significa que existe uma certa hierarquia ou estrutura dos objetos que visitamos. Por exemplo, podemos usar processamento de diretório separado e processamento de arquivo separado. Java tem uma implementação pronta para uso desse padrão na forma java.nio.file.FileVisitor:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
  public static void main(String[] args) {
    SimpleFileVisitor visitor = new SimpleFileVisitor() {
      @Override
      public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        System.out.println("File:" + file.toString());
        return FileVisitResult.CONTINUE;
      }
    };
    Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
    try {
      Files.walkFileTree(pathSource, visitor);
    } catch (AccessDeniedException e) {
      // skip
    } catch (IOException e) {
      // Do something
    }
  }
}
Às vezes, é necessário que alguns objetos reajam às mudanças em outros objetos, e então o padrão “Observador” nos ajudará . A maneira mais conveniente é fornecer um mecanismo de assinatura que permita que alguns objetos monitorem e respondam a eventos que ocorrem em outros objetos. Este padrão é frequentemente usado em vários ouvintes e observadores que reagem a diferentes eventos. Como exemplo simples, podemos relembrar a implementação deste padrão desde a primeira versão do JDK:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Observer observer = (obj, arg) -> {
      System.out.println("Arg: " + arg);
    };
    Observable target = new Observable(){
      @Override
      public void notifyObservers(Object arg) {
        setChanged();
        super.notifyObservers(arg);
      }
    };
    target.addObserver(observer);
    target.notifyObservers("Hello, World!");
  }
}
Existe outro padrão comportamental útil - “ Mediador ”. É útil porque em sistemas complexos ajuda a remover a conexão entre diferentes objetos e a delegar todas as interações entre objetos a algum objeto, que é um intermediário. Uma das aplicações mais marcantes desse padrão é o Spring MVC, que utiliza esse padrão. Você pode ler mais sobre isso aqui: " Spring: Mediator Pattern ". Muitas vezes você pode ver o mesmo em exemplos java.util.Timer:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Timer mediator = new Timer("Mediator");
    TimerTask command = new TimerTask() {
      @Override
      public void run() {
        System.out.println("Command pattern");
        mediator.cancel();
      }
    };
    mediator.schedule(command, 1000);
  }
}
O exemplo se parece mais com um padrão de comando. E a essência do padrão "Mediador" está oculta na implementação de Timer'a. Dentro do cronômetro existe uma fila de tarefas TaskQueue, existe um thread TimerThread. Nós, como clientes desta classe, não interagimos com eles, mas interagimos com Timero objeto, que, em resposta à nossa chamada aos seus métodos, acessa os métodos de outros objetos dos quais é intermediário. Externamente pode parecer muito semelhante à "Fachada". Mas a diferença é que quando se utiliza uma Fachada, os componentes não sabem que a fachada existe e conversam entre si. E quando o "Mediador" é usado, os componentes conhecem e usam o intermediário, mas não entram em contato diretamente entre si. Vale a pena considerar o padrão “ Template Method ". O padrão fica claro pelo seu nome. O resultado final é que o código é escrito de tal forma que os usuários do código (desenvolvedores) recebem algum modelo de algoritmo, cujas etapas podem ser redefinidas. Isso permite que os usuários do código não escrevam o algoritmo inteiro, mas pensem apenas em como executar corretamente uma ou outra etapa desse algoritmo. Por exemplo, Java possui uma classe abstrata AbstractListque define o comportamento de um iterador por List. No entanto, o próprio iterador usa métodos folha como: get, set, remove. O comportamento desses métodos é determinado pelo desenvolvedor dos descendentes AbstractList. Assim, o iterador em AbstractList- é um modelo para o algoritmo de iteração em uma planilha. E os desenvolvedores de implementações específicas AbstractListalteram o comportamento dessa iteração definindo o comportamento de etapas específicas. O último dos padrões que analisamos é o padrão “ Snapshot ” (Momento). Sua essência é preservar um determinado estado de um objeto com a capacidade de restaurar esse estado. O exemplo mais reconhecível do JDK é a serialização de objetos, ou seja, java.io.Serializable. Vejamos um exemplo:
import java.io.*;
import java.util.*;
class Main {
  public static void main(String[] args) throws IOException {
    ArrayList<String> list = new ArrayList<>();
    list.add("test");
    // Save State
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
      out.writeObject(list);
    }
    // Load state
    byte[] bytes = stream.toByteArray();
    InputStream inputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
      List<String> listNew = (List<String>) in.readObject();
      System.out.println(listNew.get(0));
    } catch (ClassNotFoundException e) {
      // Do something. Can't find class fpr saved state
    }
  }
}
Padrões de Projeto em Java - 8

Conclusão

Como vimos na análise, há uma grande variedade de padrões. Cada um deles resolve seu próprio problema. E o conhecimento desses padrões pode ajudá-lo a entender a tempo como escrever seu sistema para que seja flexível, sustentável e resistente a mudanças. E, finalmente, alguns links para um mergulho mais profundo: #Viacheslav
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION