Ícone do site Tiago Temporin

A importância de interfaces em arquiteturas de camadas

Um código organizado é essencial para manter a facilidade em sua manutenção. Go, com sua simplicidade e eficiência, oferece diversos recursos para garantir uma arquitetura bem estruturada.

Um desses recursos é o uso de interfaces, que desempenham um papel fundamental na separação de responsabilidades e na criação de abstrações entre as camadas da aplicação.

Neste post, exploraremos como as interfaces podem ser aplicadas dentro de uma arquitetura em camadas, promovendo um código desacoplado, testável e flexível.

O que é uma arquitetura em camadas

Uma arquitetura em camadas é um padrão de design de software que divide a aplicação em diferentes camadas, onde cada uma tem uma responsabilidade específica.

A ideia é que cada camada se comunique com as outras de maneira controlada, sem expor detalhes internos. Alguns exemplos dessas camadas incluem:

  1. Camada de domínio: Onde as regras de negócio residem.
  2. Camada de repositório: Responsável por persistência de dados.
  3. Camada de aplicação: Gerencia fluxos de operação, coordena as ações entre as camadas.
  4. Ports e adapters: Faz a comunicação entre o core da aplicação e o mundo externo, seja com o usuário, APIs, ou outros serviços.

Esse padrão de camadas facilita a manutenção, pois cada camada tem uma função específica e bem definida. Além disso, possibilita a implementação de testes unitários mais isolados, já que as dependências entre as camadas podem ser “mockadas”.

O que são interfaces em Go

Em Go, uma interface é um tipo que define um conjunto de métodos, mas não implementa esses métodos diretamente. Quem implementa as interfaces são os tipos que “satisfazem” os requisitos daquela interface. Por exemplo:

type Repository interface {
    Save(data string) error
}

Aqui, qualquer tipo que possua um método Save(data string) error satisfaz essa interface. A flexibilidade das interfaces em Go reside no fato de que elas permitem criar abstrações, onde podemos mudar a implementação real sem precisar modificar o código que usa a interface.

Como utilizá-las para abstrações das camadas

Embora os exemplos abaixo estejam mais ligados ao Domain-Driven Design e Arquitetura Hexagonal, a utilização de interfaces para criar boas abstrações dentro das camadas não se limita somente a essas arquiteturas.

A ideia dessa sessão, é trazer exemplos de como criar camadas com um bom desacoplamento, tornando-a mais flexível e fácil para testar.

Camada de Domínio

Na camada de domínio, as interfaces permitem definir contratos que descrevem as operações de negócio, mas sem se preocupar com detalhes de implementação.

Suponha que tenhamos uma lógica de negócio que precisa salvar um dado, mas sem conhecer o mecanismo de persistência (banco de dados, cache, etc.).

type DomainService struct {
    repo Repository
}

func NewDomainService(repo Repository) *DomainService {
    return &DomainService{repo}
}

func (s *DomainService) ProcessData(data string) error {
    // Regras de negócio aqui...
    return s.repo.Save(data)
}

Aqui, DomainService apenas depende de uma interface Repository, deixando a implementação concreta para outra parte da aplicação.

Camada de Repositório

A camada de repositório contém a implementação concreta da interface usada no domínio. Isso permite, por exemplo, mudar o banco de dados ou até mesmo usar outro mecanismo de persistência (como um arquivo em disco ou um serviço externo), sem alterar a lógica de negócio.

type RepositoryDB struct {
    db *sql.DB
}

func NewRepositoryDB(db *sql.DB) *RepositoryDB {
    return &RepositoryDB{db}
}

func (r *RepositoryDB) Save(data string) error {
    // Código para salvar o dado no banco de dados
    _, err := r.db.Exec("INSERT INTO tabela (dado) VALUES (?)", data)
    return err
}

Como RepositoryDB implementa o método Save, ele pode ser usado pela camada de domínio como uma dependência concreta da interface Repository.

Camada de Ports e Adapters

Ports e Adapters fazem parte da Arquitetura Hexagonal, onde interfaces são usadas para definir os contratos do core da aplicação com o mundo externo. Interfaces desempenham um papel crucial ao isolar a lógica da aplicação dos detalhes de entrada/saída.

Um exemplo seria um port que define como a aplicação recebe dados de uma API externa:

type ExternalAPI interface {
    Find() (string, error)
}

type APIAdapter struct {
    clientHTTP *http.Client
}

func (a *APIAdapter) Find() (string, error) {
    // Implementação para buscar os dados de uma API externa
    resp, err := a.clientHTTP.Get("<https://api.external.com/data>")
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    var data string
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return "", err
    }

    return data, nil
}

A camada de aplicação, por sua vez, pode utilizar essa interface, permitindo flexibilidade no consumo de dados:

type AppService struct {
    api ExternalAPI
}

func (s *AppService) Exec() error {
    data, err := s.api.Find()
    if err != nil {
        return err
    }
    // Lógica de aplicação com os dados buscados
    return nil
}

Conclusão

O uso de interfaces em Go dentro de uma arquitetura em camadas permite desacoplar as diferentes partes da aplicação, facilitando a substituição de implementações e promovendo a testabilidade.

Através dessa abordagem, é possível garantir que as camadas de domínio, repositório e interface estejam claramente separadas e sigam o princípio da responsabilidade única, permitindo evoluir a aplicação de forma mais segura e modular.

Interfaces, quando utilizadas corretamente, tornam o código mais flexível, o que se traduz em uma manutenção simplificada e maior capacidade de adaptação às mudanças de requisitos.

Até a próxima!


Faça parte da comunidade!

Receba os melhores conteúdos sobre Go, Kubernetes, arquitetura de software, Cloud e esteja sempre atualizado com as tendências e práticas do mercado.

* indicates required