Como os Padrões de Design Podem Transformar Sua Programação: Uma Análise Aprofundada

Descubra como a aplicação inteligente dos padrões de design pode revolucionar a sua abordagem de programação. Desde a escalabilidade até a manutenção, mergulhe nesse artigo para desvendar os segredos de um código mais eficiente e robusto.
Guilherme Ferreira | 8 de agosto de 2023

Como programador, sempre busco maneiras de melhorar a eficiência e a clareza do meu código. Durante a minha jornada, deparei-me com uma estratégia poderosa que transformou completamente a maneira como programo: os padrões de design. Gostaria de compartilhar com vocês o poder dessas ferramentas e como elas podem elevar suas habilidades de programação a um novo patamar.

Neste artigo, faremos uma análise aprofundada de como os padrões de design podem transformar sua programação, melhorando a qualidade do seu código, facilitando a manutenção e aumentando a eficiência do seu trabalho. Se você, assim como eu, tem a curiosidade e a paixão para aprender constantemente e se esforça para ser o melhor desenvolvedor que pode ser, então este artigo é para você.

Vamos navegar pelo fascinante mundo dos padrões de design. Inicialmente, vamos esclarecer o que são esses padrões, sua origem e por que eles são tão fundamentais na programação. Em seguida, mergulharemos nos principais tipos de padrões de design: criacionais, estruturais e comportamentais, elucidando suas funcionalidades e aplicações. Para trazer essas ideias abstratas à vida, iremos explorar exemplos práticos de códigos.

Além disso, discutiremos como os padrões de design podem melhorar a legibilidade, escalabilidade e manutenibilidade do seu código, tornando-o um instrumento ainda mais poderoso em suas mãos. Também nos propomos a desmitificar alguns mitos frequentemente associados aos padrões de design. E para finalizar, vamos compartilhar dicas práticas e recomendações sobre como começar a aplicar os padrões de design na sua rotina de programação.

O que são padrões de design?

Padrões de design são soluções comprovadas para problemas comuns que os desenvolvedores enfrentam durante o processo de programação. De forma simples, eles são como modelos que podem ser seguidos para resolver determinadas situações. Não são códigos prontos que podem ser diretamente colados em seu programa, mas sim conceitos de alto nível que guiam a estruturação e a organização do seu código.

A origem dos padrões de design pode ser rastreada até o livro seminal “Design Patterns: Elements of Reusable Object-Oriented Software”, publicado em 1994 por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides - também conhecidos como “Gang of Four” (Gangue dos Quatro). Neste trabalho, os autores definem 23 padrões de design que se tornaram a base para muitos outros que vieram a seguir.

Os padrões de design são valiosos porque eles encapsulam o conhecimento coletivo de desenvolvedores experientes, fornecendo uma linguagem comum que facilita a comunicação entre os programadores. Quando um desenvolvedor diz, por exemplo, “Vamos usar o padrão Singleton aqui”, todos na equipe entendem o que isso significa em termos de estrutura e comportamento do código.

Em resumo, padrões de design são uma poderosa adição ao conjunto de habilidades de qualquer programador. Eles melhoram a qualidade do código, facilitam a manutenção, promovem a reutilização de código e tornam a comunicação entre desenvolvedores mais eficiente. No entanto, eles devem ser aplicados com discernimento, sempre tendo em mente que o objetivo é resolver problemas de maneira eficiente, e não apenas usar um padrão por usar.

Os tipos principais de padrões de design

Agora que já esclarecemos o que são padrões de design e sua importância, vamos nos aprofundar um pouco mais e explorar os três tipos principais desses padrões: os criacionais, os estruturais e os comportamentais. Cada um desses tipos tem um propósito distinto e resolve um conjunto específico de problemas. Ao compreender esses três tipos de padrões de design, você estará equipado para lidar com uma ampla gama de desafios na programação. Então, sem mais delongas, vamos explorar cada um deles.

Padrões de design criacionais se preocupam principalmente com a maneira como os objetos são criados. Eles fornecem mecanismos de criação de objetos que aumentam a flexibilidade e a reutilização do código existente. Exemplos notáveis de padrões criacionais incluem o Singleton, que garante que apenas uma instância de uma classe exista, o Builder, que proporciona uma maneira de construir um objeto passo a passo, e o Factory Method, que permite a uma classe delegar a instanciação a subclasses.

