Nesse penúltimo post sobre SOLID, vamos falar sobre a letra “O”, ou seja, o princípio Open-Closed. Antes de começar, caso você esteja chegando no blog agora, vou deixar aqui os links para os posts anteriores da série.
- O que é SOLID?
- O que é e como aplicar Dependency Inversion Principle
- O que é e como Interface Segregation é aplicada no Go
- Aplicando Liskov Substitution Principle
Voltando para o assunto desse post, vamos relembrar o que o princípio Open-Closed nos diz.
💡 Uma classe deve estar aberta para extensões, mas fechada para modificações. Ou seja, sempre que for necessário adicionar funcionalidades, devemos estender a classe ao invés de modificá-la.
Ao ler “…estender a classe…” na definição do princípio, provavelmente você pensou. “Vixi, não dá para aplicar em Go”, ou, “Fácil, só usar embedding nas structs”.
Embora o segundo pensamento possa ser a solução em alguns casos, o fato de, em alguns momentos usarmos a relação struct → classe, não quer dizer que ela sempre será utilizada ou válida. Em outras palavras, sempre fazer o paralelo entre structs e classes pode nos levar a alguns erros graves.
Como Go não tem orientação a objetos, aplicar o princípio Open-Closed na linguagem pode ser um pouco diferente.
Sem Open-Closed
No exemplo abaixo, temos uma função que recebe um source, de onde deverá ler uma lista de cidades e retorná-las em forma de um slice de City.
func GetCities(sourceType string, source string) ([]City, error) {
var data []byte
var err error
if sourceType == "file" {
data, err = ioutil.ReadFile(source)
if err != nil {
return nil, err
}
} else if sourceType == "link" {
resp, err := http.Get(source)
if err != nil {
return nil, err
}
data, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
}
var cities []City
err = yaml.Unmarshal(data, &cities)
if err != nil {
return nil, err
}
return cities, nil
}
Bem simples, certo? Porém, o que vai acontecer com essa função caso eu queira adicionar outro source? Vou ter que modificá-la, certo? Isso é uma violação do princípio, mais especificamente da parte que diz “…mas fechada para modificações.”.
Embora o inicio do princípio diga “uma classe”, na verdade ele pode ser aplicado em classes, funções e métodos.
Com Open-Closed
Agora, na função refatorada abaixo, repare como ganhamos extensibilidade sem ter mais que fazer modificações na função em si.
// declara um novo tipo
type DataReader func(source string) ([]byte, error)
// ex bloco de if sourceType == "file"
func ReadFromFile(fileName string) ([]byte, error) {
data, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
return data, nil
}
// ex bloco de if sourceType == "link"
func ReadFromLink(link string) ([]byte, error) {
resp, err := http.Get(link)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return data, nil
}
// Função passa receber a função de leitura como parâmetro
func GetCities(reader DataReader, source string) ([]City, error) {
data, err := reader(source)
if err != nil {
return nil, err
}
var cities []City
err = yaml.Unmarshal(data, &cities)
if err != nil {
return nil, err
}
return cities, nil
}
Como a função GetCities agora recebe um DataReader, ou seja, uma função com a assinatura func(source string) ([]byte, error) como primeiro parâmetro, além das duas funções já existentes, podemos criar novas funções para ler de outros sources.
Em outras palavras, nossa função agora respeita 100% o princípio Open-Closed, pois ela está aberta para extensões e fechada para modificações.
Exemplo do package io
Sempre que possível, gosto de trazer exemplos do próprio código fonte do Go. Felizmente, assim como fizemos no post sobre Interface Segregation, o princípio Open-Closed também pode ser visto no package io, mais especificamente na função ReadAll.
type Reader interface {
Read(p []byte) (n int, err error)
}
func ReadAll(r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
}
}
Nesse exemplo, a função aguarda por uma interface do tipo Reader, ou seja, uma struct que implemente a função Read.
Dessa forma, assim como no nosso primeiro exemplo, ela se mantém extensível sem a necessidade de modificação.
Conclusão
Como podemos ver no primeiro exemplo, aplicar o princípio Open-Closed, além de tornar as funções extensíveis, torna-as mais específicas também, facilitando assim a manutenção e criação de testes unitários.
Seja na forma de um novo tipo que represente a assinatura de uma função ou uma interface, o importante é criar funções extensíveis.
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.
Livros Recomendados
Abaixo listei alguns dos melhores livros que já li sobre arquitetura de software e desenvolvimento.




