Com (fora) o aplicativo trava, por favor!

Publicados: 2020-02-12

Os programadores tentam evitar falhas em seu código. Se alguém usar seu aplicativo, ele não deve quebrar ou fechar inesperadamente. Essa é uma das medidas mais simples de qualidade – se um aplicativo trava com frequência, provavelmente não é bem feito.

As falhas de aplicativos ocorrem quando um programa está prestes a fazer algo indefinido ou ruim , como dividir um valor por zero ou acessar recursos restritos em uma máquina. Também pode ser feito explicitamente pelo programador que escreveu o aplicativo. “Isso nunca vai acontecer, então eu vou pular” – esse é um pensamento bastante comum e não completamente irracional. Há alguns casos que simplesmente não podem ocorrer, nunca, até que… aconteça.

Promessas quebradas

Um dos casos mais comuns em que sabemos que algo não pode acontecer são as APIs. Nós concordamos entre back-end e front-end – essa é a única resposta do servidor que você pode obter para esta solicitação. Os mantenedores desta biblioteca documentaram este comportamento de função. A função não pode fazer mais nada. Ambas as formas de pensar estão corretas, mas ambas podem causar problemas.

Ao usar uma biblioteca, você pode contar com ferramentas de linguagem para ajudá-lo a lidar com todos os casos possíveis. Se a linguagem que você usa não tem nenhuma forma de verificação de tipo ou análise estática, você deve cuidar disso sozinho. Ainda assim, você pode verificar isso antes de enviar para o ambiente de produção, para que não seja um grande problema. Isso pode ser difícil, mas você lê changelogs antes de atualizar suas dependências e escreve testes de unidade, certo? Ou você usa ou cria uma biblioteca, quanto mais estrita a digitação puder fornecer, melhor para seu código e outros programadores.

A comunicação back-end-front-end é um pouco mais difícil. Muitas vezes é fracamente acoplado, então a mudança de um lado pode ser feita facilmente sem estar ciente de como isso afetará o outro lado. A mudança no back-end geralmente pode quebrar suas suposições no front-end e ambos geralmente são distribuídos separadamente. Tem que acabar mal. Somos apenas humanos e às vezes acontece que não entendemos o outro lado ou esquecemos de contar a eles sobre essa pequena mudança. Novamente, isso não é um grande problema com a distribuição adequada da rede – a resposta de decodificação falhará e sabemos como lidar com isso. Mesmo o melhor código de decodificação pode ser afetado por um design ruim…

Funções parciais. Projeto ruim.

“Teremos aqui duas variáveis ​​booleanas: 'isActive' e 'canTransfer', claro que você não pode transferir quando não estiver ativo, mas isso é apenas um detalhe.” Aqui começa, nosso projeto ruim que pode bater forte. Agora alguém fará uma função com esses dois argumentos e processará alguns dados com base nele. A solução mais simples é… apenas travar em um estado inválido, isso nunca deve acontecer, então não devemos nos importar. Nós até nos importamos às vezes e deixamos algum comentário para corrigir depois ou para perguntar o que deve acontecer, mas pode ser enviado eventualmente sem concluir essa tarefa.

 // pseudo-código
function doTransfer(Bool isActive, Bool canTransfer) {
  If ( isActive e canTransfer ) {
    // faz algo para transferência disponível
  } else if ( não isActive e não canTransfer ) {
    // faz algo para transferência não disponível
  } else if ( isActive e não canTransfer ) {
    // faz algo para transferência não disponível
  } else { // também conhecido como ( não isActive e canTransfer )
    // existem quatro estados possíveis
    // isso não deve acontecer, a transferência não deve estar disponível quando não ativa
    batida()
  }
}

Este exemplo pode parecer bobo, mas às vezes você pode se pegar nesse tipo de armadilha que é um pouco mais difícil de detectar e resolver do que isso. Você terminará com algo chamado função parcial. Esta é uma função que é definida apenas para algumas de suas possíveis entradas ignorando ou travando com outras. Você deve sempre evitar funções parciais (observe que em linguagens tipadas dinamicamente a maioria das funções pode ser tratada como parcial). Se o seu idioma não puder garantir o comportamento adequado com verificação de tipo e análise estática, ele poderá travar após algum tempo de maneira inesperada. O código está em constante evolução e as suposições de ontem podem não ser válidas hoje.

Falha rápido. Falha com frequência.

Como você pode se proteger? A melhor defesa é o ataque! Existe este belo ditado: “Falhe rápido. Falhe com frequência.” Mas não acabamos de concordar que devemos evitar falhas de aplicativos, funções parciais e design ruim? O Erlang OTP oferece aos programadores uma vantagem mítica de que ele se cura após estados inesperados e atualiza durante a execução. Eles podem pagar isso, mas nem todos têm esse tipo de luxo. Então, por que devemos falhar rápido e com frequência?

Em primeiro lugar, encontrar esses estados e comportamentos inesperados . Se você não verificar se o estado do seu aplicativo está correto, pode levar a resultados ainda piores do que travar!