Padrões de design estruturais lidam com a composição de classes e objetos para formar estruturas maiores. Eles garantem que as partes se encaixem de maneira a fazer o todo funcionar. Por exemplo, o padrão Adapter permite que classes com interfaces incompatíveis trabalhem juntas, o padrão Decorator adiciona responsabilidades a um objeto de maneira dinâmica, e o padrão Composite permite tratar um grupo de objetos da mesma maneira que se trataria um único objeto.

Padrões de design comportamentais estão preocupados com a comunicação e a distribuição de responsabilidades entre objetos. Eles lidam com algoritmos e a atribuição de responsabilidades entre objetos. Alguns exemplos são o padrão Observer, que define uma dependência um-para-muitos entre objetos, permitindo que quando um objeto muda de estado, todos os seus dependentes sejam notificados e atualizados automaticamente, e o padrão Strategy, que permite a um objeto alterar o comportamento em tempo de execução.

Cada um desses tipos de padrões de design tem um papel específico na solução de problemas comuns de programação, e um bom desenvolvedor deve ter uma compreensão sólida de todos eles.

Exemplos práticos de padrões de design

Embora os padrões de design sejam vastos e variados em seus propósitos e aplicações, não vamos nos aprofundar em todos eles neste momento. Nossa exploração será focada em quatro padrões icônicos que exemplificam cada categoria: o Singleton (criacional), o Factory (criacional), o Observer (comportamental) e, por fim, o Adapter (estrutural). Esses padrões foram escolhidos não apenas por sua popularidade, mas também porque representam claramente os objetivos e soluções de seus respectivos tipos de padrão.

Ao utilizarmos Java e Elixir para nossos exemplos, buscamos mostrar como esses padrões se manifestam tanto em uma linguagem orientada a objetos quanto em uma funcional. Mesmo que você não esteja totalmente confortável com alguma dessas linguagens, o foco aqui é o padrão em si e sua aplicabilidade, e não as especificidades da linguagem. Por isso, encorajo você a focar nos princípios e no design, em vez de se prender nos detalhes da sintaxe.

Assim, sem mais demora, vamos mergulhar no mundo dos padrões de design e ver como esses quatro padrões específicos podem ser implementados em diferentes contextos e linguagens.

Padrão Singleton

O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Ele resolve o problema de garantir que uma determinada classe tenha apenas uma instância durante o ciclo de vida de uma aplicação e fornece um ponto de acesso global para essa instância. Isso é particularmente útil quando precisamos ter um único ponto de controle, como uma configuração centralizada ou um pool de conexões. Por exemplo, imagine uma aplicação que precisa gerenciar conexões com um banco de dados. Sem o Singleton, cada vez que solicitássemos uma conexão, poderíamos criar uma nova, o que seria ineficiente e poderia levar a exaustão de recursos. Com o padrão Singleton, podemos ter certeza de que usamos a mesma conexão ou pool de conexões em toda a aplicação, garantindo eficiência e consistência.

Vamos ver um exemplo em Java:

