Como utilizar o princípio de Open-Closed

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.

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!


Se inscreva na nossa newsletter

* indicates required

Deixe uma resposta