Blog do Aguiar

Padrões Criacionais do GoF: Uma Abordagem Essencial para o Desenvolvimento de Software

Os padrões de design criacionais são parte integrante do catálogo de padrões do GoF (Gang of Four), um grupo de autores que popularizou esses conceitos no clássico livro “Design Patterns: Elements of Reusable Object-Oriented Software”. Esses padrões abordam problemas comuns relacionados à criação de objetos em sistemas orientados a objetos, promovendo flexibilidade e reutilização do código. Neste artigo, exploraremos os padrões criacionais e suas características principais.

O que são padrões criacionais?

Padrões criacionais fornecem soluções para problemas recorrentes no processo de criação de objetos. Eles ajudam a abstrair o processo de instanciar classes, separando a lógica de criação do código principal. Essa abordagem facilita a manutenção, promove o desacoplamento e torna os sistemas mais escaláveis.

Principais Padrões Criacionais do GoF

  1. Factory Method
    Problema: Como criar objetos sem especificar a classe exata a ser instanciada?
    Solução: Define um método abstrato em uma classe base, permitindo que subclasses decidam qual objeto concreto será criado.
    • Vantagens: Flexibilidade e substituição fácil de implementações concretas.
    • Exemplo: Um sistema de notificações que decide dinamicamente se envia SMS, e-mail ou push notification
    • abstract class Notification {
          abstract void notifyUser();
      }
      
      class EmailNotification extends Notification {
          void notifyUser() {
              System.out.println("Enviando e-mail...");
          }
      }
      
      class NotificationFactory {
          public static Notification createNotification(String type) {
              if (type.equals("email")) {
                  return new EmailNotification();
              }
              // Adicione outras notificações conforme necessário
              return null;
          }
      }
      
      
  2. Abstract Factory
    Problema: Como criar famílias de objetos relacionados sem depender de suas implementações concretas?
    Solução: Fornece uma interface para criar famílias de objetos relacionados, mas sem especificar suas classes concretas.
    • Vantagens: Garante a consistência entre os objetos criados.
    • Exemplo: Criar componentes gráficos para diferentes sistemas operacionais (Windows, macOS, Linux).
    • // Abstract Product A
      interface Button {
          void render();
      }
      
      // Concrete Product A1
      class WindowsButton implements Button {
          public void render() {
              System.out.println("Rendering a Windows Button.");
          }
      }
      
      // Concrete Product A2
      class MacOSButton implements Button {
          public void render() {
              System.out.println("Rendering a MacOS Button.");
          }
      }
      
      // Abstract Product B
      interface Checkbox {
          void render();
      }
      
      // Concrete Product B1
      class WindowsCheckbox implements Checkbox {
          public void render() {
              System.out.println("Rendering a Windows Checkbox.");
          }
      }
      
      // Concrete Product B2
      class MacOSCheckbox implements Checkbox {
          public void render() {
              System.out.println("Rendering a MacOS Checkbox.");
          }
      }
      
      // Abstract Factory
      interface GUIFactory {
          Button createButton();
          Checkbox createCheckbox();
      }
      
      // Concrete Factory 1
      class WindowsFactory implements GUIFactory {
          public Button createButton() {
              return new WindowsButton();
          }
      
          public Checkbox createCheckbox() {
              return new WindowsCheckbox();
          }
      }
      
      // Concrete Factory 2
      class MacOSFactory implements GUIFactory {
          public Button createButton() {
              return new MacOSButton();
          }
      
          public Checkbox createCheckbox() {
              return new MacOSCheckbox();
          }
      }
      
      // Client
      class Application {
          private Button button;
          private Checkbox checkbox;
      
          public Application(GUIFactory factory) {
              button = factory.createButton();
              checkbox = factory.createCheckbox();
          }
      
          public void render() {
              button.render();
              checkbox.render();
          }
      }
      
      // Main
      public class AbstractFactoryExample {
          public static void main(String[] args) {
              GUIFactory factory = new WindowsFactory();
              Application app = new Application(factory);
              app.render();
      
              factory = new MacOSFactory();
              app = new Application(factory);
              app.render();
          }
      }
      
      
  3. Singleton
    Problema: Como garantir que uma classe tenha apenas uma instância global acessível em todo o sistema?
    Solução: Força a criação de uma única instância e fornece um ponto global de acesso a ela.
      • Vantagens: Ideal para gerenciar recursos compartilhados, como conexões de banco de dados.
      • Cuidado: Pode ser considerado um anti-padrão se mal utilizado, pois aumenta o acoplamento.
      • class DatabaseConnection {
            private static DatabaseConnection instance;
        
            private DatabaseConnection() { }
        
            public static DatabaseConnection getInstance() {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
                return instance;
            }
        }
        
        
  4. Builder
    Problema: Como criar objetos complexos com muitos parâmetros opcionais ou configurações?
    Solução: Separa a construção de um objeto da sua representação, permitindo criar diferentes configurações passo a passo.
    • Vantagens: Torna o código mais legível e modular.
    • Exemplo: Construção de um pedido de fast food com opções como hambúrguer, refrigerante e sobremesa.
    • // Product
      class Computer {
          private String CPU;
          private String GPU;
          private int RAM;
          private int storage;
      
          // Setters
          public void setCPU(String CPU) { this.CPU = CPU; }
          public void setGPU(String GPU) { this.GPU = GPU; }
          public void setRAM(int RAM) { this.RAM = RAM; }
          public void setStorage(int storage) { this.storage = storage; }
      
          @Override
          public String toString() {
              return "Computer [CPU=" + CPU + ", GPU=" + GPU + ", RAM=" + RAM + "GB, Storage=" + storage + "GB]";
          }
      }
      
      // Abstract Builder
      interface ComputerBuilder {
          void buildCPU();
          void buildGPU();
          void buildRAM();
          void buildStorage();
          Computer getComputer();
      }
      
      // Concrete Builder
      class GamingComputerBuilder implements ComputerBuilder {
          private Computer computer;
      
          public GamingComputerBuilder() {
              this.computer = new Computer();
          }
      
          public void buildCPU() {
              computer.setCPU("Intel i9");
          }
      
          public void buildGPU() {
              computer.setGPU("NVIDIA RTX 3080");
          }
      
          public void buildRAM() {
              computer.setRAM(32);
          }
      
          public void buildStorage() {
              computer.setStorage(2000);
          }
      
          public Computer getComputer() {
              return computer;
          }
      }
      
      // Concrete Builder 2
      class OfficeComputerBuilder implements ComputerBuilder {
          private Computer computer;
      
          public OfficeComputerBuilder() {
              this.computer = new Computer();
          }
      
          public void buildCPU() {
              computer.setCPU("Intel i5");
          }
      
          public void buildGPU() {
              computer.setGPU("Integrated Graphics");
          }
      
          public void buildRAM() {
              computer.setRAM(16);
          }
      
          public void buildStorage() {
              computer.setStorage(500);
          }
      
          public Computer getComputer() {
              return computer;
          }
      }
      
      // Director
      class ComputerDirector {
          private ComputerBuilder builder;
      
          public ComputerDirector(ComputerBuilder builder) {
              this.builder = builder;
          }
      
          public Computer constructComputer() {
              builder.buildCPU();
              builder.buildGPU();
              builder.buildRAM();
              builder.buildStorage();
              return builder.getComputer();
          }
      }
      
      // Main
      public class BuilderExample {
          public static void main(String[] args) {
              ComputerBuilder gamingBuilder = new GamingComputerBuilder();
              ComputerDirector director = new ComputerDirector(gamingBuilder);
              Computer gamingComputer = director.constructComputer();
              System.out.println("Gaming Computer: " + gamingComputer);
      
              ComputerBuilder officeBuilder = new OfficeComputerBuilder();
              director = new ComputerDirector(officeBuilder);
              Computer officeComputer = director.constructComputer();
              System.out.println("Office Computer: " + officeComputer);
          }
      }
      
      
      
  5. Prototype
    Problema: Como criar cópias de objetos existentes sem depender diretamente de suas classes?
    Solução: Cria novos objetos clonando instâncias já existentes.
    • Vantagens: Útil quando a criação de objetos é custosa ou complexa.
    • Exemplo: Sistema de gráficos que duplica formas geométricas (como círculos e retângulos) ao invés de recriá-las do zero.
    • // Prototype Interface
      interface Shape extends Cloneable {
          Shape clone();
          void render();
      }
      
      // Concrete Prototype 1
      class Circle implements Shape {
          private int radius;
      
          public Circle(int radius) {
              this.radius = radius;
          }
      
          public Circle(Circle source) {
              this.radius = source.radius;
          }
      
          @Override
          public Circle clone() {
              return new Circle(this);
          }
      
          @Override
          public void render() {
              System.out.println("Rendering a Circle with radius: " + radius);
          }
      }
      
      // Concrete Prototype 2
      class Rectangle implements Shape {
          private int width;
          private int height;
      
          public Rectangle(int width, int height) {
              this.width = width;
              this.height = height;
          }
      
          public Rectangle(Rectangle source) {
              this.width = source.width;
              this.height = source.height;
          }
      
          @Override
          public Rectangle clone() {
              return new Rectangle(this);
          }
      
          @Override
          public void render() {
              System.out.println("Rendering a Rectangle with width: " + width + " and height: " + height);
          }
      }
      
      // Client
      public class PrototypeExample {
          public static void main(String[] args) {
              Circle circle1 = new Circle(10);
              Circle circle2 = circle1.clone();
      
              Rectangle rectangle1 = new Rectangle(20, 15);
              Rectangle rectangle2 = rectangle1.clone();
      
              circle1.render();
              circle2.render();
              rectangle1.render();
              rectangle2.render();
          }
      }
      
      

Quando usar os padrões criacionais?

Esses padrões são úteis quando:

Conclusão

Os padrões criacionais do GoF são poderosos aliados no desenvolvimento de software, ajudando a criar sistemas mais limpos e organizados. Cada padrão resolve um problema específico relacionado à criação de objetos, permitindo que desenvolvedores escolham a melhor abordagem para cada cenário. Entender e aplicar esses padrões é uma habilidade essencial para criar código robusto e escalável.

Share this content:

Sair da versão mobile