Última atualização:

O princípio do Samurai 🤺

Nawarian
Nawarian principios

Em programação é recorrente encontrar pessoas discutindo sobre qual a melhor forma de escrever esta ou aquela função, que tipo de código é mais elegante e etc.. Mas muitas dessas discussões são baseadas em opiniões e gostos pessoais, que usam argumentos circulares e travam o debate.

Por isso diferentes princípios de programação e de design foram estabelecidos, de forma a encurtar discussões sobre gostos e boas práticas. Dentre os princípios mais famosos estão o SOLID, DRY, KISS e YAGNI.

o princípio do Samurai não é tão conhecido, especialmente para quem se acostumou a programar em linguagens mais modernas. Mas é uma sabedoria anciã que vem lá da época do C, que eu acho que toda pessoa desenvolvedora deveria carregar consigo.

O que é o princípio do Samurai?

Nós, aqui do ocidente, gostamos de inventar misticismo sobre as coisas que não entendemos do oriente e, com certeza, o Samurai é uma delas. Mas em programação, o princípio do Samurai não tem nada a ver com o Bushidô!

O princípio do Samurai é um princípio de design de software que diz “retorne com sucesso, senão nem mesmo retorne”, baseado naquela mística da honra ilibada dos Samurais. Que é um jeito bonitinho de dizer que devemos manter um único tipo de retorno para nossas rotinas, funções e métodos.

Eu aprendi sobre este princípio lendo o livro Fluent C, que trás diversos padrões e boas práticas para programas escritos na linguagem C com exemplos. Eu definitivamente recomendo a leitura!

“Sempre retorne com sucesso, senão nem mesmo retorne” 🤺

Este princípio ressoa bastante com a ideia de early return, mas a principal diferença é que não queremos apenas retornar o quanto antes: queremos interromper a execução do programa caso algo esteja errado.

E apesar de ser muito útil em linguagens como C, este princípio se aplica e deveria estar fortemente presente em linguagens mais modernas como o PHP, Rust e Golang.

Mas como diachos a gente chegou num princípio desse e como se aplica a software? Vem comigo que eu tenho muita explicação e exemplo de código pra você!

Contexto e história: linguagem C

Na linguagem C é fundamental saber lidar com ponteiros e fazer alocações dinâmicas. Acontece que algumas operações com ponteiros e memória podem falhar por circunstâncias do sistema operacional: alocar memória para ler um arquivo pode causar um estouro de pilha, receber um dado por rede por falhar por perda de conexão e etc..

No caso de linguagens como C, estas situações podem criar o famoso NULL Pointer ou ponteiros nulos. Variáveis que deveriam possuir um conteúdo mas não possuem por algum motivo.

No exemplo a seguir, nós vamos extrair palavras-chave de um texto. Preste atenção no que fazemos com a variável handle que é um ponteiro que representa o arquivo de texto que estamos lendo.

Utilização e tratamento de um ponteiro potencialmente nulo (condição IF linha 7)
Utilização e tratamento de um ponteiro potencialmente nulo (condição IF linha 7)

A transcrição da imagem acima está aqui:

string_t *fetch_keywords(string_t filename)
{
FILE *handle;
string_t *keywords;
handle = fopen(filename, "r");

if (handle != NULL) {
keywords = _fetch_keywords_from_file(handle);
fclose(handle);
return keywords;
}

return NULL;
}

Você não precisa ser especialista em C pra entender o que tá acontecendo. Vem que eu te explico:

A função fetch_keywords() têm como retorno o tipo string_t*: o ponteiro para um vetor de strings. E recebe como parâmetro uma única string chamada filename, que indica qual arquivo deverá ser aberto para leitura.

Para abrir o arquivo nós usamos a função fopen() que retorna um ponteiro do tipo FILE*. Caso fopen() encontre e consiga abrir o arquivo para leitura, o ponteiro FILE *handle passa a ter um valor lá dentro. Senão, caso um erro tenha ocorrido ao abrir o arquivo, FILE *handle passa a ter o valor NULL.

É importante então sempre testar se os ponteiros de arquivo são nulos ou não, porque acessar um ponteiro nulo cria um erro fatal em sua aplicação. Aqui um exemplo de como acessar um ponteiro nulo pode causar um segmentation fault:

O ponteiro FILE * é nulo e ao chamar fread() com este ponteiro nulo, a aplicação gera um Segmentation fault.
O ponteiro FILE * é nulo e ao chamar fread() com este ponteiro nulo, a aplicação gera um Segmentation fault.

Agora, repare que o retorno da nossa função fetch_keywords() é um ponteiro para um vetor de strings (string_t *). Este ponteiro também pode ser NULL, para indicar que não encontramos nenhuma palavra-chave no arquivo.

