Aula 18: Introdução a Design Patterns

Soluções comprovadas para desafios de design de software.

O Que Você Vai Aprender

Nesta aula, você será introduzido ao mundo dos Design Patterns (Padrões de Projeto). Você entenderá o que são e por que são importantes, além de explorar dois padrões clássicos: o Factory Method e o Singleton, com exemplos práticos e um simulador interativo.

💡 Conceito de Padrões de Projeto

Compreender o que são, sua importância e como se aplicam ao design de software.

🏭 Factory Method

Aprender a criar objetos sem especificar a classe exata, promovendo flexibilidade.

☝️ Singleton

Entender como garantir que uma classe tenha apenas uma única instância em todo o sistema.

🛠️ Aplicações Práticas

Ver exemplos de código e simulações para solidificar o aprendizado.

Conceito: Padrões de Projeto (Design Patterns)

Padrões de Projeto são soluções elegantes, reutilizáveis e testadas para problemas comuns que surgem durante o processo de design de software em Programação Orientada a Objetos. Eles não são classes ou bibliotecas prontas, mas sim "receitas" ou "plantilhas" que descrevem como resolver um problema específico de design em um determinado contexto.

Por que Usar Padrões de Projeto?

  • Reutilização: Evitam reinventar a roda, oferecendo soluções comprovadas.
  • Comunicação: Fornecem um vocabulário comum entre desenvolvedores.
  • Manutenibilidade: Levam a sistemas mais flexíveis e fáceis de manter.
  • Modularidade: Ajudam a criar sistemas mais desacoplados.
Image of Design Patterns Concept

Factory Method: Fábricas de Objetos

O padrão Factory Method (Método de Fábrica) é um padrão de criação que oferece uma interface para criar objetos em uma superclasse, mas permite que subclasses alterem o tipo de objetos que serão criados.

Propósito:

Ele remove a responsabilidade de instanciar objetos diretamente do código cliente, delegando-a a um método de fábrica. Isso torna o sistema mais flexível, pois o código cliente não precisa saber a classe exata do objeto que está sendo criado.

# Exemplo de Factory Method em Python

# Classes de Produtos
class Carro:
    def __init__(self, modelo):
        self.modelo = modelo
    def __str__(self):
        return f"Carro: {self.modelo}"

class Moto:
    def __init__(self, tipo):
        self.tipo = tipo
    def __str__(self):
        return f"Moto: {self.tipo}"

# Classe Fábrica Abstrata (ou Interface)
class VeiculoFactory:
    def criar_veiculo(self, tipo_veiculo):
        raise NotImplementedError("Este método deve ser implementado pelas subclasses")

# Fábricas Concretas
class CarroFactory(VeiculoFactory):
    def criar_veiculo(self, modelo):
        return Carro(modelo)

class MotoFactory(VeiculoFactory):
    def criar_veiculo(self, tipo):
        return Moto(tipo)

# Uso:
# carro_fabrica = CarroFactory()
# meu_carro = carro_fabrica.criar_veiculo("Sedan")
# print(meu_carro) # Saída: Carro: Sedan

# moto_fabrica = MotoFactory()
# minha_moto = moto_fabrica.criar_veiculo("Esportiva")
# print(minha_moto) # Saída: Moto: Esportiva

Singleton: Uma Única Instância

O padrão Singleton (Instância Única) é um padrão de criação que garante que uma classe tenha apenas uma única instância e fornece um ponto de acesso global a ela.

Propósito:

É útil quando você precisa de apenas um objeto para coordenar ações em todo o sistema, como um gerenciador de logs, uma configuração de aplicativo ou um pool de conexões de banco de dados.

# Exemplo de Singleton em Python

class Configuracao:
    _instance = None # Armazena a única instância

    def __new__(cls):
        if cls._instance is None:
            # Cria a instância apenas se ela ainda não existir
            cls._instance = super(Configuracao, cls).__new__(cls)
            cls._instance.config = {} # Inicializa a configuração
            print("Configuração inicializada (primeira vez).")
        return cls._instance

    def set_config(self, chave, valor):
        self.config[chave] = valor
        print(f"Configuração '{chave}' definida como '{valor}'.")

    def get_config(self, chave):
        return self.config.get(chave, "Não encontrado")

# Uso:
# conf1 = Configuracao() # Primeira vez, inicializa
# conf1.set_config("tema", "dark")

# conf2 = Configuracao() # Retorna a mesma instância existente
# conf2.set_config("idioma", "pt-br")

# print(f"Configuração tema (via conf1): {conf1.get_config('tema')}")
# print(f"Configuração idioma (via conf2): {conf2.get_config('idioma')}")

# print(f"conf1 é a mesma instância que conf2? {conf1 is conf2}") # Saída: True

