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.
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: criacionais, estruturais e comportamentais. Para trazer essas ideias abstratas à vida, vamos explorar exemplos práticos de código em Java e Elixir.
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 encapsulam o conhecimento coletivo de desenvolvedores experientes, fornecendo uma linguagem comum que facilita a comunicação entre os programadores. Quando um desenvolvedor diz “Vamos usar o padrão Singleton aqui”, todos na equipe entendem o que isso significa em termos de estrutura e comportamento do código.
Os tipos principais de padrões de design
Os três tipos principais de padrões de design têm propósitos distintos e resolvem conjuntos específicos de problemas:
Criacionais: focam principalmente na maneira como os objetos são criados. Exemplos notáveis incluem o Singleton, o Builder e o Factory Method.
Estruturais: lidam com a composição de classes e objetos para formar estruturas maiores. Por exemplo, o padrão Adapter permite que classes com interfaces incompatíveis trabalhem juntas, o Decorator adiciona responsabilidades a um objeto de maneira dinâmica, e o Composite permite tratar um grupo de objetos da mesma maneira que se trataria um único objeto.
Comportamentais: estão preocupados com a comunicação e a distribuição de responsabilidades entre objetos. Exemplos são o Observer, que define uma dependência um-para-muitos entre objetos, e o Strategy, que permite a um objeto alterar o comportamento em tempo de execução.
Exemplos práticos de padrões de design
Vamos explorar quatro padrões icônicos: Singleton (criacional), Factory (criacional), Adapter (estrutural) e Observer (comportamental). Usaremos Java e Elixir para mostrar como esses padrões se manifestam tanto em uma linguagem orientada a objetos quanto em uma funcional.
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. Isso é particularmente útil quando precisamos ter um único ponto de controle, como uma configuração centralizada ou um pool de conexões.
Java:
public class Logger {
private static Logger instance;
private Logger() {}
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.");
Elixir:
Uma abordagem comum no ecossistema Erlang/Elixir é usar GenServer para isso — um comportamento OTP que implementa um servidor genérico em um processo que pode manter estado.
defmodule LoggerSingleton do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def get_instance do
case GenServer.whereis(__MODULE__) do
nil -> {:error, "Logger not started"}
pid -> pid
end
end
def log(message) do
GenServer.call(__MODULE__, {:log, message})
end
def init(:ok), do: {:ok, nil}
def handle_call({:log, message}, _from, state) do
IO.puts("Log: #{message}")
{:reply, :ok, state}
end
end
# Uso
{:ok, _pid} = LoggerSingleton.start_link()
LoggerSingleton.log("This is a log message.")
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. Isso é particularmente útil quando o sistema precisa ser independente de como seus objetos são criados.
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();
Elixir:
Em Elixir, podemos usar módulos, structs e protocolos para criar um padrão semelhante ao Factory.
defprotocol Animal do
@spec make_sound(animal :: t) :: String.t()
def make_sound(animal)
end
defmodule Dog do
defstruct []
defimpl Animal do
def make_sound(_dog), do: "Woof!"
end
end
defmodule Cat do
defstruct []
defimpl Animal do
def make_sound(_cat), do: "Meow!"
end
end
defmodule AnimalFactory do
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))
Padrão Adapter
O padrão Adapter é um padrão estrutural que permite a colaboração de objetos com interfaces incompatíveis. Ele age como um “tradutor” entre eles — como um adaptador universal de tomada quando você viaja para outro país.
Java:
interface OldSystem {
void oldRequest();
}
class OldSystemImplementation implements OldSystem {
public void oldRequest() {
System.out.println("Old request.");
}
}
interface NewSystem {
void newRequest();
}
class SystemAdapter implements NewSystem {
private OldSystem oldSystem;
public SystemAdapter(OldSystem oldSystem) {
this.oldSystem = oldSystem;
}
@Override
public void newRequest() {
oldSystem.oldRequest();
}
}
// Uso
OldSystem oldSys = new OldSystemImplementation();
NewSystem newSys = new SystemAdapter(oldSys);
newSys.newRequest(); // Saída: "Old request."
Elixir:
defmodule OldSystem do
def old_request, do: IO.puts("Old request.")
end
defmodule NewSystem do
def new_request(adapter), do: adapter.request()
end
defmodule SystemAdapter do
def request, do: OldSystem.old_request()
end
# Uso
NewSystem.new_request(SystemAdapter) # Saída: "Old request."
Padrão Observer
O padrão Observer define uma dependência um-para-muitos entre objetos: quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente. Ideal para dashboards, sistemas de alertas ou qualquer cenário em que múltiplos componentes precisam reagir a mudanças.
Java:
import java.util.ArrayList;
import java.util.List;
interface Observable {
void addObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
interface Observer {
void update(String message);
}
class NewsAgency implements Observable {
private List<Observer> observers = new ArrayList<>();
private String news;
public void addObserver(Observer o) { observers.add(o); }
public void removeObserver(Observer o) { observers.remove(o); }
public void notifyObservers() {
for (Observer o : observers) { o.update(news); }
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
class NewsChannel implements Observer {
private String news;
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!");
Elixir:
Em Elixir, podemos implementar o padrão usando processos e mensagens com GenServer.
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!")
Como os padrões de design melhoram a qualidade do código
Os padrões de design trazem benefícios concretos: reusabilidade (abstraem partes comuns e reduzem duplicação), modularidade (dividem o software em partes menores e bem definidas), comunicação entre desenvolvedores (oferecem linguagem comum amplamente conhecida), flexibilidade (favorecem dependência em abstrações) e legibilidade (organizam o código de forma familiar e navegável).
Desmistificando os mitos sobre padrões de design
“Padrões de design tornam o código complicado”: quando aplicados corretamente, eles simplificam a arquitetura do software, tornando o código mais fácil de entender e manter.
“São apenas para grandes projetos”: não importa o tamanho do projeto, os padrões de design podem ser úteis. Eles ajudam a resolver problemas comuns de programação em qualquer escala.
“Os padrões de design limitam a criatividade”: assim como os músicos aprendem escalas musicais para improvisar mais eficazmente, os desenvolvedores usam padrões de design como base para soluções mais criativas.
Como começar a aplicar os padrões de design
- Estude os padrões: comece com os mais comuns — Singleton, Factory, Strategy e Observer. Entenda para que servem e em que situações são úteis.
- Analise código existente: tente identificar padrões em código aberto ou no código que você trabalha no dia a dia.
- Pratique: aplique padrões nos seus projetos. Não force o uso, mas quando perceber que um padrão pode resolver um problema de design, não hesite em usá-lo.
- Reflita e aprenda com os erros: à medida que ganhar experiência, você entenderá melhor quando e onde aplicar cada padrão.
Na Ateliware, consideramos os padrões de design como receitas testadas e comprovadas. Eles nos ajudam a evitar reinventar a roda, permitindo que nos concentremos em criar soluções inovadoras para nossos clientes. O segredo não está apenas em seguir as receitas, mas em saber quando e como usar cada ingrediente.
Referências:
- Gamma, Erich, et al. Design Patterns: Elements of Reusable Object-Oriented Software. Pearson, 1995.
- Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2012.
- Freeman, Eric, et al. Head First Design Patterns. O’Reilly, 2004.