public class Logger { // 1. A única instância é mantida na variável de classe ‘instance’ private static Logger instance;

// 2. O construtor privado impede a instanciação da classe a partir do exterior
private Logger() {}

// 3. O método público 'getInstance' é o único ponto de acesso global ao objeto 
singleton
public static synchronized Logger getInstance() {
    if (instance == null) {
        instance = new Logger();
    }
    return instance;
}

public void log(String message) {
    System.out.println("Log: " + message);
}

// Uso Logger logger = Logger.getInstance(); logger.log(“This is a log message.”);

O padrão Singleton também pode ser implementado em Elixir, apesar de ser uma linguagem funcional. Uma abordagem comum no ecossistema Erlang/Elixir é usar GenServer para isso. GenServer é um comportamento OTP que implementa um servidor genérico em um processo que pode manter estado. Como os processos em Erlang e Elixir são leves e isolados, podemos usar um GenServer para criar e manter uma única instância de um recurso.

Aqui está um exemplo de como você pode implementar o padrão Singleton em Elixir, seguindo o exemplo do “Logger” em Java:

defmodule LoggerSingleton do
use GenServer

# API

# Inicia o GenServer ou retorna o PID existente.
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end

# Método para obter a instância única (PID) do GenServer.
def get_instance do
case GenServer.whereis(__MODULE__) do
nil -> {:error, "Logger not started"}
pid -> pid
end
end

# Método de conveniência para registrar mensagens.
def log(message) do
GenServer.call(__MODULE__, {:log, message})
end

# Funções de callback do GenServer

# Chamado quando o GenServer é iniciado.
def init(:ok) do
{:ok, nil}
end

# Chamado quando uma mensagem é enviada ao GenServer.
def handle_call({:log, message}, _from, state) do
IO.puts("Log: #{message}")
{:reply, :ok, state}
end
end

# Uso
# Inicie o GenServer.
{:ok, _pid} = LoggerSingleton.start_link()
# Use o Logger.
LoggerSingleton.log("This is a log message.")

Neste exemplo, o módulo LoggerSingleton implementa o padrão Singleton usando um GenServer. Quando start_link/0 é chamado, ele inicia um novo processo GenServer se ainda não estiver em execução. O método log/1 é uma função de conveniência que envia uma mensagem ao GenServer para registrar a mensagem fornecida. O estado mantido pelo GenServer neste caso é apenas nil, já que estamos usando-o principalmente para garantir que tenhamos uma única instância do logger em execução.

Padrão Factory

O padrão Factory fornece uma interface para criar objetos em uma superclasse, mas permite que as subclasses alterem o tipo de objetos que serão criados.

Um dos principais desafios no design orientado a objetos é a criação de instâncias de objetos sem especificar explicitamente a classe concreta do objeto. O padrão Factory aborda exatamente esse dilema, oferecendo um mecanismo para criar objetos sem necessitar referenciar diretamente suas classes concretas. Este padrão é particularmente útil quando o sistema precisa ser independente de como seus objetos são criados, compostos e representados. Por exemplo, imagine uma aplicação de e-commerce que precisa criar diferentes tipos de contas de usuário: Comprador, Vendedor e Administrador.

Em vez de instanciar cada tipo de conta diretamente, você pode usar uma fábrica que, com base em critérios específicos (como o tipo de permissões desejadas), devolve uma instância da conta apropriada. Isso torna o processo de criação mais flexível e centralizado, permitindo mudanças fáceis no futuro, caso novos tipos de contas precisem ser adicionados ou os existentes sejam modificados. Agora vamos ver o padrão Factory em Java:

public interface Animal {
void makeSound();
}

public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}

public class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}

public class AnimalFactory {
public Animal createAnimal(String type) {
if ("dog".equalsIgnoreCase(type)) {
return new Dog();
} else if ("cat".equalsIgnoreCase(type)) {
return new Cat();
}
throw new IllegalArgumentException("Invalid animal type");
}
}

// Uso
AnimalFactory animalFactory = new AnimalFactory();
Animal dog = animalFactory.createAnimal("dog");
dog.makeSound();

Neste exemplo, temos a AnimalFactory que é responsável por criar instâncias de diferentes classes (Dog e Cat) que implementam a interface Animal. Isso permite que novos tipos de animais sejam adicionados facilmente, seguindo o Princípio Aberto/Fechado (Open/Closed Principle), uma vez que AnimalFactory permanece fechado para modificações, mas aberto para extensões.

Em Elixir, devido à natureza funcional da linguagem, não temos classes e interfaces no sentido tradicional, como em Java. No entanto, podemos usar módulos, structs e o recurso de polimorfismo via protocolos para criar um padrão semelhante ao Factory. Vamos ver como você poderia modelar o padrão Factory em Elixir:

defprotocol Animal do
@spec make_sound(animal :: t) :: String.t()
def make_sound(animal)
end

defmodule Dog do
defstruct []

# Implementação do protocolo Animal para o Dog struct
defimpl Animal do
def make_sound(_dog) do
"Woof!"
end
end
end

defmodule Cat do
defstruct []

# Implementação do protocolo Animal para o Cat struct
defimpl Animal do
def make_sound(_cat) do
"Meow!"
end
end
end

defmodule AnimalFactory do
@spec create_animal(type :: String.t()) :: struct()
def create_animal("dog"), do: %Dog{}
def create_animal("cat"), do: %Cat{}
def create_animal(_), do: raise ArgumentError, "Invalid animal type"
end

# Uso:
animal = AnimalFactory.create_animal("dog")
IO.puts(Animal.make_sound(animal)):

Neste exemplo, o protocolo Animal define uma função make_sound/1 que espera ser implementada por qualquer módulo que implemente esse protocolo. Os módulos Dog e Cat são structs que implementam o protocolo Animal.