O problema disso é que esta função obriga as funções que a chamam a também verificarem se o retorno é nulo ou não. Com isso, dois problemas aparecem:

  1. A gente NUNCA consegue garantir que seus usuários estão utilizando seu código da forma esperada;

  2. O montante de código duplicado é grande a deixa o código totalmente bagunçado.

Portanto o ideal, e o que o princípio do Samurai diz, é que você sempre deve retornar apenas em caso de sucesso. Caso não obtenha sucesso, você não pode sequer retornar. Você deve interromper a execução do programa imediatamente.

Assim:

Ao utilizar assert() não é mais necessário escrever vários IF statements.
Ao utilizar assert() não é mais necessário escrever vários IF statements.

A transcrição do código na imagem acima é a seguinte:

string_t *fetch_keywords(string_t filename)
{
  FILE *handle;
  string_t *keywords;
  handle = fopen(filename, "r");
  
  // Se handle for NULL, o programa pára por aqui
  assert(handle != NULL && "Could not open keywords file");

  keywords = _fetch_keywords_from_file(handle);
  fclose(handle);
  return keywords;
}

Aquele assert() no meio da função vai interromper o programa completamente caso a expressão ali não retorne TRUE. Desta forma você diminui o número de IFs aqui e em todos lugares que chamam esta função.

Como aplicar o princípio do Samurai em linguagens modernas?

Linguagens mais modernas do que o C como o C++, Java e PHP possuem um mecanismo de gerenciamento e propagação de erros chamado Exceções ou Exceptions.

A ideia é que um pedaço de código pode lançar uma exceção utilizando a palavra-chave throw e a execução do stack frame atual é interrompida imediatamente, retornando o stack frame superior. E caso não exista nenhum mecanismo para tratar a exceção, ela continua se propagando até encontrar algum stack frame que a capture ou até a aplicação ser interrompida.

Aqui um exemplo de exceção utilizando PHP:

Em PHP podemos lançar e capturar exceções específicas, para controlar o fluxo do programa.
Em PHP podemos lançar e capturar exceções específicas, para controlar o fluxo do programa.

A transcrição do código da imagem acima é a seguinte:

function a(bool $x): void
{
  try {
    b($x);
  } catch (EntityNotFoundException $e) {
    echo $e->getMessage();
  }
}

function b(bool $x): void
{
  if ($x === true) {
    throw new InvalidArgumentException("Outro tipo de exceção");
  }
  
  throw new EntityNotFoundException("Mensagem da minha exception");
}

a(false);
a(true);
echo 'Nunca alcancaremos esta linha!';

No exemplo acima, a chamada a(false) causa a função b() lançar uma exceção do tipo EntityNotFoundException, que é explicitamente capturada no escopo da função a(). Portanto toda vez que uma exceção deste tipo é lançada, a() apenas faz um echo para jogar a mensagem da exceção na tela e seguir seu rumo normalmente.

Quando chamamos a(true) nós forçamos que b() lance uma exceção de outro tipo, que não é capturada no stack frame da função a(). De fato, ninguém na aplicação tenta capturar aquela exceção, que acaba por interromper o código completamente.

🏮 Em linguagens modernas, a forma mais fácil de adotar o princípio do Samurai é lançando exceções👌

Algumas linguagens modernas não possuem exceções, como é o caso do Rust e Golang. Apesar de o próprio ecossistema da linguagem te forçar a lidar com casos de erro, a saída para quando não se consegue lidar com determinado fluxo de exceção é interromper a execução do programa.

Portanto caso a sua linguagem não possua o mecanismo de exceção (exceptions) você pode sempre aplicar o drástico exit em sua aplicação, que força o interrompimento do programa como um todo.

No caso do Golang você pode utilizar o os.Exit, enquanto o Rust possui vários mecanismos diferentes: o std::process::exit que faz a mesma coisa que o Golang faz, mas também possui diferentes macros e métodos que podem encerrar a execução do programa completamente como é o caso das macros assert e panic.

Exemplo de interrupção do programa utilizando a macro panic! do Rust.
Exemplo de interrupção do programa utilizando a macro panic! do Rust.

A transcrição do programa acima é a seguinte:

fn main() {
  panic!("Oh noez!");
}

Até a próxima

Espero que este pequeno artigo-resposta tenha sido útil pra ti e, no mínimo, aguçado sua curiosidade sobre o tema. Todos os princípios de software têm um motivo para existir e, mais importante do que entender o princípio em si, é entender como se aplica e em quais contextos.

Não se esqueça de compartilhar este artigo em suas redes sociais e com seus colegas de trabalho. E também pega pra ouvir Samurai do Djavan, que esta musica e artigo vão ficar na sua cabeça pelo menos por 2 dias 🤣

via GIPHY

Comentários