Parametrizar aplicações, segregando as responsabilidades [1/2]

Ao desenvolver aplicações devemos pensar em segregar as responsabilidades, colocar cada coisa no melhor local possível. Isso também é verdade para as variáveis e parâmetros de configuração. Trabalhar com projetos onde essas estrutura não são organizadas, faz com que o ambiente se torne caótico, confuso, pouco adaptado, lento e muito cheio de tentativas erros.

Nesse post, eu vou explicar o modelo que acredito se o mais adequado para o trabalho, demonstrar o seu formato e sua padronização.

Alguns pequenos avisos antes:

  • Vou estar usando o Java como linguagem, mas se agarre ao conceito, este é praticável com mais ou menos esforço para qualquer linguagem.
  • Vou tratar aqui o termo variáveis e parâmetros como a mesma coisa.
  • Esse texto vai ser longo, porque tem muitos detalhes a apresentar. Mesmo sendo dividido em partes. Por favor, reserve um tempo.

Como de costume, segue o link do projeto para acompanhar: https://github.com/escovabit-tec-br/spring-boot-variables-place

Tipos de Variáveis

Começamos elencando os tipos de variáveis que temos e detalhando sua utilizações, sendo elas:

  • Variáveis de classe
  • Variáveis de compilação
  • Variáveis de ambiente
  • Variáveis de execução

Variáveis de classe

As variáveis atendem a necessidade de resolver o problema do famoso “magic number“. Esta é a mais simples de explicar. Basicamente é tirar de dentro da logica de negocio as configurações para aquela logica. resumindo:

Externalizar as regras de negócios para variáveis fora do código de negocio.

Um video ótimo sobre o assunto: https://www.youtube.com/shorts/qADaSdE3sqE

Com o nosso exemplo, vamos supor que existe a seguinte demanda:

Fazer uma classe que consiga contar quantos números um texto (String) tem e devolva, “phone” para quando tiver 8, “mobile” para quando tiver mais que 8 e menos ou igual a 9, e “not phone” para quando for diferente. (Por favor, não se apegue ao algoritmo, o objetivo é mostrar os “magic number”.)

package br.tec.escovabit.app.variablesplace.classes;

public class VariableClassesWrong {

    public String phoneType(String phoneNumber) {
        String response = "not phone";
        if (phoneNumber.length() == 8) {
            response = "phone";
        } else if (phoneNumber.length() > 8 && phoneNumber.length() <= 9) {
            response = "mobile";
        }
        return response;
    }
}

Neste primeiro exemplo, nos temos 6 lugares com “magic number”, sendo eles:

  • As 3 Strings
  • As 3 utilização dos números inteiros (8 e 9).

Como seria a forma “correta” (a forma que resolve os “magic number”)

package br.tec.escovabit.app.variablesplace.classes;

public class VariableClassesCorrect {

    public static Integer PHONE_LENGTH = 8;
    public static Integer MOBILE_LENGTH = 9;
    public static String PHONE = "phone";
    public static String MOBILE = "mobile";
    public static String NOT_PHONE = "not phone";

    public String phoneType(String phoneNumber) {
        String response = NOT_PHONE;
        if (phoneNumber.length() == PHONE_LENGTH) {
            response = PHONE;
        } else if (phoneNumber.length() > PHONE_LENGTH && phoneNumber.length() <= MOBILE_LENGTH) {
            response = MOBILE;
        }
        return response;
    }
}

O que mudou? Desta forma, todos os valores que são “fixados” estão agora em variáveis da classe, qualquer lugar que precisar desta informação, basta busca em um só lugar.

Para esse cenário, de uma classe que só tem 1 método, com uma logica de negocio tão simples, essa estrutura possa parece um “canhão para matar formiga”. Mas esta é a base para todo o conceito.

Um exemplo básico dessa reutilização sem sair da camada de variáveis de classe, é a utilização nos teste unitários, onde você já acessa o mesmo valor definido sem precisar “escrever” novamente:

package br.tec.escovabit.app.variablesplace.classes;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class VariableClassesCorrectTest {
    VariableClassesCorrect variableClassesCorrect = new VariableClassesCorrect();

    @Test
    @DisplayName("phone")
    public void testPhone() {
        assertEquals(
                VariableClassesCorrect.PHONE,
                variableClassesCorrect.phoneType(String.valueOf("12345678")));

    }

    @Test
    @DisplayName("mobile")
    public void testMobile() {
        assertEquals(
                VariableClassesCorrect.MOBILE,
                variableClassesCorrect.phoneType(String.valueOf("123456789")));

    }

    @Test
    @DisplayName("not phone")
    public void testNotPhone() {
        assertEquals(
                VariableClassesCorrect.NOT_PHONE,
                variableClassesCorrect.phoneType(String.valueOf("1234567890")));

    }
}