Simulador Interativo

Experimente os padrões Factory Method e Singleton com este simulador interativo. As classes JavaScript abaixo imitam o comportamento das classes Python para demonstração.

Código Python das Classes Simuladas

# Classes para Factory Method
class Animal:
    def __init__(self, especie):
        self.especie = especie
    def fazer_som(self):
        return "Som genérico"
    def __str__(self):
        return f"Animal: {self.especie}"

class Cachorro(Animal):
    def __init__(self, nome):
        super().__init__("Cachorro")
        self.nome = nome
    def fazer_som(self):
        return "Au Au!"
    def __str__(self):
        return f"Cachorro: {self.nome}"

class Gato(Animal):
    def __init__(self, nome):
        super().__init__("Gato")
        self.nome = nome
    def fazer_som(self):
        return "Miau!"
    def __str__(self):
        return f"Gato: {self.nome}"

class AnimalFactory:
    @classmethod
    def criar_animal(cls, tipo, nome=None):
        if tipo == "cachorro":
            return Cachorro(nome)
        elif tipo == "gato":
            return Gato(nome)
        else:
            return Animal("Desconhecido")

# Classe para Singleton
class Logger:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Logger, cls).__new__(cls)
            cls._instance.logs = []
            print("Logger inicializado (primeira vez).")
        return cls._instance

    def log(self, mensagem):
        self.logs.append(mensagem)
        print(f"LOG: {mensagem}")

    def mostrar_logs(self):
        return "\n".join(self.logs)

Simulador Factory Method (Criação de Animais)

Use a fábrica para criar diferentes tipos de animais.

Simulador Singleton (Logger)

Observe que o Logger é sempre a mesma instância.

Resultados do simulador aparecerão aqui.

Desafios para Continuar

Coloque seus conhecimentos em prática implementando esses padrões em seus próprios projetos Python.

  • 1. Factory Method para Conexões de Banco de Dados

    Crie uma hierarquia de classes de conexão de banco de dados (ex: `ConexaoSQLServer`, `ConexaoMySQL`, `ConexaoPostgreSQL`), todas implementando um método `conectar()`. Em seguida, crie uma `ConexaoFactory` que, dado um tipo de banco de dados (string como "mysql", "sqlserver"), retorna a instância da conexão apropriada.

    # Exemplo de Estrutura:
    class Conexao:
        def conectar(self):
            raise NotImplementedError
    
    class ConexaoMySQL(Conexao):
        def conectar(self):
            return "Conectado ao MySQL!"
    
    class ConexaoSQLServer(Conexao):
        def conectar(self):
            return "Conectado ao SQL Server!"
    
    class ConexaoFactory:
        def criar_conexao(self, tipo):
            if tipo == "mysql":
                return ConexaoMySQL()
            elif tipo == "sqlserver":
                return ConexaoSQLServer()
            else:
                raise ValueError("Tipo de conexão desconhecido")
    
    # Teste:
    # factory = ConexaoFactory()
    # conn_mysql = factory.criar_conexao("mysql")
    # print(conn_mysql.conectar())
    # conn_sql = factory.criar_conexao("sqlserver")
    # print(conn_sql.conectar())
                                
  • 2. Singleton para um Gerenciador de Configurações

    Implemente uma classe `ConfiguracaoApp` usando o padrão Singleton. Esta classe deve ter métodos para `carregar_configuracoes(caminho_arquivo)` e `obter_valor(chave)`, garantindo que apenas uma instância de `ConfiguracaoApp` possa existir em qualquer momento.

    # Exemplo de Estrutura:
    class ConfiguracaoApp:
        _instance = None
        _config_data = {}
    
        def __new__(cls):
            if cls._instance is None:
                cls._instance = super(ConfiguracaoApp, cls).__new__(cls)
                print("Gerenciador de configurações inicializado.")
            return cls._instance
    
        def carregar_configuracoes(self, arquivo_path):
            # Simulação de carregamento de arquivo
            print(f"Carregando configurações de: {arquivo_path}")
            self._config_data = {"DB_HOST": "localhost", "DB_PORT": 5432, "LOG_LEVEL": "INFO"}
    
        def obter_valor(self, chave):
            return self._config_data.get(chave, None)
    
    # Teste:
    # config1 = ConfiguracaoApp()
    # config1.carregar_configuracoes("app_config.json")
    # print(f"DB Host: {config1.obter_valor('DB_HOST')}")
    
    # config2 = ConfiguracaoApp() # Deveria ser a mesma instância
    # print(f"Log Level: {config2.obter_valor('LOG_LEVEL')}")
    # print(f"config1 é a mesma instância que config2? {config1 is config2}")