O AnimalFactory é um módulo que fornece uma função create_animal/1 para criar instâncias dos structs Dog e Cat. Ao seguir essa abordagem, se você quiser adicionar mais animais no futuro, simplesmente defina um novo módulo para esse animal, implemente o protocolo Animal e adicione uma cláusula correspondente na função create_animal/1 no AnimalFactory.

Isso oferece uma flexibilidade semelhante ao padrão Factory em Java, permitindo que novos tipos de animais sejam facilmente adicionados sem modificar o código existente (cumprindo o Princípio Aberto/Fechado).

Padrão Adapter

O padrão Adapter é um padrão de design estrutural que permite a colaboração de objetos com interfaces incompatíveis. Em outras palavras, permite que dois componentes de software que de outra forma não seriam compatíveis trabalhem juntos, agindo como uma espécie de “tradutor” ou “adaptador” entre eles. Este padrão pode ser pensado como um intermediário que transforma uma interface em outra, com base na necessidade do cliente.

Imagine um cenário em que você tem um sistema que depende de bibliotecas ou classes já existentes. Agora, você introduziu uma nova biblioteca ou classe, mas ela tem uma interface totalmente diferente da antiga. Reescrever todo o sistema para se adequar à nova interface não seria eficiente, nem sempre é possível. Então, como você faria o sistema antigo trabalhar em harmonia com o novo componente? Aqui é onde o padrão Adapter brilha.

Pense em um carregador de telefone universal. Se você viajar para outro país, pode descobrir que o formato da tomada é diferente. Em vez de substituir o cabo do seu carregador, você pode simplesmente usar um adaptador que faz a interface entre o plugue do seu carregador e a tomada estrangeira. Vamos ver isso em Java:

// Interface antiga
interface OldSystem {
void oldRequest();
}

class OldSystemImplementation implements OldSystem {
public void oldRequest() {
System.out.println("Old request.");
}
}

// Interface nova
interface NewSystem {
void newRequest();
}

// Adapter
class SystemAdapter implements NewSystem {
private OldSystem oldSystem;

public SystemAdapter(OldSystem oldSystem) {
this.oldSystem = oldSystem;
}

@Override
public void newRequest() {
oldSystem.oldRequest();
}
}

// Uso
public class AdapterDemo {
public static void main(String[] args) {
OldSystem oldSys = new OldSystemImplementation();
NewSystem newSys = new SystemAdapter(oldSys);
newSys.newRequest(); // Saída: "Old request."
}
}

Agora um exemplo de como ficaria em Elixir:

defmodule OldSystem do
def old_request do
IO.puts("Old request.")
end
end

defmodule NewSystem do
def new_request(adapter) do
adapter.request()
end
end

defmodule SystemAdapter do
def request do
OldSystem.old_request()
end
end

#Uso
NewSystem.new_request(SystemAdapter) #Saída: "Old request.

Nos exemplos acima, a função new_request no Elixir e o método newRequest em Java esperam trabalhar com o novo sistema. No entanto, com a ajuda do Adapter, eles são capazes de trabalhar com o sistema antigo, sem a necessidade de grandes mudanças no código existente.

Padrão Observer

O padrão Observer, é um padrão de design onde um objeto (conhecido como “sujeito” ou “observable”) mantém uma lista de dependentes (conhecidos como “observadores”) e os notifica de quaisquer mudanças de estado, geralmente por meio de um de seus métodos. Esse padrão é frequentemente usado em situações onde uma entidade precisa informar outras entidades sobre mudanças que possam ser interessantes para elas.

Imagine que você esteja desenvolvendo um aplicativo de previsão do tempo. Usuários podem configurar alertas para certos eventos climáticos, como “vai chover” ou “temperatura abaixo de 5°C”. Seu serviço de previsão do tempo atualiza suas previsões várias vezes ao dia.

Sem o padrão Observer, a cada atualização, você teria que verificar manualmente todos os tipos de alertas e para quais usuários enviar cada alerta. Isso rapidamente se torna complicado, especialmente à medida que você adiciona mais tipos de alertas ou se precisa fazer alterações em como um alerta específico é tratado. Vamos com um exemplo em Java:

import java.util.ArrayList;
import java.util.List;

// Interface Observable
interface Observable {
void addObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}

// Interface Observer
interface Observer {
void update(String message);
}