Porque se algum dia, alguém falar que no lugar de “mobile” precisa devolver “cellphone”. A troca fica em um só lugar e não quebra todo o sistema.

Variáveis de compilação

Já neste tipo, vamos precisar fazer um entendimento do conceito por traz desse tipo de variável, para conseguir interpretar como este conceito é aplicada em cada linguagem de programação. Podemos definir o conceito como:

São as variáveis que ficam próximas ao código, mas não estão “dentro do código”.

Isso por que, nem todas as linguagem de programação realmente tem uma “compilação” de código. Linguagem como C/C++ realmente transforma seu código fonte em algo “totalmente maquina”, e desta forma o valor colocado na variável de compilação durante o processo de compilação cria uma variável de classe. Basicamente, seria como se na classe tivesse um “insira o seu valor aqui”.

Mas para outras linguagens, o conceito também existe, só não tem o mesmo “código compilado final”.

Exemplo, no Javascript com Angular você tem um processo de compilação (build), que cria código Javascript fazendo o trabalho de trocar os parâmetros de compilação para variáveis de classe.

No Java, você tem o conceito dos arquivos de recursos (src/main/resources) que ficam literalmente juntos com o código fonte da aplicação e são anexados juntos ao artefato criado no processo de compilação. Este seria a implementação do conceito de variáveis de compilação pelo Java.

(Uma escovada de bit – Para o Java, como não se transforma diretamente código fonte (file.java) em código de maquina (bytecode), mas transforma em “java” código (file.class / java bytecode), não se aplica efetivamente o processo de compilação. leitura sobre a JVM)

Usando o Spring Boot para facilitar a leitura das variáveis (informações de como fazer no braço), o código fica assim:

package br.tec.escovabit.app.variablesplace.build;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class VariableBuildCorrect {

    /**
     * procura o valor em app.config.phone.phone.length dentro do
     * src/main/resources/bootstrap.yml
     * ou
     * usa o valor 8
     */

    @Value("${app.config.phone.phone.length:8}")
    private Integer phoneLength;

    /**
     * procura o valor em app.config.phone.mobile.length dentro do
     * src/main/resources/bootstrap.yml
     * ou
     * usa o valor 9
     */

    @Value("${app.config.phone.mobile.length:9}")
    private Integer mobileLength;

    /**
     * procura o valor em app.config.phone.phone.msg dentro do
     * src/main/resources/bootstrap.yml
     * ou
     * usa o valor phone
     */

    @Value("${app.config.phone.phone.msg:phone}")
    private String phone;

    /**
     * procura o valor em app.config.phone.mobile.msg dentro do
     * src/main/resources/bootstrap.yml
     * ou
     * usa o valor mobile
     */

    @Value("${app.config.phone.mobile.msg:mobile}")
    private String mobile;

    /**
     * Sem valor padrão para forçar vir de bootstrap.yml
     */

    @Value("${app.config.phone.notPhone.msg}")
    private String notPhone;

    public String phoneType(String phoneNumber) {
        String response = notPhone;
        if (phoneNumber.length() == phoneLength) {
            response = phone;
        } else if (phoneNumber.length() > phoneLength && phoneNumber.length() <= mobileLength) {
            response = mobile;
        }
        return response;
    }
}

Onde usamos a anotação @Value para recuperar o valor. Para este cenário é uma boa pratica atribuir valores padrões. Pois é um critério de “sobrescrever” o valor caso seja necessário.

Já o arquivo bootstrap.yml que esta no diretório src/main/resources fica com a configuração:

app:
  config:
    phone:
      notPhone:
        msg: 'notPhone'

Para finalizar o assunto. Sempre que for usar variáveis de compilação, você precisa analisar e pensar se ela deve ou não ter um valor padrão atribuído. Na grande maioria dos casos, o valor padrão é a melhor das opções.

E quando não usar? Quando se quer efetivamente receber um erro por não ter essa variável com valor. Exemplos:

  • usuários e senhas
  • personalizações para clientes (Multitenancy)
  • parâmetros que podem afetar custos financeiros

Variáveis de ambiente

Agora vamos entrar na que eu considero a mais problemática das variáveis, pois é a que é mais usada de forma errada. Primeiro vamos entendê-la.

Variáveis de ambientes, são variáveis fornecidas a aplicação de acordo com o ambiente em que ela se encontra.