Em segundo lugar, para ajudar outros programadores a colaborar na mesma base de código . Se você está sozinho em um projeto agora, pode haver outra pessoa depois de você. Você pode esquecer algumas suposições e requisitos. É bastante comum não ler a documentação fornecida até que tudo funcione ou não documentar métodos e tipos internos. Nesse estado, alguém chama uma das funções disponíveis com um valor inesperado, mas válido. Por exemplo, digamos que temos uma função 'wait' que recebe qualquer valor inteiro e espera por essa quantidade de segundos. E se alguém passar '-17' para ele? Se não travar imediatamente depois de fazer isso, pode resultar em alguns erros graves e estados inválidos. Ele espera para sempre ou não?

A parte mais importante da falha intencional é fazê-lo graciosamente . Se você travar seu aplicativo, você deve fornecer algumas informações para permitir um diagnóstico. É muito fácil quando você está usando um depurador, mas você deve ter alguma maneira de relatar falhas no aplicativo sem ele. Você pode usar sistemas de registro para manter essas informações entre os lançamentos de aplicativos ou examiná-las externamente.

A segunda parte mais importante do travamento intencional é evitar isso no ambiente de produção…

Não falhe. Sempre.

Você enviará seu código eventualmente. Você não pode torná-lo perfeito, muitas vezes é muito caro até mesmo pensar em fazer garantias de correção. No entanto, você deve garantir que ele não se comporte mal ou falhe. Como você pode conseguir isso, já que já decidimos travar rápido e com frequência?

Uma parte importante do travamento intencional é fazê-lo apenas em ambientes de não produção . Você deve usar asserções que são eliminadas nas compilações de produção do seu aplicativo. Isso ajudará durante o desenvolvimento e permitirá detectar problemas sem afetar os usuários finais. No entanto, ainda é melhor travar algumas vezes para evitar estados de aplicativos inválidos. Como podemos conseguir isso se já fizemos funções parciais?

Torne os estados indefinidos e inválidos impossíveis de representar e faça fallback para os válidos de outra forma. Isso pode parecer fácil, mas requer muito pensamento e trabalho. Não importa o quanto seja, é sempre menos do que procurar bugs, fazer correções temporárias e… incomodar os usuários. Isso tornará automaticamente algumas das funções parciais menos prováveis ​​de acontecer.

 // pseudo-código
function doTransfer(Estado de estado) {
  interruptor (estado) {
    caso State.canTransfer {
      // faz algo para transferência disponível
    }
    case State.cannotTransfer {
      // faz algo para transferência não disponível
    }
    case State.notActive {
      // faz algo para transferência não disponível
    }
    // É impossível representar a transferência disponível sem estar ativo
    // existem apenas três estados possíveis
  }
}

Como você pode tornar os estados inválidos impossíveis? Vamos escolher dois dos exemplos anteriores. No caso de nossas duas variáveis ​​booleanas 'isActive' e 'canTransfer' podemos alterar essas duas para uma enumeração única. Representará exaustivamente todos os estados possíveis e válidos. Mesmo assim, alguém pode enviar variáveis ​​indefinidas, mas isso é muito mais fácil de lidar. Será um valor inválido que não será importado para o nosso programa ao invés de um estado inválido sendo passado para dentro tornando tudo mais difícil.

Nossa função de espera também pode ser melhorada em linguagens fortemente tipadas. Podemos fazê-lo usar apenas inteiros sem sinal na entrada. Isso por si só resolverá todos os nossos problemas, pois argumentos inválidos serão removidos pelo compilador. Mas e se sua linguagem não tiver tipos? Temos algumas soluções possíveis. Primeiro – apenas travar, esta função é indefinida para números negativos e não faremos coisas inválidas ou indefinidas. Teremos que encontrar uso inválido dele durante os testes. Testes unitários (que devemos fazer de qualquer maneira) serão muito importantes aqui. Segundo – isso pode ser arriscado, mas dependendo do contexto pode ser útil. Podemos fazer fallback para valores válidos mantendo asserções em compilações de não produção para corrigir estados inválidos quando possível. Pode não ser uma boa solução para funções como esta, mas se fizermos um valor absoluto de inteiro, evitaremos falhas no aplicativo. Dependendo da linguagem concreta, também pode ser uma boa ideia lançar/levantar algum erro/exceção. Pode valer a pena recorrer, se possível, mesmo quando o usuário vê um erro, é uma experiência muito melhor do que travar.

Vamos dar mais um exemplo aqui. Se o estado dos dados do usuário em seu aplicativo front-end estiver prestes a ser inválido em alguns casos, pode ser melhor forçar um logout e obter dados válidos novamente do servidor em vez de travar. O usuário pode ser forçado a fazer isso de qualquer maneira ou pode ser pego dentro de um loop de travamento sem fim. Mais uma vez – devemos afirmar e travar em tais situações em ambientes de não produção, mas não permita que seus usuários sejam testadores externos.

Resumo

Ninguém gosta de aplicativos travando e instáveis. Nós não gostamos de fazê-los ou usá-los. Falhar rapidamente com asserções que fornecem diagnóstico útil durante o desenvolvimento e testes detectará muitos problemas cedo. O fallback em estados válidos em produção tornará seu aplicativo muito mais estável. Tornar estados inválidos irrepresentáveis ​​eliminará toda uma classe de problemas. Dê a si mesmo um pouco mais de tempo para pensar antes do desenvolvimento sobre como remover e recorrer a estados inválidos e um pouco mais durante a escrita para incluir algumas asserções. Você pode começar a melhorar seus aplicativos hoje mesmo!

Consulte Mais informação:

  • Projeto por contrato
  • Tipo de dados algébricos