// Classe Concreta Observable
class NewsAgency implements Observable {
private List<Observer> observers = new ArrayList<>();
private String news;

@Override
public void addObserver(Observer o) {
observers.add(o);
}

@Override
public void removeObserver(Observer o) {
observers.remove(o);
}

@Override
public void notifyObservers() {
for (Observer o : observers) {
o.update(news);
}
}

public void setNews(String news) {
this.news = news;
notifyObservers();
}
}

// Classe Concreta Observer
class NewsChannel implements Observer {
private String news;

@Override
public void update(String news) {
this.news = news;
System.out.println("News Received: " + news);
}
}

// Uso:
NewsAgency agency = new NewsAgency();
NewsChannel channel = new NewsChannel();

agency.addObserver(channel);
agency.setNews("New news item!");

Em Elixir, você pode implementar o padrão Observable usando processos e mensagens:

defmodule Observable do
use GenServer

def start_link(_), do: GenServer.start_link(__MODULE__, [])

def add_observer(pid, observer_pid) do
GenServer.cast(pid, {:add_observer, observer_pid})
end

def notify(pid, message) do
GenServer.cast(pid, {:notify, message})
end

def init([]), do: {:ok, []}

def handle_cast({:add_observer, observer_pid}, observers) do
{:noreply, [observer_pid | observers]}
end

def handle_cast({:notify, message}, observers) do
Enum.each(observers, fn observer -> send(observer, {:update, message}) end)
{:noreply, observers}
end
end

defmodule Observer do
def receive_updates do
receive do
{:update, message} -> IO.puts("Received update: #{message}")
end
end
end

#Uso:
{:ok, agency_pid} = Observable.start_link([])
observer_pid = spawn(fn -> Observer.receive_updates() end)

Observable.add_observer(agency_pid, observer_pid)
Observable.notify(agency_pid, "New news item!")

No exemplo Elixir, usamos GenServer para o observable (NewsAgency) e um processo simples para o observer (NewsChannel). O observable armazena uma lista de observadores e notifica-os enviando mensagens sempre que há uma atualização. O observador simplesmente escuta e imprime as atualizações recebidas.

Como os padrões de design melhoram a qualidade do código

Os padrões de design trazem uma série de benefícios para a qualidade do código e, consequentemente, para a manutenibilidade e expansão de nossos projetos. Aqui estão alguns dos aspectos mais significativos:

Reusabilidade do código

Ajudam a identificar partes comuns em diferentes partes do código e a abstrair essas partes comuns para criar componentes reutilizáveis. Isso reduz a duplicação de código e facilita a manutenção, pois uma alteração em um componente reutilizável será propagada para todas as suas instâncias.

Favorecem a modularidade

Promovem a modularidade ao dividir o software em partes menores e bem definidas. Isso facilita o desenvolvimento, o teste e a manutenção de cada módulo individualmente.

Melhoram a comunicação entre os desenvolvedores

Como os padrões de design são amplamente conhecidos na indústria de software, eles oferecem uma linguagem comum para os desenvolvedores discutirem a arquitetura do software. Quando um desenvolvedor diz “Vamos usar o padrão Strategy aqui”, todos os outros desenvolvedores imediatamente entendem o que isso implica.

Tornam o código mais flexível

Tornam o código mais flexível, facilitando a adição de novas funcionalidades ou a alteração de funcionalidades existentes. Isso se deve ao fato de que os padrões de design promovem a dependência em abstrações, não em implementações concretas.

Facilitam o entendimento do código

Ajudam a organizar o código de uma forma que é fácil de entender e navegar. Quando os desenvolvedores veem um padrão de design que reconhecem, eles podem entender rapidamente a estrutura e o propósito do código.

Desmistificando os mitos sobre os padrões de design

Preparado para derrubar alguns mitos? Aqui vão três dos mais comuns sobre padrões de design. Alguns são tão persistentes quanto uma mala sem alça, mas vamos enfrentá-los de uma vez por todas.

Padrões de design tornam o código complicado: É um mito comum que os padrões de design complicam desnecessariamente o código. No entanto, quando aplicados corretamente, eles podem simplificar a arquitetura do software, tornando o código mais fácil de entender e manter.

Os padrões de design são apenas para grandes projetos: Não importa o tamanho do seu projeto, os padrões de design podem ser úteis. Eles podem ajudar a resolver problemas comuns de programação e melhorar a estrutura do seu código, seja em um pequeno script ou em uma grande aplicação.