Ref: https://en.wikipedia.org/wiki/Environment_variable

Exemplos, baseados na wikipedia:

  • Qual é a home do usuário: $HOME
  • Qual é a linguagem do sistema: $LANGUAGE
  • Qual é o diretório de temporário: $TMP

Mas não são somente essas variáveis e nem são somente as padrões do sistemas. Podemos criamos novas variáveis de ambiente para atendar um serie de necessidades.

Exemplos:

  • Qual é este ambiente (DEV, HML, ou PRD): $ENVIRONMENT
  • Qual é a região que o servidor se encontra: $AWS_REGION
  • Qual é o diretório dos arquivos da aplicação: $PWD

Desta forma, podemos instrumentalizar a aplicação com informações “externas” a ela, fazendo com que ela possa se adapte ao ambiente (“ecossistema”) em que ela se encontra. Essa variáveis são criadas no sistema operacional, fora do controle da aplicação, e existem independentes da aplicação.

Exemplo de como carregar uma variável de ambiente:

package br.tec.escovabit.app.variablesplace.environment;

import org.springframework.stereotype.Component;

@Component
public class VariableEnvironmentCorrect {

    public String getJavaHome() {
        String java_home = System.getenv("JAVA_HOME");
        return java_home;
    }
}

E onde está o grande problema desta camada? Esta em querer colocar tudo aqui. Muitos usam essa camada como bala de prata, pra colocar qualquer tipo de parametrização que seja especializada por ambiente (DEV, HML, PRD, esse tido de definição de ambiente).

Exemplo:

  • Qual é o endereço da API?
  • Qual é a quantidade de consumidores?
  • Qual é a quantidade de conexões com o banco de dados?

Até mesmo parâmetros que são vinculados diretamente com as regras de negocio do sistemas.

Por favor, não faça isso.

Quando for pensar em criar uma variável de ambiente, veja se a necessidade responde essas perguntas:

  1. Está variável é funcional para todas as aplicação que estiver rodando no mesmo lugar?
  2. A aplicação conseguiria de alguma outra maneira obter o valor desta variável?
  3. Está variável precisa de dupla custodia (Desenvolvedor e Infraestrutura)?

Se a resposta para qualquer uma das 3 for não, você esta colocar no lugar errado. Você deve repensar e ver se envia a variável para a camada anterior (Compilação – Build) ou posterior (Execução – Runtime).

Exemplo de variáveis de dupla custodia:

  • Usuário e senha
  • API Token
  • Chaves de criptografia

Variáveis de execução

Essa é a mais subi utilizada das camadas, e a que mais obtém resultados para organização.

Variáveis de execução, são carregadas junto a aplicação durante o processo de inicialização da própria aplicação.

Ou seja, quando se da inicio a aplicação, ela precisa carregar de alguma fonte as próprias variáveis. Essa fonte pode ser um serviço externo, um banco de dados, etc. Isso existe, para acolher todas as variáveis que precisam de algum dinamismo, são de exclusividade da aplicação e não são criadas pelo processo de compilação (build).

A grande vantagem dessa camada, além do dinamismo, é que multipás aplicações podem consultar o mesmo serviço e aplicações podem rodar simulando outros ambientes.

O principal motivo das pessoas não fazerem o uso dessa camada, é a necessidade de se integrar com algum outro sistema externo. Para isso, ou se usa algo pronto do framework utilizado, ou você precisa desenvolver essa integração. Não que seja de outro mundo fazer isso, geralmente é um cliente REST, mas as pessoas não fazem. 🙁

Fluxo simples de “pegar” as variáveis

Como isso depende do uso de um produto, geralmente os produtos já contem bibliotecas para as linguagens, abaixo dois produtos que fazem bem a função e um que “dá pra usar”:

1 – O Spring Cloud Config

Que é o meu xodó, e já tem bastantes textos aqui no site.

https://spring.io/projects/spring-cloud

http://3.139.95.241/2022/02/15/trabalhando-com-spring-cloud-config-server/

2 – Azure App Configuration

Voltado para quem já tem Cloud Azure

https://docs.microsoft.com/en-us/azure/architecture/patterns/external-configuration-store

3 – HashiCorp Vault

Não é o foco principal, mas prove uma solução plausível.

https://www.vaultproject.io/docs

Conclusão

Agora, você já sabe que existe mais de 1 lugar para coloca as suas variáveis, a utilização e a importância de cada uma das camadas.

Leitura recomendada: https://12factor.net/

Nos, vemos nos próximos posts.