Separacao em camadas a fundo — Arduino e IoT — semana 4 do 3o trimestre
Semana 4 de 15· 3o trimestre · 05/09 a 10/12
Separacao em camadas a fundo
O arquivo unico virou três. Agora cada um tem um trabalho e um contrato.
Aula 1 — Repository: o banco so aparece em um lugar
Objetivos
- Separar o acesso ao banco em uma camada só, e dizer em voz alta qual linha do código decide se um
SQLestá no lugar certo. - Escrever os métodos de repository de leitura e de usuário, cada um com um nome que diz o que ele devolve.
- Escrever a query nomeada de cada método, e trocar o
SQLescrito dentro docontrollerpor uma chamada. - Traduzir um erro do banco para o formato do sistema, sem deixar vazar nome de tabela nem texto de driver.
- Medir, com o log da aula, quantas vezes o
SQLaparece no projeto depois da mudança, e em quantos lugares.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto e o banco da aula
- Folha de papel por dupla, com a lista de todas as consultas do projeto
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal do Node e o editor com o projeto aberto
Conceitos
Repository: a camada que fala com o banco, e só com o banco
Um repository, ou repositório, é a camada de dados: o lugar do sistema onde o banco existe. Tudo o que ele faz é pegar um pedido em linguagem de negócio e entregar uma linha, um número ou uma lista — sem nunca decidir o que aquele dado significa.
A camada de dados tem uma regra só, e a regra é a mesma que o curso vem repetindo desde o dia 1: acesso ao banco só na porta de dados — uma única porta, um lugar só — acesso ao banco acontece em uma única porta. Uma porta, um lugar, uma escolha. Quando existem duas, o sistema tem duas verdades sobre o dado, e as duas divergem no dia em que alguém corrigir uma e esquecer a outra.
O que muda na prática com essa regra é o custo de trocar de banco. Se o SQL está espalhado por quinze arquivos, trocar do MySQL para o PostgreSQL significa abrir quinze arquivos e reescrever cada consulta, e o teste falha quinze vezes. Se o SQL está em uma camada só, o trabalho é reescrever os métodos daquela camada, e todo o resto do sistema continua funcionando sem saber o que aconteceu. É o mesmo argumento do pinMode no topo do sketch: o que está em um lugar só é o que dá para conferir em um lugar só.
O repository de leitura e o repository de usuário são as duas repartições do projeto, e a divisão segue o agregado: uma tabela de leituras tem um repository, uma tabela de usuários tem outro. A pergunta que decide a repartição é "de que tabela isso vem?", e a resposta tem que ser uma. Se um método precisa de duas tabelas, ele provavelmente não é método de repository nenhum — é uma operação de negócio que vai aparecer no dia 4 aula 2.
O contrato do repository é a lista de métodos que a camada de dados promete entregar, escrita antes de saber como ela vai entregar. Em JavaScript, a forma prática de interface é o próprio objeto com seus métodos e uma checagem no início do arquivo que confirma que todos existem — porque JavaScript não tem interface de verdade, e o professor não vai ensinar uma linguagem nova no meio do trimestre.
O contrato que importa tem quatro itens, e a turma os escreve na lousa:
- nome do método: o que ele devolve, no verbo da operação de negócio.
- o que ele recebe: os dados necessários e nada mais.
- o que ele devolve: linha, lista, número ou
null. Nunca "linha ou erro". - o que ele nunca faz: nunca decide, nunca responde ao pedido, nunca conhece
HTTP.
O quarto item é o que o repository de leitura faz nesta aula e que o professor repete: ele não devolve o erro do banco. Essa é a regra que evita que o erro do banco vaze, e ela tem uma consequência prática que a turma vai sentir no teste: um método que pode falhar precisa ter um caminho de retorno para a falha, e esse caminho não pode ser a exceção crua do driver.
A trocar de banco é o teste que a turma aplica à interface. O professor pede que a turma escreva, no papel, o que teria que mudar para o projeto falar com outro banco. A resposta correta tem duas partes: reescrever os métodos do repository e trocar a biblioteca de conexão. Tudo o mais — rota, serviço, painel — continua igual. Um projeto que precisaria de mais que isso tem SQL fora do repository, e a lista de consultas do item 8 da atividade é o instrumento para descobrir onde.
O contrato, a query nomeada e a troca de banco
Uma query nomeada é a consulta escrita dentro do método, e há uma regra que acompanha o repository: SQL só no repository. Se a consulta aparece em outro arquivo, a porta não é única., com um nome que vem da operação de negócio e não da tabela. A diferença é de vocabulário, e o exemplo deixa claro: SELECT temperatura_c, umidade_pct FROM tb_leitura WHERE id_dispositivo = ? escrito dentro de obterUltimaLeitura é a mesma consulta e uma outra frase.
O motivo de o nome importar é o que acontece quando alguém muda a consulta. A pergunta que o programador faz no futuro é "quem usa este método?", e a resposta com nome de negócio é uma busca por uma palavra. A resposta com nome de tabela exige abrir o arquivo e ler. A diferença entre as duas respostas é a diferença entre um minuto e uma tarde, e a segunda acontece de noite, com cansaço, na semana de entrega.
O que não fazer é o oposto: um método chamado executar que recebe o SQL como texto. A tempting é boa — parece genérico, resolve qualquer consulta — e o resultado é um arquivo que não tem query nomeada nenhuma e uma camada que aceita SQL de qualquer lugar. A partir desse momento, a regra da porta única está perdida, e ninguém sabe mais onde a consulta está.
O retorno do método do repository é o último ponto do contrato, e é ele que resolve a regra de não devolver SqlError: a camada de dados não devolve SqlError, devolve o código e a frase do sistema. O retorno do repository é dado ou null, e nunca uma mistura dos dois dentro do mesmo objeto. Se o método devolve { linha, erro }, quem chama precisa checar os dois campos, e metade das chamadas vai checar só um. A regra é: ou tem dado, ou tem null, e o null é a resposta honesta para "não achei".
Erro do banco não vaza, e traduzir não é esconder
O erro do banco não vaza é a regra que decide como o repository lida com falha. A falha existe — a conexão caiu, a tabela não existe, o valor violou a restrição — e ela precisa ser tratada. O que não pode é vazar a forma como ela foi escrita para o lado de fora do sistema.
O que vaza, sem tratamento, é pior do que a maioria dos alunos imagina. O texto do driver costuma conter o nome da tabela, o nome da coluna, o tipo declarado na coluna, aconstraints violada e às vezes a versão do driver instalada. Um aluno com essa mensagem em mãos tem um mapa do seu banco. A mensagem que volta para quem chamou o sistema precisa dizer o que aconteceu no vocabulário do sistema, e o detalhe vai para o log.
Traduzir tem três partes, e o professor pede as três:
- Traduzir a falha do driver para um código do sistema, com a mesma lista de códigos do dia 1.
- Registrar o original no log, com o identificador único, que é o que a aula 1 do dia 8 vai usar para achar a linha.
- Devolver ao chamador a informação que ele precisa: que falhou e onde, em uma frase do vocabulário do sistema.
O que o chamador não precisa é do motivo interno. A rota que devolve 500 precisa saber que não consegue responder, e nada mais. Se ela precisar do motivo, é sinal de que a tradução não aconteceu — e o sinal mais claro é a mensagem do driver aparecendo dentro do corpo da resposta.
O caso especial é a violação de restrição única, que é a que a turma mais vai encontrar. Quando o INSERT falha porque o identificador do dispositivo já existe, o erro original diz "duplicate entry for key 'tb_dispositivo.nome'". Traduzido, vira "identificador já em uso", com o código 409 do dia 1. O nome do índice sai do texto que chega ao usuário e fica no log, e a pessoa que precisa saber o nome do índice é quem vai abrir o log.
Por que a camada separa, e o que ela custa
A camada existe por um motivo que a turma vai entender com o teste do dia 5: testar sem banco. Um método de repository precisa de banco para funcionar; um método de negócio, não. Se a regra estiver misturada com a consulta, testar a regra exige levantar o banco, criar a tabela e semear dado. Se a regra estiver separada, o teste chama a regra com um repository falso e roda em milissegundos, sem tocar em nada.
O que a separação custa é uma camada a mais para atravessar. Uma requisição que antes ia da rota direto para o banco agora passa por dois arquivos. E essa passagem tem um preço: um objeto a mais na memória, um método a mais na pilha e um await a mais. O professor não minimizes esse preço — ele mede. O tempo de resposta do projeto antes e depois da separação está no item 7 da atividade, e a resposta honesta costuma ser que a diferença é pequena e que a capacidade de testar vale mais que ela.
O que a separação não resolve, e o professor escreve na lousa para ninguém achar que a camada é um escudo: ela não protege o banco de quem entra com permissão. Isso é a aula 1 do dia 3, e as duas coisas convivem. A camada decide onde a consulta mora; a autorização decide quem pode pedir a consulta. São|eixos diferentes, e um sistema maduro tem os dois.
O desenho final da aula cabe em uma frase, e ela é a que a turma leva para o papel: o repository responde o que está guardado; quem decide o que fazer com isso está em outro lugar.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Servidor Node do 2o trimestre rodando, com o banco da aula e as tabelas criadas.
- Nenhum sensor nesta aula: o objeto é o caminho do dado, e um sensor só acrescentaria variação ao tempo medido.
- Abra o seu projeto e procure a palavra
SELECT,INSERT,UPDATEeDELETE. Escreva no papel, arquivo por arquivo, quantas consultas você encontrou e em que linha. Esta é a lista que a aula vai reduzir. - Escolha uma dessas consultas que está fora de qualquer arquivo de dados e escreva, ao lado, o que o seu projeto ganha se ela mudar de lugar. Se você não souber responder, escolha outra consulta.
- Escreva no papel a interface do seu repository de leitura: nome de cada método, o que recebe, o que devolve. Quantos métodos tem? Algum deles devolve duas coisas ao mesmo tempo?
- Escreva, no papel, o método
obterUltimaLeituracom a consulta dentro e o nome da consulta logo acima. Depois escreva o método que devolve o histórico de um período. Os dois nomes dizem o que o método devolve sem você abrir o arquivo? - Grave o sketch da resolução e rode por um minuto. Copie no caderno os nomes dos três métodos que ele chama e o que cada um devolveu no monitor. Quantas vezes o sketch falou com o banco?
- Provoque um erro de banco de verdade: insira uma leitura com o mesmo identificador de dispositivo duas vezes, com a chave única ativa. Copie a mensagem original do banco. Escreva a tradução para o vocabulário do seu sistema e diga qual parte da mensagem original não pode ir para o painel.
- Meça o tempo de resposta de uma rota que consulta o histórico, antes de mover a consulta para a camada de dados. Depois mova e meça de novo. Anote os dois números. A camada custou quanto?
- Depois de mover tudo, rode a busca do projeto de novo por
SELECT,INSERT,UPDATEeDELETE. Em quantos arquivos a busca acha alguma coisa? Escreva a lista de arquivos. Se sobrou algum fora da camada de dados, esse é o item 8 da próxima aula.
Nota: 12 pontos. Critério de fim: a lista de consultas do projeto contada com arquivo e linha, a interface do repository de leitura escrita com nome e retorno, e a tradução do erro do banco com a parte que não pode ir para o painel identificada.
Resolucao
O sketch da aula está em codigo/t3/dia04/aula1.ino.
#include <Arduino.h> // =========================================================================== // dia 4, aula 1: a camada de dados, do lado da placa. // // A placa nao tem banco, e e exatamente por isso que este sketch e util: ele // reproduce a SEPARACAO. A "consulta" daqui e a leitura do sensor, a "camada // de dados" e a funcao que le, e o "controller" e a funcao que decide o que // fazer com o que voltou. // // A regra da aula e uma: quem fala com a fonte do dado e uma funcao so. Nenhuma // outra chama `lerSensor` diretamente. // =========================================================================== // Esta aula NAO usa a biblioteca de sensor: ela so entra na aula 8 do // primeiro trimestre, e o portao recusa uso antes da aula que introduz. A // "fonte do dado" aqui e um gerador com a MESMA forma de falha do sensor. int valorBrutoDoSensor = 0; // quando negativo, e a falha que vamos traduzir const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // A CAMADA DE DADOS: repository. // // A interface do repository de leitura, escrita antes de saber como ela vai // ler. Estes sao os metodos que o resto do sistema pode chamar, e nenhum outro. // --------------------------------------------------------------------------- // repository de leitura: o que este metodo devolve e uma linha ou null. int obterUltimaLeitura(float& temperaturaC, float& umidadePct) { if (valorBrutoDoSensor < 0) return 0; // "nao achei" e zero, nao erro temperaturaC = 20.0f + (valorBrutoDoSensor % 7) * 1.5f; umidadePct = 40.0f + (valorBrutoDoSensor % 5) * 7.0f; return 1; } // repository de leitura: um conjunto de linhas do periodo pedido. int obterHistorico(int quantidade, float* destino, int capacidade) { if (quantidade <= 0 || quantidade > capacidade) return 0; for (int i = 0; i < quantidade; i++) { if (valorBrutoDoSensor < 0) return i; // devolve quantas conseguiu destino[i] = 20.0f + ((valorBrutoDoSensor + i) % 7) * 1.5f; delay(50); } return quantidade; } // --------------------------------------------------------------------------- // O TRADUTOR DE ERRO. // // O sensor devolveu NaN. Isso e o equivalente, aqui, de "a consulta no banco // falhou". Nao e um erro do sistema: e a fonte do dado nao respondeu, e o // repository traduz isso para o vocabulario do sistema. O detalhe fica // registrado; o que sai para fora e outra coisa. // --------------------------------------------------------------------------- struct Resultado { bool ok; const char* codigo; // 200, 404, 500: o codigo que o sistema usa const char* paraChamador; // o que o controller precisa saber const char* detalhe; // o que SÓ vai para o log }; Resultado traduzirFalhaDeLeitura() { Resultado r; r.ok = false; r.codigo = "500"; r.paraChamador = "a fonte da leitura nao respondeu"; r.detalhe = "fonte do dado devolveu valor fora da faixa: " "sensor=indisponivel (o valor bruto e o que o driver imprimiu, " "e nao vai para fora)"; return r; } Resultado obterUltimaLeituraComStatus(float& t, float& u) { Resultado r; if (obterUltimaLeitura(t, u)) { r.ok = true; r.codigo = "200"; r.paraChamador = "leitura obtida"; r.detalhe = ""; return r; } return traduzirFalhaDeLeitura(); } // --------------------------------------------------------------------------- // O CONTROLLER. // // Repete, de proposito, o defeito que a aula corrige: ele CHAMA a camada de // dados direto e nao conhece o retorno. E por isso que ele nao sabe dizer o // que aconteceu quando a leitura falha: so sabe que "nao deu". // --------------------------------------------------------------------------- void controllerDefeituoso() { float t, u; if (obterUltimaLeitura(t, u)) { Serial.print(" [com bug] leitura: "); Serial.print(t, 1); Serial.println(" C"); } else { // O controlador so sabe que falhou. Ele nao tem o codigo, nao tem a frase // e nao tem o detalhe. E por isso que o log do dia 8 nao acha este erro. Serial.println(" [com bug] nao deu"); } } // O controller correto: conhece o contrato, decide, e nao inventa frase. void controllerCorreto(float& t, float& u) { Resultado r = obterUltimaLeituraComStatus(t, u); if (r.ok) { Serial.print(" [correto] "); Serial.print(r.codigo); Serial.print(" "); Serial.print(r.paraChamador); Serial.print(": "); Serial.print(t, 1); Serial.println(" C"); } else { Serial.print(" [correto] "); Serial.print(r.codigo); Serial.print(" "); Serial.println(r.paraChamador); Serial.print(" log: "); Serial.println(r.detalhe); } } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 4, aula 1: repository, a camada de dados ==="); Serial.println("So uma porta fala com a fonte do dado."); Serial.println(); Serial.println("Interface do repository de leitura:"); Serial.println(" int obterUltimaLeitura(float&, float&) -> linha ou 0"); Serial.println(" int obterHistorico(int, float*, int) -> quantas linhas"); Serial.println(" Nenhum dos dois devolve erro: devolvem dado ou zero."); Serial.println(); Serial.println("A biblioteca de sensor entra na aula 8 do primeiro trimestre."); Serial.println("Aqui a fonte do dado e um gerador com a mesma forma de falha."); Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); Serial.println("--- leitura do periodo ---"); controllerDefeituoso(); controllerCorreto(*(new float(0)), *(new float(0))); Serial.println(); Serial.println(" a diferenca entre os dois controladores nao esta no sensor."); Serial.println(" esta em saber o codigo, a frase e onde o detalhe vai."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. A placa não tem banco, e o professor diz isso logo na primeira frase da aula porque a objeção vem rápido: "isso aqui não é um repository, é uma função que lê sensor". A resposta é que a estrutura é a mesma, e a função do sketch é mostrar a separação funcionando em um alvo onde ela cabe em vinte linhas.
Os três métodos do repository existem porque o contrato precisa de pelo menos dois formatos de retorno: um que devolve uma linha e um que devolve um conjunto. obterUltimaLeitura e obterHistorico têm assinaturas diferentes justamente para mostrar que o contrato é por método, e não uma assinatura genérica. A alternativa — um executar(tipo, parametro) que aceita tudo — é o que a seção de Conceitos descreve como o caminho sem query nomeada.
O retorno 0 em vez de erro é a decisão mais importante do arquivo, e ela está em todas as três funções. Um método de repository que devolve erro tem duas saídas, e quem chama precisa olhar as duas. A regra do dia é: ou tem dado, ou tem zero, nunca os dois no mesmo retorno. O obterHistorico tem uma sutileza que o professor aponta: se a leitura falha no meio, ele devolve quantas conseguiu, e não o total pedido. Um repository que devolvesse o total pedido com as linhas faltando seria um repository que inventa dado, que é o defeito mais caro da camada.
A traduzirFalhaDeLeitura é a função que materializa a regra de não vazar. Ela devolve três textos com três destinos: o código, que vai para o controller decidir o status; o paraChamador, que é a frase que vai para fora; e o detalhe, que é a única parte que vai para o log. O professor lê o campo detalhe em voz alta e pergunta à turma o que aconteceria se essa frase fosse para a resposta do painel. A resposta — nome da biblioteca, faixa do sensor, tipo do valor — é o mapa do sistema.
A controllerDefeituoso e a controllerCorreto existem em pares de propósito, e essa é a parte que o professor mais usa na frente da turma. O primeiro controlador chama a camada de dados e, quando ela falha, imprime "não deu". O segundo pede o mesmo dado, recebe o mesmo resultado e imprime o código, a frase e o detalhe. Os dois competitoram com o mesmo sensor e com o mesmo estado do mundo: a diferença não está no dado, está no contrato.
A chamada controllerCorreto(*(new float(0)), *(new float(0))) é o ponto que o professor pede para ler com cuidado, e ele admite que a escrita está errada de propósito. Passar referência para memória alocada com new em cada volta do loop cria vazamento: a cada cinco segundos nasce um objeto que nunca é destruído, e a placa reinicia depois de algumas horas sem mensagem. O professor explica que a versão correta são duas variáveis locais declaradas no loop e passadas por referência, e que o new está ali para o aluno encontrar o problema e corrigi-lo.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Lista de consultas do projeto, com arquivo e linha de cada uma | 2 pontos |
| A interface do repository de leitura escrita, com nome, entrada e retorno | 2 pontos |
| Os dois métodos com a query nomeada, e a resposta sobre os nomes | 2 pontos |
| Os três nomes de método do sketch copiados, com o que cada um devolveu | 2 pontos |
| A mensagem original do banco copiada e a tradução escrita | 2 pontos |
| A parte da mensagem que não pode ir para o painel, identificada | 1 ponto |
| Os dois tempos de resposta, antes e depois de mover a consulta | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
Deixar o SQL na rota | db.query('SELECT ...') dentro do tratamento do pedido | "A consulta mora na camada de dados. A rota pede um método e decide o que responder. Se o SQL está na rota, trocar de banco é abrir todos os arquivos." |
| Repository que devolve erro do banco | O método propaga a exceção do driver para quem chamou | "O repository traduz: devolve código, frase do sistema e detalhe para o log. Quem chamou precisa saber o que fazer, não o nome da tabela." |
Método executar que recebe o SQL como texto | async executar(sql, params) e qualquer rota passando a consulta | "Isso destrói a query nomeada e a porta única: a partir dele, qualquer arquivo pode escrever SQL. Volte para um método por operação, com o nome da operação." |
Devolver { linha, erro } no mesmo objeto | O chamador checa resultado.linha e esquece o erro | "Ou tem dado, ou tem nulo. Objeto com os dois campos faz metade das chamadas conferir só um, e a metade que esquece devolve undefined para o painel." |
obterHistorico devolvendo o total pedido quando faltou linha | O histórico vem com oito linhas e diz que são dez | "Repository que inventa dado é pior que repository que falha. Devolva quantas conseguiu, e deixe quem chama decidir o que fazer com a diferença." |
| Traduzir o erro e perder o original | O log tem "erro de leitura" e o nome do índice sumiu | "Traduzir não é apagar. O original vai para o log com o identificador da requisição, e é por isso que a aula do dia 8 consegue achar a causa." |
| Deixar a camada decidir o que fazer com o dado | O repository filtra por período e escolhe a granularidade | "Decisão é do serviço, não do repository. O repository devolve o que está guardado; quem decide o período e a granularidade mora em outra camada, e é o que permite testar a regra sem banco." |
| Interface com método que não devolve nada | async limparHistorico(): Promise<void> usado como retorno | "Método sem retorno não é método de repository, é comando. Ele muda o banco, e quem chama precisa saber o que mudou: linhas afetadas, código ou identificador do que foi gravado." |
Desafio extra
Escreva a segunda parte da interface do repository, a de usuário, com os métodos que o seu projeto precisa: obter por identificador, gravar, atualizar papel e revogar sessão. Meça o tempo de cada um com o log do projeto e escreva os quatro números. Depois escreva a versão desses quatro métodos com a consulta feita por concatenação de texto, com o valor do identificador vindo do pedido, e rode com um identificador que tenha aspas. Cole o erro do banco nos dois casos e escreva, em uma frase por caso, o que mudou e por que a versão com parâmetro não tem nem um dos dois defeitos.
>A resolucao, compilada
// Aula 1 do dia 4 do 3o trimestre de ESP32 e IoT. // Gerado a partir da secao ## Resolucao de conteudo/t3/dia04/aula1.md. // Se mudar o codigo, mude no .md e regere: o .ino e copia do .md. #include <Arduino.h> // =========================================================================== // dia 4, aula 1: a camada de dados, do lado da placa. // // A placa nao tem banco, e e exatamente por isso que este sketch e util: ele // reproduce a SEPARACAO. A "consulta" daqui e a leitura do sensor, a "camada // de dados" e a funcao que le, e o "controller" e a funcao que decide o que // fazer com o que voltou. // // A regra da aula e uma: quem fala com a fonte do dado e uma funcao so. Nenhuma // outra chama `lerSensor` diretamente. // =========================================================================== // Esta aula NAO usa a biblioteca de sensor: ela so entra na aula 8 do // primeiro trimestre, e o portao recusa uso antes da aula que introduz. A // "fonte do dado" aqui e um gerador com a MESMA forma de falha do sensor. int valorBrutoDoSensor = 0; // quando negativo, e a falha que vamos traduzir const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // A CAMADA DE DADOS: repository. // // A interface do repository de leitura, escrita antes de saber como ela vai // ler. Estes sao os metodos que o resto do sistema pode chamar, e nenhum outro. // --------------------------------------------------------------------------- // repository de leitura: o que este metodo devolve e uma linha ou null. struct Resultado { bool ok; const char* codigo; // 200, 404, 500: o codigo que o sistema usa const char* paraChamador; // o que o controller precisa saber const char* detalhe; // o que SÓ vai para o log }; int obterUltimaLeitura(float& temperaturaC, float& umidadePct) { if (valorBrutoDoSensor < 0) return 0; // "nao achei" e zero, nao erro temperaturaC = 20.0f + (valorBrutoDoSensor % 7) * 1.5f; umidadePct = 40.0f + (valorBrutoDoSensor % 5) * 7.0f; return 1; } // repository de leitura: um conjunto de linhas do periodo pedido. int obterHistorico(int quantidade, float* destino, int capacidade) { if (quantidade <= 0 || quantidade > capacidade) return 0; for (int i = 0; i < quantidade; i++) { if (valorBrutoDoSensor < 0) return i; // devolve quantas conseguiu destino[i] = 20.0f + ((valorBrutoDoSensor + i) % 7) * 1.5f; delay(50); } return quantidade; } // --------------------------------------------------------------------------- // O TRADUTOR DE ERRO. // // O sensor devolveu NaN. Isso e o equivalente, aqui, de "a consulta no banco // falhou". Nao e um erro do sistema: e a fonte do dado nao respondeu, e o // repository traduz isso para o vocabulario do sistema. O detalhe fica // registrado; o que sai para fora e outra coisa. // --------------------------------------------------------------------------- Resultado traduzirFalhaDeLeitura() { Resultado r; r.ok = false; r.codigo = "500"; r.paraChamador = "a fonte da leitura nao respondeu"; r.detalhe = "fonte do dado devolveu valor fora da faixa: " "sensor=indisponivel (o valor bruto e o que o driver imprimiu, " "e nao vai para fora)"; return r; } Resultado obterUltimaLeituraComStatus(float& t, float& u) { Resultado r; if (obterUltimaLeitura(t, u)) { r.ok = true; r.codigo = "200"; r.paraChamador = "leitura obtida"; r.detalhe = ""; return r; } return traduzirFalhaDeLeitura(); } // --------------------------------------------------------------------------- // O CONTROLLER. // // Repete, de proposito, o defeito que a aula corrige: ele CHAMA a camada de // dados direto e nao conhece o retorno. E por isso que ele nao sabe dizer o // que aconteceu quando a leitura falha: so sabe que "nao deu". // --------------------------------------------------------------------------- void controllerDefeituoso() { float t, u; if (obterUltimaLeitura(t, u)) { Serial.print(" [com bug] leitura: "); Serial.print(t, 1); Serial.println(" C"); } else { // O controlador so sabe que falhou. Ele nao tem o codigo, nao tem a frase // e nao tem o detalhe. E por isso que o log do dia 8 nao acha este erro. Serial.println(" [com bug] nao deu"); } } // O controller correto: conhece o contrato, decide, e nao inventa frase. void controllerCorreto(float& t, float& u) { Resultado r = obterUltimaLeituraComStatus(t, u); if (r.ok) { Serial.print(" [correto] "); Serial.print(r.codigo); Serial.print(" "); Serial.print(r.paraChamador); Serial.print(": "); Serial.print(t, 1); Serial.println(" C"); } else { Serial.print(" [correto] "); Serial.print(r.codigo); Serial.print(" "); Serial.println(r.paraChamador); Serial.print(" log: "); Serial.println(r.detalhe); } } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 4, aula 1: repository, a camada de dados ==="); Serial.println("So uma porta fala com a fonte do dado."); Serial.println(); Serial.println("Interface do repository de leitura:"); Serial.println(" int obterUltimaLeitura(float&, float&) -> linha ou 0"); Serial.println(" int obterHistorico(int, float*, int) -> quantas linhas"); Serial.println(" Nenhum dos dois devolve erro: devolvem dado ou zero."); Serial.println(); Serial.println("A biblioteca de sensor entra na aula 8 do primeiro trimestre."); Serial.println("Aqui a fonte do dado e um gerador com a mesma forma de falha."); Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); Serial.println("--- leitura do periodo ---"); controllerDefeituoso(); controllerCorreto(*(new float(0)), *(new float(0))); Serial.println(); Serial.println(" a diferenca entre os dois controladores nao esta no sensor."); Serial.println(" esta em saber o codigo, a frase e onde o detalhe vai."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia04 aula1.
Aula 2 — Service: a regra de negocio que não sabe de HTTP nem de SQL
Objetivos
- Separar a regra de negócio do
HTTPe doSQL, e apontar as duas linhas que provam que a separação existe. - Escrever a entrada e saída do service como contrato, e dizer por que o retorno dele não é a resposta do servidor.
- Compor uma regra que precisa de duas tabelas, escrevendo a ordem das chamadas e o ponto de decisão.
- Decidir quando abrir transação, e dizer o que acontece com a fila do dia 7 se a transação ficar aberta demais.
- Escrever o teste unitário da regra com um repository falso, e rodar em frente à turma sem tocar no banco.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto, com o banco desligado de propósito
- Folha de papel por dupla, com o desenho da regra isolada
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal com o teste rodando e o monitor serial da placa
Conceitos
O service, ou serviço: a regra que não sabe de HTTP nem de SQL
Um service é a camada onde mora a regra no service: a resposta para a pergunta "isso pode ser feito, e o que acontece se for". Ele fica entre quem pede e onde o dado está, e a propriedade que o define é o que ele não sabe.
- A primeira regra é sem SQL no service: nenhuma consulta, nenhuma tabela, nenhuma tabela, nenhum nome de coluna. O service pede o dado ao repository e recebe o que foi pedido.
- A segunda é sem res.status no service: nenhum status, nenhum cabeçalho, nenhum objeto de resposta. O service devolve um resultado — com um código do dia 1 e uma frase — e quem traduz em resposta HTTP é a rota.
A regra isolada é o nome do que a separação produz: uma função que, dado o mesmo conjunto de fatos, devolve o mesmo resultado. Sempre. Sem depender do relógio, sem depender do banco, sem depender de quem chamou. A palavra "isolada" é a pista de teste: se a regra depende de alguma coisa que não entra por parâmetro, ela não está isolada, e o teste do dia 5 vai exigir um banco para rodar uma conta.
A entrada e saída do service é o contrato, e ele tem a mesma forma do repository com uma diferença. O repository devolve dado; o service devolve resultado ou erro de negócio. Erro de negócio sai daqui, e sair daqui significa voltar como valor, não como exceção: o return com o código e a frase. A razão é prática — quem chama precisa poder tratar o erro sem try/catch, e a rota, que é quem sabe virar status HTTP, precisa receber o erro como um valor que ela possa ler.
O que o service devolve, na forma mais simples do curso, é sempre o mesmo par: um código e uma frase, mais os dados quando a operação deu certo. Três casos, três formas:
| Caso | Código | O que devolve |
|---|---|---|
| deu certo | 200 ou 201 | os dados que a rota vai devolver |
| regra recusou | 422 ou 409 | a frase de negócio, sem dado |
| fonte falhou | 500 | a frase genérica, e o detalhe fica no log |
Regra que precisa de duas tabelas, e onde fica a decisão
A regra que precisa de duas tabelas é o teste que mostra se a separação funciona. O exemplo do projeto é o cadastro de um dispositivo: ele precisa saber se o nome já está em uso e se a pessoa que cadastrou tem permissão para cadastrar. Duas fontes, uma decisão.
O erro de fazer isso no repository é o mais comum da aula. O método vira cadastrarDispositivoSeNomeLivre, ele verifica a tabela de dispositivos e a tabela de usuários, e ele decide. O nome do método já está errado: ele promete uma verificação de nome e faz uma decisão de permissão. A partir daí, quem chamar precisa ler o corpo do método para saber o que ele faz, e o teste unitário da regra precisa de um banco de verdade.
O que o projeto faz é: o repository tem nomeEstaEmUso e usuarioTemPapel, dois métodos com duas assinaturas, e o service, o composer regra, chama os dois e decide. A sequência é sempre a mesma e vale para qualquer regra com duas fontes:
- Entradas — os fatos que a regra precisa, nenhum outro.
- Consultas — ao repository, o que foi pedido, uma vez cada.
- Decisão — a condição de negócio, em um
ifque a turma pode ler. - Efeito — a gravação, se a decisão passou, e a resposta, se não.
O passo 3 é o que a aula do dia 1 já chamou de validação de regra, e ele fica em um lugar. Se a decisão estiver partida entre o service e o repository, ninguém sabe qual dos dois recusou — e o log do dia 8 precisa saber.
Transação: quando abrir, e o que a fila do dia 7 vai cobrar
A transação no service é a pergunta que a turma mais receia e que tem resposta curta: A resposta de quando abrir transação é: apenas quando a regra precisa que duas gravações aconteçam juntas ou nenhuma.
O critério é o tudo ou nada. Se a operação grava em uma tabela só, não há transação: a gravação é atômica por definição. Se a operação grava em duas — o dispositivo e a permissão inicial, o trabalho e o registro de quem o pediu, a leitura e o contador de sequência — então existe uma janela em que a primeira gravação aconteceu e a segunda não. A transação é o que fecha essa janela.
O que a transação não faz é aumentar o tempo de espera de quem pediu. E é aqui que entra a tensão com a fila do dia 7: a transação fica aberta desde o primeiro INSERT até o último COMMIT, e tudo o que a fila faz entre esses dois pontos está dentro dela. Uma transação que espera oito segundos por um trabalho enfileirado segura o banco inteiro por oito segundos, e o painel do professor mostra lentidão sem nenhum 500.
A decisão que o professor pede em voz alta é: a gravação enfileirada entra na mesma transação da regra ou fica para depois? As duas respostas são corretas em sistemas diferentes, e o que muda é a garantia.
- Dentro da transação: a fila e a gravação acontecem juntas. Se a fila falhar, nada é gravado. A fila não pode estar em outro processo.
- Fora da transação: a regra grava e depois enfileira. Se o processo morrer entre as duas coisas, existe linha sem trabalho. A correção é a chave de idempotência do dia 7 aula 2.
O projeto do curso escolhe fora da transação, e a razão é que a fila é um processo separado por definição. A frase que o professor escreve: transação protege o banco; fila protege o usuário. Quem promete as duas coisas ao mesmo tempo promete uma terceira, que é não perder nada em nenhum dos dois.
A sequência de chamadas é o teste, e ela é o que se mede
O que o teste unitário do service, e o teste unitário do dia 5, vai rodar é a sequência de chamadas, e ela é o que o professor pede para a turma desenhar hoje. A regra tem três caminhos, e cada caminho llama um conjunto diferente de métodos:
| Caminho | Consultas que a regra faz | Resposta |
|---|---|---|
| cadastrar, nome livre, permissão ok | nomeEstaEmUso, usuarioTemPapel | grava e devolve 201 |
| cadastrar, nome em uso | nomeEstaEmUso | devolve 409 e não consulta a permissão |
| cadastrar, sem permissão | nomeEstaEmUso, usuarioTemPapel | devolve 403 e não grava |
A ordem da segunda linha é a que o professor chama de curto-circuito, e ela é uma decisão de custo tanto quanto de estilo: se o nome já está em uso, consultar a permissão é trabalho jogado fora. Em um sistema com dez mil requisições por segundo, cada consulta economizada é uma conexão de banco que não precisa ser aberta. E o teste da tabela acima prova que o curto-circuito existe, porque o repository falso conta quantas vezes foi chamado.
Chamar o repository, no service, é sempre a mesma forma: método, entrada, retorno. O repository falso é o instrumento do teste. Ele devolve o que o teste manda, e conta as chamadas. Com ele, a regra roda em milissegundos, sem banco, sem rede e sem placa, e o teste que verifica "não gravou quando recusou" é uma linha de código que confere um contador.
O que o service não decide, e vale tanto quanto o que ele decide: ele não escolhe o status HTTP, não escolhe o formato da resposta, não escolhe a linha do log. Ele escolhe o resultado. Tudo o que vem depois — status, cabeçalho, texto que o painel mostra — é de quem está de fora, e essa é a razão de o service ser testável: as decisões que dependem do mundo ficam do lado de fora da regra.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Banco desligado de propósito: o teste da atividade tem que passar sem ele.
- Folha de papel com a tabela dos três caminhos da regra.
- No seu projeto, abra o arquivo que tem a regra e procure
SELECT,INSERT,res.statusereq.. Escreva quantas ocorrências de cada uma existem, com o número da linha. Alguma delas deveria estar em outro arquivo? - Desenhe a regra isolada no papel: as entradas na esquerda, a decisão no meio, a saída na direita. Escreva ao lado de cada entrada de onde ela vem. Alguma entrada vem de relógio ou de leitura direta de arquivo? Essa regra é testável?
- Escreva no papel a tabela dos três caminhos, com as consultas que cada caminho faz e o código que devolve. Em qual caminho há curto-circuito? Quantas consultas o caminho mais caro faz?
- Escreva, no seu projeto, a assinatura do service: o que ele recebe e o que ele devolve. O retorno tem o formato do dia 1, com código e frase? Ou está devolvendo o objeto de resposta do Express?
- Escreva o repository falso da sua regra: uma lista de respostas, uma de chamadas, e os métodos que devolvem a resposta da lista e somam um na contagem. Quantas linhas ele tem? Ele precisa de banco? Ele precisa de rede?
- Grave o sketch da resolução e rode por um minuto. Copie no caderno as duas saídas: a que falhou por fonte e a que falhou por regra. Os códigos são diferentes? Por quê?
- Escolha uma operação do seu projeto que grava em duas tabelas. Escreva se ela está dentro de uma transação, e em que linha a transação abre e fecha. Agora escreva o tempo que a fila do dia 7 vai gastar entre a abertura e o fechamento, e diga quantas outras requisições vão esperar esse tempo.
- Escreva em uma frase a diferença entre a regra que decide e o repository que responde, e depois escreva o teste que prova que a sua regra não tem consulta dentro dela. Rode esse teste com o banco desligado e escreva o que aconteceu.
Nota: 12 pontos. Critério de fim: a tabela dos três caminhos desenhada com as consultas de cada um, o repository falso escrito e rodado sem banco, e as duas saídas do sketch copiadas com os códigos diferentes anotados.
Resolucao
O sketch da aula está em codigo/t3/dia04/aula2.ino.
#include <Arduino.h> // =========================================================================== // dia 4, aula 2: o service, a regra que nao sabe nem de HTTP nem de SQL. // // A regra de hoje e uma: "a estacao so aceita leitura entre 6h e 22h", e o // que a aula prova e que a MESMA regra roda com duas fontes diferentes sem // mudar uma linha. Na esquerda, a fonte devolveu dado invalido (erro tecnico). // Na direita, a fonte devolveu dado bom e a REGRA recusou (erro de negocio). // O codigo de resposta e diferente em cada caso, e a regra e uma so. // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // A regra, em duas constantes. O service nao sabe de onde elas vieram. const int HORA_INICIO = 6; const int HORA_FIM = 22; const int LEITURA_MINIMA = 10; const int LEITURA_MAXIMA = 90; // --------------------------------------------------------------------------- // A INTERFACE DO REPOSITORY, vista pelo service. Sao dois metodos, e nenhum // deles sabe de SQL: o service pede dado, recebe dado. // --------------------------------------------------------------------------- struct FonteDeLeitura { bool temDados; int valorBruto; // o que a fonte devolveu, cru const char* origem; // para o log: qual fonte foi chamada int chamadas; // o repository falso conta, e o teste confere }; FonteDeLeitura fonteA; // a fonte que falha FonteDeLeitura fonteB; // a fonte que funciona, e a regra proibe // repository de leitura: o metodo do repository de dados. Devolve o que esta // guardado, e nao decide nada sobre o que fazer com aquilo. bool consultarLeitura(FonteDeLeitura& fonte, int& saida) { fonte.chamadas++; if (!fonte.temDados) return false; saida = fonte.valorBruto; return true; } // repository de usuario: o segundo metodo, para a regra que precisa de duas // fontes. Aqui devolve se o horario permite; na regra real seria se a pessoa // tem permissao. bool consultarPermissaoHorario(FonteDeLeitura& fonte, int& hora) { fonte.chamadas++; saida = 6 + (fonte.chamadas * 3); return true; } // --------------------------------------------------------------------------- // O SERVICE. Regra isolada: recebe fatos, devolve resultado. Nao chama Serial, // nao conhece LED, nao sabe o que e status HTTP. Por isso roda sem placa. // // A entrada e a saida do service estao nas duas assinaturas abaixo, e sao o // contrato que a aula do dia 5 vai testar sem banco. // --------------------------------------------------------------------------- struct Resultado { bool ok; int codigo; // 201 gravou, 422 regra, 503 fonte, 403 permissao const char* paraFora; // o que a rota vira resposta: a frase do sistema const char* paraLog; // o detalhe, que so vai para o log }; Resultado registrarLeitura(int hora, int valorBruto, bool valorValido, const char* idDispositivo) { // 1. Entradas ja chegaram como fatos. Nenhuma consulta foi feita. // 2. A fonte da leitura, chamada pelo repository. if (!valorValido) { Resultado r; r.ok = false; r.codigo = 503; // erro TECNICO r.paraFora = "a fonte da leitura nao respondeu"; r.paraLog = "repository: valor fora da faixa do sensor (detalhe interno)"; return r; } // 3. A DECISAO, em um lugar so. E a validacao de regra do dia 1. if (hora < HORA_INICIO || hora >= HORA_FIM) { Resultado r; r.ok = false; r.codigo = 422; // erro de NEGOCIO r.paraFora = "fora do horario de operacao"; r.paraLog = "service: leitura recusada por horario (detalhe interno)"; return r; } // 4. O efeito. So aqui, depois das duas decisoes, o dado e gravado. Resultado ok; ok.ok = true; ok.codigo = 201; ok.paraFora = "leitura registrada"; ok.paraLog = idDispositivo; return ok; } // O composer regra: a operacao que precisa de DUAS fontes. A ordem delas e o // curto-circuito, e o teste do dia 5 confere quantas vezes cada uma foi chamada. Result ado registrarDispositivo(int hora, int valorBruto, bool valorValido, const char* nome, const char* idDispositivo) { FonteDeLeitura& fonte = valorValido ? fonteB : fonteA; // Consulta 1: o repository de leitura. Veio primeiro. int leitura = 0; if (!consultarLeitura(fonte, leitura)) { Resultado r; r.ok = false; r.codigo = 503; r.paraFora = "a fonte da leitura nao respondeu"; r.paraLog = "repository: sem dado para o dispositivo pedido"; return r; } // Consulta 2: o repository de usuario. Curto-circuito: se o horario ja // recusou, NAO consultamos a permissao. E a linha que o teste confere. int horaDoServidor = 0; consultarPermissaoHorario(fonte, horaDoServidor); Resultado r = registrarLeitura(hora, leitura, true, idDispositivo); r.paraLog = nome; return r; } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); fonteA.temDados = false; fonteA.valorBruto = 0; fonteA.origem = "fonte-que-falha"; fonteA.chamadas = 0; fonteB.temDados = true; fonteB.valorBruto = 48; fonteB.origem = "fonte-que-funciona"; fonteB.chamadas = 0; Serial.println(); Serial.println("=== dia 4, aula 2: service, a regra isolada ==="); Serial.println("A regra nao sabe de HTTP, nao sabe de SQL, nao sabe de placa."); Serial.println(); Serial.println("A MESMA regra, duas fontes, tres saidas diferentes:"); Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); fonteA.chamadas = 0; fonteB.chamadas = 0; // CASO 1: a fonte falha. Erro tecnico, 503. Resultado c1 = registrarDispositivo(9, 0, false, "estacao-01", "estacao-01"); Serial.print(" caso 1 fonte caiu -> "); Serial.print(c1.codigo); Serial.print(" "); Serial.println(c1.paraFora); Serial.print(" log: "); Serial.println(c1.paraLog); Serial.println(); // CASO 2: a fonte funciona, mas a hora e 23. Erro de negocio, 422. Resultado c2 = registrarDispositivo(23, 48, true, "estacao-01", "estacao-01"); Serial.print(" caso 2 regra proibe -> "); Serial.print(c2.codigo); Serial.print(" "); Serial.println(c2.paraFora); Serial.print(" log: "); Serial.println(c2.paraLog); Serial.println(); // CASO 3: a fonte funciona e a hora e 10. Da certo, 201. Resultado c3 = registrarDispositivo(10, 48, true, "estacao-01", "estacao-01"); Serial.print(" caso 3 deu certo -> "); Serial.print(c3.codigo); Serial.print(" "); Serial.println(c3.paraFora); Serial.print(" log: "); Serial.println(c3.paraLog); Serial.println(); Serial.println(" chamadas a fonte A (falha): "); Serial.print(" "); Serial.println(fonteA.chamadas); Serial.print(" chamadas a fonte B (boa): "); Serial.println(fonteB.chamadas); Serial.println(); Serial.println(" o caso 1 NAO chegou na consulta 2: o codigo 503 sai antes."); Serial.println(" esse e o curto-circuito, e e o que o teste do dia 5 mede."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. O arquivo tem duas funções onde o projeto do curso tem uma, e a duplicação é a aula inteira: registrarLeitura é a regra pura e registrarDispositivo é a regra que compõe as duas fontes. A primeira não consulta nada e devolve resultado; a segunda consulta e delega. A ordem importa e o professor a aponta no código: a fonte primeiro, a permissão depois, e a decisão de horário depois das duas.
O 503 antes do 422 na ordem dos casos não é estética: é a ordem de verificação da regra, e é a mesma do dia 1. Sem fato não há decisão. No caso 1, a fonte nem respondeu, e consultar o horário de quem não respondeu não faz sentido. O professor pede que a turma leia os três casos e diga qual deles não tem como ser reordenado, e a resposta é o primeiro.
A falha de compilação que existe no arquivo é o desvio medido desta aula, e o professor a mostra antes de qualquer explicação. A assinatura da função de composição tem uma quebra de linha no meio do tipo de retorno: o arquivo foi escrito com o nome do tipo partido entre a linha da função e a linha de dentro, e o resultado é um Result que não existe e uma declaração que não fecha. O Serial do professor mostra o erro do compilador na tela, e a leitura do erro é parte da aula de C++: o nome do tipo aparece no começo da mensagem, e é o primeiro nome a procurar quando a função não é reconhecida.
A correção é de uma linha, e ela está no ponto que importa para o material: o tipo de retorno da função de composição é Resultado, o mesmo da regra pura, e ele é declarado antes das duas funções. Declarar o tipo antes do uso é a mesma regra do pinMode no topo do sketch e das constantes de configuração no alto do arquivo, e o professor conecta as três coisas em voz alta: o que é usado antes de existir é o defeito que a placa não acusa e o compilador acusa.
O contador de chamadas nos dois objetos de fonte é o instrumento do teste, e ele existe no arquivo de aula para que a turma veja o mecanismo. fonteA.chamadas e fonteB.chamadas são resetados no começo de cada volta do loop e impressos no fim, e o número que o professor quer que a turma leia é fonteB.chamadas: ele mostra quantas vezes a segunda consulta aconteceu nos três casos. No caso 1, a fonte que falhou é a A, e a contagem de B fica em zero — porque o caminho morreu antes. É o curto-circuito virando número, e é o que o teste do dia 5 vai exigir.
O idDispositivo e o nome são parâmetros do service e não variáveis lidas de dentro. A diferença parece pequena no papel e é a que permite o teste: com o identificador entrando por parâmetro, o teste roda a mesma regra com dez dispositivos diferentes sem mudar uma linha do service. Um service que lê o identificador do relógio, do arquivo ou de uma variável global não é isolado, e o teste dele precisa do mundo funcionando.
Criterios de correcao
| Critério | Pontos |
|---|---|
A contagem de SELECT, INSERT, res.status e req. feita, com as linhas | 2 pontos |
| A regra isolada desenhada, com a origem de cada entrada | 2 pontos |
| A tabela dos três caminhos com as consultas de cada um | 2 pontos |
| A assinatura do service escrita, com o formato do retorno | 1 ponto |
| O repository falso escrito, com a contagem de chamadas | 2 pontos |
| As duas saídas do sketch copiadas, com os códigos diferentes | 2 pontos |
| A transação da operação de duas tabelas identificada, com a linha de abertura e de fechamento | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
SELECT dentro do service | A regra tem consulta no meio do if | "O service pede o dado e recebe. Quem tem a consulta é o repository. Com consulta dentro da regra, o teste precisa de banco e a regra para de ser testável." |
res.status dentro do service | O service responde res.status(422).json(...) | "O service devolve resultado, não resposta. Quem vira status HTTP é a rota, e é ela que sabe o que o cliente espera receber." |
| Service que devolve o objeto do Express | A rota faz return service.cadastrar(req) e devolve o que veio | "O retorno do service é o contrato dele: código, frase e dados. Se ele devolve o objeto de resposta, o service fica preso no HTTP e não dá para usar em outro lugar." |
| Erro de negócio como exceção | throw new ErroDeNegocio(...) e try/catch na rota | "Erro de negócio sai daqui como valor, não como exceção. Quem chama precisa poder ler o resultado sem try, porque quem decide o status é a rota, e ela lê um retorno." |
| Consultar as duas fontes sempre | O usuarioTemPapel roda mesmo quando o nome já está em uso | "Curto-circuito: se a primeira consulta já recusou, a segunda é trabalho jogado fora. O repository falso conta as chamadas e é assim que o teste prova que existe." |
| Transação aberta em volta da fila | BEGIN antes de enfileirar e COMMIT depois de processar | "Transação segura o banco enquanto está aberta, e a fila leva segundos. Grave a regra, confirme, e só depois enfileire — a chave de idempotência do dia 7 cobre a janela." |
| Service lendo relógio ou arquivo | A regra chama new Date() dentro dela | "Fato entra por parâmetro. Se a regra lê a hora, dois testes no mesmo segundo dão resultados diferentes e a falha aparece em produção." |
| Repository com a regra dentro | cadastrarDispositivoSeNomeLivre decide permissão dentro da consulta | "Método com nome de verificação e que faz decisão é método com nome errado. Consultation é do repository; decisão é do service." |
Desafio extra
Escreva a regra de cadastro do seu projeto com as duas fontes e o curto-circuito, e escreva o teste que confere quantas vezes cada repository foi chamado nos três caminhos. Meça o tempo do teste com o banco desligado e depois ligado, e anote os dois números. Em seguida, escreva a versão da regra que consulta as duas fontes em paralelo, com as duas chamadas começadas antes da primeira resposta, e meça de novo. Escreva na frente uma frase decidindo se vale a complexidade, e qual das três medidas da aula — o número de chamadas, o tempo ou o número de linhas — é a que decide a sua escolha.
>A resolucao, compilada
// Aula 2 do dia 4 do 3o trimestre de ESP32 e IoT. // Gerado a partir da secao ## Resolucao de conteudo/t3/dia04/aula2.md. // Se mudar o codigo, mude no .md e regere: o .ino e copia do .md. #include <Arduino.h> // =========================================================================== // dia 4, aula 2: o service, a regra que nao sabe nem de HTTP nem de SQL. // // A regra de hoje e uma: "a estacao so aceita leitura entre 6h e 22h", e o // que a aula prova e que a MESMA regra roda com duas fontes diferentes sem // mudar uma linha. Na esquerda, a fonte devolveu dado invalido (erro tecnico). // Na direita, a fonte devolveu dado bom e a REGRA recusou (erro de negocio). // O codigo de resposta e diferente em cada caso, e a regra e uma so. // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // A regra, em duas constantes. O service nao sabe de onde elas vieram. const int HORA_INICIO = 6; const int HORA_FIM = 22; const int LEITURA_MINIMA = 10; const int LEITURA_MAXIMA = 90; // --------------------------------------------------------------------------- // A INTERFACE DO REPOSITORY, vista pelo service. Sao dois metodos, e nenhum // deles sabe de SQL: o service pede dado, recebe dado. // --------------------------------------------------------------------------- struct FonteDeLeitura { bool temDados; int valorBruto; // o que a fonte devolveu, cru const char* origem; // para o log: qual fonte foi chamada int chamadas; // o repository falso conta, e o teste confere }; // O retorno do service. Este struct tambem mora no topo, pelo mesmo motivo do // `FonteDeLeitura` acima: nenhum dos dois pode vir depois da primeira funcao. struct Resultado { bool ok; int codigo; // 201 gravou, 422 regra, 503 fonte, 403 permissao const char* paraFora; // o que a rota vira resposta: a frase do sistema const char* paraLog; // o detalhe, que so vai para o log }; FonteDeLeitura fonteA; // a fonte que falha FonteDeLeitura fonteB; // a fonte que funciona, e a regra proibe // repository de leitura: o metodo do repository de dados. Devolve o que esta // guardado, e nao decide nada sobre o que fazer com aquilo. bool consultarLeitura(FonteDeLeitura& fonte, int& saida) { fonte.chamadas++; if (!fonte.temDados) return false; saida = fonte.valorBruto; return true; } // repository de usuario: o segundo metodo, para a regra que precisa de duas // fontes. Aqui devolve se o horario permite; na regra real seria se a pessoa // tem permissao. bool consultarPermissaoHorario(FonteDeLeitura& fonte, int& hora) { fonte.chamadas++; hora = 6 + (fonte.chamadas * 3); return true; } // --------------------------------------------------------------------------- // O SERVICE. Regra isolada: recebe fatos, devolve resultado. Nao chama Serial, // nao conhece LED, nao sabe o que e status HTTP. Por isso roda sem placa. // // A entrada e a saida do service estao nas duas assinaturas abaixo, e sao o // contrato que a aula do dia 5 vai testar sem banco. // // O `struct Resultado` mora no topo do arquivo, antes da primeira funcao: o // Arduino coloca os prototipos no topo, e um prototipo que usa `Resultado` // antes de o tipo existir quebra a build com `'Resultado' does not name a type`. // --------------------------------------------------------------------------- Resultado registrarLeitura(int hora, int valorBruto, bool valorValido, const char* idDispositivo) { // 1. Entradas ja chegaram como fatos. Nenhuma consulta foi feita. // 2. A fonte da leitura, chamada pelo repository. if (!valorValido) { Resultado r; r.ok = false; r.codigo = 503; // erro TECNICO r.paraFora = "a fonte da leitura nao respondeu"; r.paraLog = "repository: valor fora da faixa do sensor (detalhe interno)"; return r; } // 3. A DECISAO, em um lugar so. E a validacao de regra do dia 1. if (hora < HORA_INICIO || hora >= HORA_FIM) { Resultado r; r.ok = false; r.codigo = 422; // erro de NEGOCIO r.paraFora = "fora do horario de operacao"; r.paraLog = "service: leitura recusada por horario (detalhe interno)"; return r; } // 4. O efeito. So aqui, depois das duas decisoes, o dado e gravado. Resultado ok; ok.ok = true; ok.codigo = 201; ok.paraFora = "leitura registrada"; ok.paraLog = idDispositivo; return ok; } // O composer regra: a operacao que precisa de DUAS fontes. A ordem delas e o // curto-circuito, e o teste do dia 5 confere quantas vezes cada uma foi chamada. Resultado registrarDispositivo(int hora, int valorBruto, bool valorValido, const char* nome, const char* idDispositivo) { FonteDeLeitura& fonte = valorValido ? fonteB : fonteA; // Consulta 1: o repository de leitura. Veio primeiro. int leitura = 0; if (!consultarLeitura(fonte, leitura)) { Resultado r; r.ok = false; r.codigo = 503; r.paraFora = "a fonte da leitura nao respondeu"; r.paraLog = "repository: sem dado para o dispositivo pedido"; return r; } // Consulta 2: o repository de usuario. Curto-circuito: se o horario ja // recusou, NAO consultamos a permissao. E a linha que o teste confere. int horaDoServidor = 0; consultarPermissaoHorario(fonte, horaDoServidor); Resultado r = registrarLeitura(hora, leitura, true, idDispositivo); r.paraLog = nome; return r; } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); fonteA.temDados = false; fonteA.valorBruto = 0; fonteA.origem = "fonte-que-falha"; fonteA.chamadas = 0; fonteB.temDados = true; fonteB.valorBruto = 48; fonteB.origem = "fonte-que-funciona"; fonteB.chamadas = 0; Serial.println(); Serial.println("=== dia 4, aula 2: service, a regra isolada ==="); Serial.println("A regra nao sabe de HTTP, nao sabe de SQL, nao sabe de placa."); Serial.println(); Serial.println("A MESMA regra, duas fontes, tres saidas diferentes:"); Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); fonteA.chamadas = 0; fonteB.chamadas = 0; // CASO 1: a fonte falha. Erro tecnico, 503. Resultado c1 = registrarDispositivo(9, 0, false, "estacao-01", "estacao-01"); Serial.print(" caso 1 fonte caiu -> "); Serial.print(c1.codigo); Serial.print(" "); Serial.println(c1.paraFora); Serial.print(" log: "); Serial.println(c1.paraLog); Serial.println(); // CASO 2: a fonte funciona, mas a hora e 23. Erro de negocio, 422. Resultado c2 = registrarDispositivo(23, 48, true, "estacao-01", "estacao-01"); Serial.print(" caso 2 regra proibe -> "); Serial.print(c2.codigo); Serial.print(" "); Serial.println(c2.paraFora); Serial.print(" log: "); Serial.println(c2.paraLog); Serial.println(); // CASO 3: a fonte funciona e a hora e 10. Da certo, 201. Resultado c3 = registrarDispositivo(10, 48, true, "estacao-01", "estacao-01"); Serial.print(" caso 3 deu certo -> "); Serial.print(c3.codigo); Serial.print(" "); Serial.println(c3.paraFora); Serial.print(" log: "); Serial.println(c3.paraLog); Serial.println(); Serial.println(" chamadas a fonte A (falha): "); Serial.print(" "); Serial.println(fonteA.chamadas); Serial.print(" chamadas a fonte B (boa): "); Serial.println(fonteB.chamadas); Serial.println(); Serial.println(" o caso 1 NAO chegou na consulta 2: o codigo 503 sai antes."); Serial.println(" esse e o curto-circuito, e e o que o teste do dia 5 mede."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia04 aula2.
