Separacao em camadas a fundo — Arduino e IoT — semana 4 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 4 · Separacao em camadas a fundo — Material de Apoio Arduino

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 SQL está 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 SQL escrito dentro do controller por 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 SQL aparece 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:

  1. Traduzir a falha do driver para um código do sistema, com a mesma lista de códigos do dia 1.
  2. Registrar o original no log, com o identificador único, que é o que a aula 1 do dia 8 vai usar para achar a linha.
  3. 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:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • 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.
  1. Abra o seu projeto e procure a palavra SELECT, INSERT, UPDATE e DELETE. Escreva no papel, arquivo por arquivo, quantas consultas você encontrou e em que linha. Esta é a lista que a aula vai reduzir.
  2. 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.
  3. 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?
  4. Escreva, no papel, o método obterUltimaLeitura com 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?
  5. 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?
  6. 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.
  7. 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?
  8. Depois de mover tudo, rode a busca do projeto de novo por SELECT, INSERT, UPDATE e DELETE. 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érioPontos
Lista de consultas do projeto, com arquivo e linha de cada uma2 pontos
A interface do repository de leitura escrita, com nome, entrada e retorno2 pontos
Os dois métodos com a query nomeada, e a resposta sobre os nomes2 pontos
Os três nomes de método do sketch copiados, com o que cada um devolveu2 pontos
A mensagem original do banco copiada e a tradução escrita2 pontos
A parte da mensagem que não pode ir para o painel, identificada1 ponto
Os dois tempos de resposta, antes e depois de mover a consulta1 ponto

Erros comuns

ErroComo apareceCorreção
Deixar o SQL na rotadb.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 bancoO 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 textoasync 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 objetoO 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 linhaO 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 originalO 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 dadoO 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 nadaasync 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 HTTP e do SQL, 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:

CasoCódigoO que devolve
deu certo200 ou 201os dados que a rota vai devolver
regra recusou422 ou 409a frase de negócio, sem dado
fonte falhou500a 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:

  1. Entradas — os fatos que a regra precisa, nenhum outro.
  2. Consultas — ao repository, o que foi pedido, uma vez cada.
  3. Decisão — a condição de negócio, em um if que a turma pode ler.
  4. 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:

CaminhoConsultas que a regra fazResposta
cadastrar, nome livre, permissão oknomeEstaEmUso, usuarioTemPapelgrava e devolve 201
cadastrar, nome em usonomeEstaEmUsodevolve 409 e não consulta a permissão
cadastrar, sem permissãonomeEstaEmUso, usuarioTemPapeldevolve 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:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • 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.
  1. No seu projeto, abra o arquivo que tem a regra e procure SELECT, INSERT, res.status e req.. Escreva quantas ocorrências de cada uma existem, com o número da linha. Alguma delas deveria estar em outro arquivo?
  2. 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?
  3. 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?
  4. 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?
  5. 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?
  6. 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ê?
  7. 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.
  8. 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érioPontos
A contagem de SELECT, INSERT, res.status e req. feita, com as linhas2 pontos
A regra isolada desenhada, com a origem de cada entrada2 pontos
A tabela dos três caminhos com as consultas de cada um2 pontos
A assinatura do service escrita, com o formato do retorno1 ponto
O repository falso escrito, com a contagem de chamadas2 pontos
As duas saídas do sketch copiadas, com os códigos diferentes2 pontos
A transação da operação de duas tabelas identificada, com a linha de abertura e de fechamento1 ponto

Erros comuns

ErroComo apareceCorreção
SELECT dentro do serviceA 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 serviceO 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 ExpressA 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çãothrow 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 sempreO 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 filaBEGIN 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 arquivoA 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 dentrocadastrarDispositivoSeNomeLivre 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.