Arquitetura Limpa em Go – Parte 4 – Padrões de Projeto

Entenda como a intersecção entre Padrões de Projeto e Arquitetura Limpa em Go podem tornar seu desenvolvimento mais eficiente e organizado.

Pessoal, neste post da série, vamos falar um pouco sobre Padrões de Projeto dentro da Arquitetura Limpa. Esses padrões podem se tornar verdadeiros superpoderes no nosso arsenal de desenvolvimento, quando bem aplicados.

A Magia dos Padrões de Projeto

Os Padrões de Projeto são mais do que simples soluções; eles são uma linguagem comum que nos permite discutir, descrever e solucionar problemas de design de software de maneira eficaz. Quando aplicados corretamente, eles transformam o caos em ordem, especialmente em uma Arquitetura Limpa.

Explorando Padrões de Projeto em Go

Vamos falar de padrões e como eles se encaixam na Arquitetura Limpa com Go:

1. Factory Method

Imagine que você tem várias formas de criar um objeto em seu sistema. O padrão Factory Method nos ajuda a encapsular essa lógica de criação, tornando o processo mais flexível e desacoplado.

type UserFactory struct {
    // ... campos e métodos
}

func (f *UserFactory) CreateUser() *User {
    // ... lógica para criar um usuário
}

2. Strategy

O padrão Strategy é excelente para situações onde você tem múltiplas formas de executar uma operação. Ele permite que você alterne entre diferentes algoritmos sem alterar o cliente que os utiliza.

type SortStrategy interface {
    Sort([]int) []int
}

type QuickSort struct{}

func (s *QuickSort) Sort(array []int) []int {
    // ... implementação do quicksort
}

// Contexto de uso
sorter := QuickSort{}
result := sorter.Sort(minhaArray)

3. Observer

Este padrão é perfeito para quando você tem situações onde mudanças em um objeto precisam ser notificadas a vários outros objetos. Em Go, isso pode ser implementado de forma elegante usando canais e goroutines.

package main

import (
    "fmt"
    "sync"
    "time"
)

// Observer interface
type Observer interface {
    Update(data string)
}

// ConcreteObserver que implementa a interface Observer.
type ConcreteObserver struct {
    ID   int
    Data chan string
}

func NewConcreteObserver(id int) *ConcreteObserver {
    return &ConcreteObserver{
        ID:   id,
        Data: make(chan string),
    }
}

func (co *ConcreteObserver) Update(data string) {
    co.Data <- data
}

// Função que o ConcreteObserver irá rodar para processar dados recebidos.
func (co *ConcreteObserver) Listen() {
    for data := range co.Data {
        fmt.Printf("Observer %d received data: %s\n", co.ID, data)
    }
}

// Publisher estrutura
type Publisher struct {
    observers []Observer
    mu        sync.Mutex // Para sincronizar o registro/desregistro de observers
}

func NewPublisher() *Publisher {
    return &Publisher{}
}

func (p *Publisher) Register(o Observer) {
    p.mu.Lock()
    defer p.mu.Unlock()
    p.observers = append(p.observers, o)
}

func (p *Publisher) Unregister(o Observer) {
    p.mu.Lock()
    defer p.mu.Unlock()
    for i, observer := range p.observers {
        if observer == o {
            p.observers = append(p.observers[:i], p.observers[i+1:]...)
            break
        }
    }
}

func (p *Publisher) Notify(data string) {
    p.mu.Lock()
    defer p.mu.Unlock()
    for _, observer := range p.observers {
        go observer.Update(data)
    }
}

func main() {
    pub := NewPublisher()

    observer1 := NewConcreteObserver(1)
    observer2 := NewConcreteObserver(2)

    pub.Register(observer1)
    pub.Register(observer2)

    go observer1.Listen()
    go observer2.Listen()

    pub.Notify("Hello, World!")

    // Aguarde um pouco para observar a saída antes de fechar os canais
    time.Sleep(1 * time.Second)
    close(observer1.Data)
    close(observer2.Data)
}

Neste exemplo, ConcreteObserver é um tipo que implementa a interface Observer, recebendo dados através de um canal. Quando o método Notify do Publisher é chamado, ele envia uma mensagem para todos os observers registrados de forma assíncrona, utilizando goroutines. Cada ConcreteObserver executa a função Listen, aguardando dados no seu canal Data e imprimindo-os na saída padrão quando recebidos.

Cabr notar que este exemplo simplificado serve para demonstrar o uso de canais e goroutines em um padrão Observer em Go. Em um caso de uso real, você deve considerar aspectos adicionais, como tratamento de erros e fechamento apropriado de canais para evitar memory leaks ou panics.

4. Singleton

O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Em Go, isso pode ser útil para gerenciar recursos como conexões de banco de dados.

type Database struct {
    // ...
}

var instance *Database

func GetDatabaseInstance() *Database {
    if instance == nil {
        instance = &Database{ /*...*/ }
    }
    return instance
}

5. Decorator

O Decorator permite adicionar novas funcionalidades a um objeto dinamicamente. Em Go, isso pode ser feito através de composição e interfaces, sem necessidade de subclasses.

type Coffee interface {
    Cost() float64
}

type Espresso struct{}

func (e *Espresso) Cost() float64 {
    return 1.99
}

type WithMilk struct {
    Coffee Coffee
}

func (m *WithMilk) Cost() float64 {
    return m.Coffee.Cost() + 0.50
}

6. Command

O padrão Command transforma uma solicitação em um objeto independente, permitindo parametrizar clientes com diferentes solicitações. É particularmente útil em Go para desacoplar o código que invoca a operação do código que executa a operação.

type Command interface {
    Execute()
}

type StartServerCommand struct {
    Server *Server
}

func (c *StartServerCommand) Execute() {
    c.Server.Start()
}

7. Adapter

O Adapter permite que interfaces incompatíveis trabalhem em conjunto. Isso é especialmente útil em Go para integrar sistemas que não foram projetados para trabalhar juntos.

type OldPrinter interface {
    Print(s string)
}

type NewPrinter interface {
    PrintFormatted(s string)
}

type PrinterAdapter struct {
    OldPrinter OldPrinter
}

func (p *PrinterAdapter) PrintFormatted(s string) {
    p.OldPrinter.Print(s)
}

8. Facade

O Facade fornece uma interface simplificada para um conjunto complexo de interfaces. Em Go, isso pode ajudar a criar uma interface mais limpa e simples para sistemas complexos.

type ComputerFacade struct {
    Processor *Processor
    Memory    *Memory
    HardDrive *HardDrive
}

func (f *ComputerFacade) Start() {
    f.Processor.Boot()
    f.Memory.Load(BOOT_ADDRESS, f.HardDrive.Read(BOOT_SECTOR))
    // ...
}

Na Arquitetura Limpa, esses padrões nos ajudam a organizar o código de uma forma que respeite a separação de interesses e a independência das camadas. Cada padrão tem seu lugar, contribuindo para um sistema mais coeso, flexível e testável.

Conclusão

Padrões de Projeto não são apenas técnicas; eles são uma forma de arte que enriquece o nosso trabalho. Ao aplicá-los, criamos sistemas robustos, elegantes e escaláveis.

No próximo post da série, vou falar sobre testes em uma Arquitetura Limpa. Em Go, claro.

Let’s code!

1 comment
Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post Anterior

Arquitetura Limpa em Go – Parte 3 – Refatorando

Próximo Post

Arquitetura Limpa em Go – Parte 5 – Testes

Posts Relacionados