Os padrões de design limitam a criatividade: Alguns podem pensar que o uso de padrões de design limita a criatividade, mas na verdade, eles fornecem uma estrutura que pode facilitar o processo de design. Assim como os músicos aprendem escalas musicais para improvisar mais eficazmente, os desenvolvedores podem usar padrões de design como base para soluções mais criativas.

Como começar a aplicar os padrões de design na sua programação

Quer mergulhar de cabeça nos padrões de design, mas não sabe por onde começar? Não se preocupe, não é como entrar na piscina sem saber nadar. Aqui estão algumas dicas que vão ajudar você a flutuar nesse novo universo.

Estude os padrões: Comece aprendendo os padrões de design mais comuns, como Singleton, Factory, Strategy e Observer. Entenda para que servem e em que situações podem ser úteis.

Análise de código existente: Tente identificar padrões de design em código aberto ou no código que você trabalha no dia a dia. Isso vai ajudar a consolidar seu entendimento.

Pratique: Comece a aplicar padrões de design em seus projetos. Não force o uso de um padrão, mas quando perceber que um padrão pode resolver um problema de design que você está enfrentando, não hesite em usá-lo.

Reflita e Aprenda com os erros: À medida que ganhar experiência, você começará a entender melhor quando e onde aplicar cada padrão. Aprender com os erros é parte do processo. Se um padrão não funcionou como esperado, tente entender por quê e como você pode fazer melhor da próxima vez.

Os padrões de design são uma parte valiosa da caixa de ferramentas de qualquer programador. Com estudo e prática, eles podem ajudar você a escrever código de maior qualidade que é mais fácil de manter e estender.

E lá vamos nós, no final da nossa jornada pelos misteriosos e fascinantes labirintos dos padrões de design. Mas, espere! Não precisa se desesperar, prometemos que não vai ser tão difícil quanto encontrar a saída de um labirinto de espelhos após algumas rodadas de café expresso.

Então, como aplicamos tudo isso aqui na Ateliware? Bom, você pode ter certeza de que não estamos apenas arremessando palavras e padrões ao vento. Fazemos questão de adotar e aplicar os princípios de design em nossos projetos - e não apenas porque eles têm nomes legais como ‘Singleton’ ou ‘Factory’. Eles são parte integrante da nossa caixa de ferramentas de desenvolvimento, ajudando-nos a construir software robusto, escalável e - ousamos dizer - belo.

Na Ateliware, consideramos os padrões de design como receitas testadas e comprovadas, um pouco como as antigas receitas de família que você guarda no fundo de uma gaveta. Elas nos ajudam a evitar reinventar a roda, permitindo que nos concentremos em criar soluções inovadoras para nossos clientes.

E assim como em qualquer cozinha, o segredo não está apenas em seguir as receitas, mas em saber quando e como usar cada ingrediente. Da mesma forma, o segredo para aproveitar ao máximo os padrões de design está em entender como, quando e por que aplicá-los.

Portanto, quer você esteja começando sua jornada como desenvolvedor ou seja um veterano experimentado, esperamos que este artigo tenha ajudado a iluminar o caminho para um código melhor e mais fácil de manter. E lembre-se, na programação, assim como na vida, a prática leva à perfeição!

Então, vamos colocar a mão na massa e… Ah, quase esqueci. No final das contas, o importante é se divertir codificando! Porque, no fundo, programar é um pouco como montar um grande e divertido quebra-cabeça, não é mesmo?

Referências:

Gamma, Erich, et al. Design patterns: elements of reusable object-oriented software. Pearson Deutschland GmbH, 1995.

Fowler, Martin. Patterns of Enterprise Application Architecture: Pattern Enterpr Applica Arch. Addison-Wesley, 2012.

Freeman, Eric, et al. Head First Design Patterns: A Brain-Friendly Guide. " O’Reilly Media, Inc.", 2004.

Rohnert, Hans. “{Pattern-Oriented} Software Architecture.” 2nd USENIX Conference on Object-Oriented Technologies (COOTS 96). 1996.

Hohpe, Gregor, and Bobby Woolf. Enterprise integration patterns: Designing, building, and deploying messaging solutions. Addison-Wesley Professional, 2004.

Kerievsky, Joshua. Refactoring to patterns. Pearson Deutschland GmbH, 2005.

Guilherme Ferreira
Software Engineer formado em Ciência da Computação. Tutor de cursos e desenvolvedor de software. Curte explorar novas tecnologias e estudar inteligência artificial.