Last Updated:

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.

Já 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

Comments