Blog

  • Gramática e Programação

    Gramática e Programação

    Eu sempre detestei aprender línguas. Só me esforço para aprender o básico de um idioma quando realmente não há outra forma de sobreviver sem ele. Foi assim que acabei aprendendo o suficiente de inglês e português.

    Lembro que, na escola, eu odiava as aulas de português. A única parte que eu realmente curtia era Literatura. Todo o resto era um verdadeiro martírio.

    Mas o destino, com seu senso de humor peculiar, colocou meus dois professores de português como vizinhos de muro. Em anos pares, eu tinha aula com uma professora; em anos ímpares, com um professor.

    Como eu sempre ficava de recuperação nessa matéria, minha mãe acabava pagando aulas particulares — com o professor do outro ano. O resultado? Eu tinha o dobro de aulas de português com os dois vizinhos ao longo do ano inteiro.

    Por conta disso, acabei aprendendo algumas coisas “úteis completamente inúteis”. Por exemplo, sei que toda paroxítona terminada em ditongo crescente era acentuada, mas que, com a nova reforma ortográfica, isso deixou de ser verdade. Se antes “idéia” tinha acento, agora não tem mais.

    Outra coisa que aprendi, à base de muita dor e sofrimento, foi análise sintática. Toda aquela parte que disseca as sentenças e mostra como elas são compostas: os sujeitos e predicados, os verbos com suas “transitividades”, os objetos, e por aí vai. Tudo isso foi entendido, ainda que sem compreender o mais importante: por que, afinal, eu estava aprendendo isso?

    Nessa época, eu já sabia programar e até tinha começado a trabalhar profissionalmente com programação (sim, comecei bem antes da lei permitir). Mesmo assim, eu não conseguia “conectar os pontos” e enxergar as relações entre a gramática e a programação.

    Programação

    Programar é comunicar. E, para comunicar, precisamos de uma linguagem comum entre todos os envolvidos no processo.

    Ao longo dos anos, a computação desenvolveu uma infinidade de linguagens de programação, cada uma com suas próprias características e propósitos. Essas linguagens facilitam a comunicação entre programadores e computadores, além de permitir que os programadores se entendam entre si.

    Por exemplo, qualquer programador que conheça Python e um computador com um interpretador Python será capaz de entender e executar códigos escritos nessa linguagem.

    Assim como os idiomas e linguagens naturais, as linguagens de programação possuem gramática. Elas se preocupam com sintaxe, semântica, pragmática, entre outros aspectos que fazem parte do estudo das línguas. Além disso, também possuem um léxico e vocabulário próprios que, se não estão estritamente sob o guarda-chuva da gramática, fazem parte do domínio linguístico.

    Python, assim como muitas outras linguagens de programação, é uma linguagem imperativa. Isso significa que você escreve comandos para instruir “como” as coisas devem acontecer. Nesse paradigma, o foco está em ações e etapas específicas.

    Por outro lado, linguagens que seguem outros paradigmas — como funcional, declarativo, lógico ou de marcação — não se preocupam tanto com o “como”, mas sim com o “o quê” deve ser feito.

    Aqui é possível traçar um paralelo interessante entre o paradigma imperativo na programação e o modo verbal imperativo na língua portuguesa: ambos lidam com comandos e ordens. No português, a conjugação dos verbos no imperativo é usada para instruir ou demandar ações — exatamente o que fazemos ao escrever código em uma linguagem de programação imperativa.

    Tá, e daí?

    Se programar é comunicar e usamos linguagens para transmitir algo, será que os conhecimentos de gramática de uma linguagem natural (português, inglês, alemão, etc.) podem ajudar a melhorar o nosso código?

    Eu acredito que sim. E, a seguir, vou listar algumas práticas que adoto ao desenvolver meus projetos.

    Vale lembrar: estou falando de projetos de software escritos em linguagens de programação imperativas, não de uma obra literária. As sugestões que vou apresentar podem melhorar o seu código, mas provavelmente seriam péssimas escolhas para seu conto, romance ou artigo acadêmico.

    Sentenças simples

    Escrever sentenças simples facilita a compreensão. A estrutura mais básica de uma sentença segue o padrão:

    Sujeito — Verbo — Complemento

    O sujeito pode assumir várias formas, mas geralmente é representado por um substantivo (nome/noun). Em linguagens orientadas a objeto, o sujeito é (quase) sempre um objeto. Assim, uma sentença simples em código poderia ser algo como:

    objeto.verbo(complemento)  # ou...
    objeto.verboComplemento()

    Parece óbvio, não? Sim, é básico, mas dito dessa forma, fica claro que os métodos de um objeto devem necessariamente ser verbos, e que os parâmetros passados ao método funcionam como complementos — nos moldes da análise sintática da transitividade dos verbos:

    • Verbo Transitivo: Requer um complemento (objeto direto ou indireto) para completar seu sentido.

    Exemplo: “Ela comprou um livro.” (complemento: “um livro”).

    • Verbo Intransitivo: Não precisa de complemento, pois possui sentido completo por si só.

    Exemplo: “Ela dormiu.”

    Se o verbo usado no nome do método for transitivo, é importante lembrar que ele exige um complemento para que a ação faça sentido. Essa escolha ajuda a tornar o código mais intuitivo e compreensível.

    Single Responsibility

    Observe que a sentença usada como exemplo contém apenas um verbo. Mantendo essa estrutura, também garantimos que cada método realiza apenas uma ação:

    1 verbo → 1 ação → 1 responsabilidade

    Trabalhar dessa forma nos ajuda a garantir que nossos métodos respeitem o Princípio da Responsabilidade Única (Single Responsibility Principle – SRP), que estabelece que cada método ou função deve ter apenas uma responsabilidade clara e bem definida.

    Interfaces REST HTTP

    Interfaces são um meio de disponibilizar o acesso a componentes do seu sistema. Existem diversos tipos de interfaces que podemos utilizar, e uma delas são as APIs REST, que usam o protocolo HTTP.

    O protocolo HTTP trabalha de forma stateless: um cliente faz uma requisição (request) para um recurso/documento/objeto em um servidor, e o servidor retorna uma resposta (response) para o cliente. O formato de uma requisição HTTP é aproximadamente assim:

    METHOD /url
    Header1: ...
    HeaderN: ...
    
    <payload>

    Alguns dos METHODs disponíveis são: GETPOSTPUTPATCHDELETE, etc. Perceba que todos os métodos são verbos, ou seja, indicam ações requisitando algo do servidor. Seguindo nossa lógica gramatical, podemos inferir que a /url é o complemento (geralmente representado por um substantivo).

    Como mencionado anteriormente, é importante manter as sentenças simples, ou seja, com apenas um verbo por requisição. Uma boa interface deveria seguir este padrão:

    GET /users/1
    POST /users
    DELETE /users/c0ffe

    No entanto, é comum encontrar requisições que apresentam dois verbos na mesma sentença:

    POST /contracts/1/remove
    GET /documents/2/cancel

    Percebe como isso soa estranho? Casos como esse geralmente aparecem quando a ação envolve processos mais complexos. Por exemplo, cancelar um contrato pode exigir várias etapas, e um simples DELETE /contracts/1 pode não ser suficiente. Quando isso acontece, uma boa prática é transformar o segundo verbo em um substantivo, tratando o processo como um objeto.

    Por exemplo, ao invés de usar algo como:

    POST /contracts/1/cancel  # verbo + verbo

    Eu prefiro reestruturar assim:

    POST /contracts/1/cancellation  # verbo + substantivo

    Essa abordagem não só torna a requisição mais natural, como também reflete melhor a lógica gramatical e mantém a consistência da interface.

  • Economia dos Testes

    Economia dos Testes

    No mundo dos testes automatizados, os testes são classificados por níveis de validação. Testes que validam uma pequena unidade de código são chamados de testes unitários (unit tests). Quando validam a integração entre componentes, são chamados de testes de integração (integration tests).

    Esses dois tipos de testes são os mais comuns. No entanto, também existem testes que simulam a interação real do usuário com a aplicação, conhecidos como testes end-to-end.

    Cada tipo de teste gera custos de desenvolvimento, manutenção e execução ao longo da vida de um sistema. Em linhas gerais, quanto mais simples e pequeno um teste, mais barato é escrevê-lo e mantê-lo. Por exemplo, testes unitários são mais baratos que testes de integração.

    O tempo de execução dos testes varia e isso também influencia no custo dele. Testes unitários são rápidos, enquanto testes funcionais ou end-to-end demoram mais devido à etapa de preparação que costuma ser bem longa.

    Testes bem escritos custam mais para serem desenvolvidos, mas reduzem os custos de manutenção. O oposto também é verdade: testes mal escritos podem até ser mais baratos para serem escritos, mas custam caro para serem mantidos.

    Os testes unitários são fáceis de escrever e manter se o código estiver bem desacoplado, pois testam apenas uma pequena unidade de código. Testes de integração, embora mais caros, garantem que a comunicação entre partes do sistema funcione corretamente sem que o tempo de execução aumente.

    Com os testes funcionais ou testes end-to-end avaliam a capacidade do software de resolver problemas reais. Como o nome diz eles testam se o software “funciona”. Embora desejáveis, são difíceis de escrever corretamente e são os mais caros de desenvolver, manter e executar.

    Entendendo a relação entre o custo e os benefícios que cada tipo de teste trás, como distribuir o “orçamento” que você tem para escrever testes?

    Minha estratégia

    Minha estratégia favorita é escrever muitos testes unitários, alcançando alta cobertura.

    Exemplo: 235 passed in 100s; 99.98% coverage

    Também escrevo testes de integração para cenários ideais (happy path) e de falhas esperadas.

    Exemplo:

    1. Teste acessar perfil público do usuário @publico
    2. Teste falha ao acessar perfil privado do usuário @privado
    3. Teste falha ao acessar qualquer perfil sem autenticação

    Não faria um teste de integração para “Falha ao acessar um usuário com nome inválido” porque um teste unitário já deveria garantir isso. Se a regra mudar, não preciso alterar testes em vários lugares.

    Para os testes funcionais ou end-to-end, foco nos fluxos fundamentais do sistema.

    Exemplo:

    1. Fluxo de Cadastro (sign up): se esse fluxo quebrar, novos clientes não serão convertidos.
    2. Fluxo de Registro (sign in): se esse fluxo quebrar, clientes não conseguirão usar o produto.
    3. Fluxo de Compra (check out): se esse fluxo quebrar, em um e-commerce, não venderíamos.

    Ou seja, escrevo testes funcionais ou end-to-end apenas para garantir que, se algo falhar, a aplicação não se torne completamente inútil.

  • Transpire Qualidade

    Transpire Qualidade

    Na biografia do Steve Jobs escrita pelo Walter Isaacson, tem um trecho em que o biografado fala sobre a influência que o pai dele deixou para ele na criação de produtos.

    Ele conta que o pai gostava de fazer alguns trabalhos de marcenaria por passatempo e que um dia ele estava corrigindo um pequeno defeito na parte interna de um guarda-roupa que ele havia terminado de construir.

    O Jobs vê o pai investindo um esforço enorme naquele conserto, pergunta:

    — Pai, esse defeito está do lado de dentro… ninguém vai ver ou se importar com isso.

    No que o pai dele responde algo como:

    — Eu vi e por isso me importo.

    O mito e as histórias criadas sobre o Steve Jobs mostram que ele sempre foi obsessivo pelos detalhes dos produtos que ele ajudou a criar até os mínimos detalhes internos das placas de circuito impresso.

    Ele fazia isso porque ele entendia que o cuidado com as todos os aspectos do produto “transpiravam” para a qualidade geral dele. Ele também fazia isso porque era um tremendo babaca, mas isso a gente pode discutir em outro momento.

    Seu código transpira

    Várias vezes ao longo da minha carreira esbarrei com empresários, gestores, profissionais da área de produto e até mesmo desenvolvedores que diziam que a qualidade do código não era tão importante quanto o fato do produto estar “funcionando” e “entregando valor para o cliente”.

    Será mesmo? Será que teu cliente sabe que aquele software porcaria que ele está usando poderia ser muito melhor, mais eficiente, mais estável, etc.?

    Vamos ser honestos… vocês não estão de saco cheio de usar software lento e cheio de problemas? Vocês já tentaram comprar passagem aérea recentemente? Fazer um PIX no aplicativo do seu banco? Já tentaram resolver um problema com seu pedido em um site de e-commerce?

    Sabe porque a nossa vida é miserável assim? Porque toda a engenharia de software está mais comprometida em desenvolver software do que refatorar software.

    Afinal de contas, todos os “stakeholders” estão interessados em vender mais, faturar mais, aumentar o “market share” e se, por exemplo, aquele sisteminha de rastreamento de encomenda está mal-feito ou com uns bugs que afetam só 1% dos usuários pode deixar lá.

    O custo de refatorar esse sistema para ele funcionar melhor seria muito alto e esses recursos seriam melhor empregados implementando um sistema de venda de publicidade para aumentar as vendas no site, não é mesmo? E aqueles 1% dos usuários afetados pelo problema já compraram, não é mesmo?

    Para os “stakeholders” é melhor investir em uma funcionalidade que aumenta o faturamento em 5% imediato do que arrumar um sistema que afeta somente 1% do que já foi faturado.

    Aquele pequeno “TODO”

    E se você está pensando que estou falando apenas dos grandes sistemas e serviços, você está enganado. Estou falando daqueles “TODO” e “FIXME” espalhados pelo seu código.

    Estou falando daquele backlog de consertos e refatorações que nunca é priorizado pelo time. Daquele código “vizinho” ao seu que está cheio de problemas e que não será melhorado porque está fora do escopo da sua tarefa.

    Quando eu trabalhava diretamente com software livre e open-source era muito comum você encontrar melhorias para fazer no código conforme você ia desenvolvendo seu próprio código. Se você mandasse seu código sem melhorar o outro código vizinho provavelmente rejeitariam sua contribuição até que você fizesse tudo direito.

    Fazer isso em uma empresa tem se mostrado quase impossível.

  • Publique-se!

    Publique-se!

    Eu produzo conteúdo técnico sobre programação e carreira desde que esse negócio de “produzir conteúdo” se chamava blogar.

    Nunca fui um blogueiro prolífico, mas sempre tentei deixar meus aprendizados e ensinamentos públicos ao longo do tempo. O meu blog pessoal mudou de nome e propósito várias vezes nessa jornada. Ele já se chamou “aCiDBaSe” (o meu ‘nome hacker’ da era IRC), Pythonologia (na minha época “Python Promoter”), Blog da Triveos (quando tive minha empresa) e, hoje, ele é só um humilde blog WordPress com 5 plugins e tema padrão rodando na minha humilde (mas excelente) hospedagem (link com meu referral) compartilhada onde também hospedo meus e-mails longe de provedores que podem decidir fechar minha conta unilateralmente.

    Sempre desejei produzir conteúdo e compartilhar as coisas que aprendi. Mas desenvolver a disciplina para fazer isso com regularidade e uma boa frequência sempre foi um problema.

    Ainda não sou o cara que gostaria de ser e não produzo tudo o que gostaria de produzir, mas recentemente eu comecei a me sentir melhor comigo mesmo nessa tarefa e, como consequência, a disciplina foi melhorando.

    Como sou um nerd que gosta de listas, eu vou enumerar aqui as mudanças que fiz na minha vida e nos meus processos que fizeram tudo ficar melhor e mais fácil:

    Me respeitar

    Eu me sentia mal por não produzir conteúdo. Me pressionava o tempo todo sobre isso e me sentia mal quando não conseguia corresponder às minhas próprias expectativas.

    Quando comecei a trabalhar isso na terapia, eu fui me dando conta que isso não é uma obrigação e que não atender a essa necessidade era algo compreensível. A angústia foi diminuindo e hoje eu estou bem melhor com o volume de coisas que faço.

    Significa que é fácil ou que tiro isso tudo de letra? Nem de perto. Mas quando a angústia e a ansiedade surgem eu consigo buscar a origem dela e trabalhar melhor o problema.

    Remover distrações

    Eu comecei a fazer isso bem antes do meu diagnóstico de TDAH, mas certamente foi um divisor de águas no processo. Mesmo sem saber do TDAH, eu já tinha percebido que uma fonte de impedimentos para minha produção eram as distrações.

    Que tipo de distração? Com ferramentas.

    Yak Shaving

    Toda vez que eu tinha uma ideia sobre algo para postar no blog, eu entrava na interface de admin, atualizava tudo, trocava o tema, tentava exportar o conteúdo para outro CMS, testava YA-ferramenta-geradora-de-sites-estáticos, testava um plugin novo, conferia o SEO e analytics do site… um processo infinito de Yak Shaving.

    Como eu disse acima: hoje eu uso WordPress na minha hospedagem (que oferece one-click-installation de WordPress) no meu domínio onde já rodo meus e-mails. Deixo autoatualização ligada para tudo. Tenho só uns poucos plugins de segurança e backup ligados. Coloco tudo por trás do Cloudflare (pacote gratuito) para ter `https` sem ter que ficar instalando/renovando certificados SSL. E uso o tema padrão do WordPress (que muda nos updates major e eu só aperto “accept” do meu lado).

    Não tenho que criar, habilitar, configurar, etc. nada para colocar o site no ar e publicar meus textos. Nada de GitHub Actions, static file generation, S3, Cloudfronts, CDNs, etc. (exceto o Cloudflare por comodidade para ter https).

    Também não tenho Analytics instalados exceto por um bem básico que vem no plugin Jetpack e um gerado pelo Cloudfront.

    A ideia é tornar o processo tão simples quanto clicar em “Novo Post”, escrever, e clicar em “Publicar”.

    Conforto

    Eu sempre gostei de escrever. Texto mesmo. Escrevi no meu blog, no meu livro, nas minhas redes sociais, etc. Acho que, por gostar de ler, eu sempre gostei de escrever. Na infância até ganhei uns concursos de redação do jornal da cidade.

    Mas senti que as novas gerações não estavam mais muito ligadas em conteúdo texto. Decidi me aventurar em novos meios: vídeos.

    Investi uma grana pesada porque eu sou (burro) assim: se é para fazer algo, tem que ser perfeito.

    Foto do meu escritório onde podemos ver alguns spots de iluminação e equipamento de stream como microfone, câmera, etc
    Estúdio “profissional” com equipamento de primeira linha (na foto não aparece mas tem câmera profissional, GoPro, DJI Osmo e até um Drone eu comprei)

    Até a chácara da família foi desenhada para ser usada como um estúdio para conteúdos mais “maker” que eu pretendia (ainda pretendo) fazer.

    Foto de um galpão em construção
    Futuro estúd… digo… chácara da família.

    Mas deu tudo errado. Eu descobri que não gosto de produzir vídeos. As únicas coisas dos meus vídeos que me deixavam satisfeitos eram os roteiros. Porque eram textos. Depois dessa etapa era tudo horrível. Principalmente a edição (eu odeio editar video…)

    Dei um pause (ainda não sei se é definitivo) no canal. É provável que eu volte a produzir coisas ali só quando eu quiser mostrar meus aspectos mais “make“. E quando eu tiver condições de pagar alguém para editar (repito: eu odeio editar video).

    Eu me sinto confortável com texto. É com ele que eu vou trabalhar. Se as pessoas não curtem texto é uma pena. Mas não vou sair do meu conforto para elas. E já tem muita gente boa fazendo video.

    Ótimo é inimigo do bom

    Eu sou muito chato com o que escrevo. Tudo tem que estar perfeito, completo, com referências, revisado, … Impecável. Isso é um baita muro na minha produção.

    Ter começado a publicar coisas incompletas, com erros (que são corrigidas quando apontadas), ou com a qualidade “mais-ou-menos-para-mais” e mesmo assim receber feedback positivo da audiência foi libertador para mim.

    Vencer meu preconceito com certas ferramentas, e nem estou falando de IA (ainda), me tornou muito mais produtivo.

    Ainda não consegui estudar o suficiente para adotar uma ferramenta de IA no meu fluxo de trabalho, mas é quase certo que vou adotar uma logo. Mais para me ajudar na revisão (e edição) do que na autoria. Por uma questão filosófica, eu (ainda) tenho um certo preconceito com o uso desses geradores na autoria do meu conteúdo (ele seria meu ainda?).

    Eu já falei um pouco sobre isso em um post aqui mesmo no LinkedIn. Lá eu falo sobre usar o LanguageTool para revisar e eliminar erros mais básicos do texto.

    Seu conteúdo na sua casa

    Sempre que o conteúdo que eu produzo se apresenta de uma forma relevante, eu publico ele no meu blog, no meu domínio, na minha hospedagem (ok, não é minha, mas eles não são uma big-tech que ganha dinheiro com meu trabalho).

    Esse artigo foi publicado inicialmente no LinkedIn, mas logo coloquei ele aqui também. Na verdade, esse é o primeiro artigo que escrevo primeiro no LinkedIn e só fiz isso porque queria conhecer melhor a ferramenta deles (curti).

    Mas a minha recomendação é: tudo o que você produz deveria estar na sua “casa”. E a sua casa deveria ser uma casa própria.

    Publique e mantenha seu conteúdo no seu site, na sua hospedagem, no seu domínio, sob suas regras e sob seu controle.

    Conhecimento precisa ser livre

    Esse item se relaciona um pouco com anterior porque quando produzimos um conteúdo em uma plataforma proprietária como aqui, Medium, Twitter, YouTube, Spotify, etc. a gente entrega o nosso trabalho para um terceiro que passa a ter mais controle do que você sobre esse trabalho.

    Não se aprisione. Use essas redes e plataformas para referenciar ou até mesmo replicar o seu trabalho.

    Não recomendo muito a replicação por questões de SEO e “findability“, mas se você estiver buscando uma presença nessas plataformas a replicação pode ser uma boa estratégia.

    E, quando estiver com seu conteúdo no ar, torne ele facilmente compartilhável. Se você faz seu conhecimento facilmente compartilhável, ele alcança mais pessoas e circula mais livremente.

    Mas ele tem que ser compartilhável por meios abertos e com padrões abertos também. Não somente em redes fechadas e proprietárias.

    RSS (Really Simple Syndication)

    A maneira que eu defendo (e promovo/agrego) o compartilhamento do seu conteúdo é através do RSS. Mas eu só considero um RSS correto quando ele é completo e não somente um resumo com um link de “leia mais/read more”.

    Aaron Swartz, grande ativista do compartilhamento livre, um dos criadores do RSS

    Todos os sistemas de publicação sérios suportam RSS (ou Atom que dá no mesmo).

    Disponibilizar seu conteúdo por RSS/Atom é mais do que só compartilhar. É um ato político no sentido de retomar a Internet das mãos das grandes empresas de tecnologia.

    O formato RSS foi co-criado pelo ativista Aaron Swartz, que acreditava e defendia a liberdade e a democratização do acesso ao conhecimento. Ele morreu por isso.

  • Nem certo e nem errado

    Nem certo e nem errado

    Se tem uma área do conhecimento onde os debates são quase sempre bem acalorados, essa área é a do desenvolvimento de software. Tem todo tipo de disputa, das mais bobas como Tab vs. Espaço, temas claros ou escuros, 80 colunas ou não, etc. até as mais complicadas: tipos estáticos ou dinâmicos (ou qualquer variante doida), OO ou funcional, código aberto ou software livre, etc.

    Em comum com todas essas discussões é a tendência entre os debatedores de apelar para conclusões binárias: “é isso aqui ou está errado.”

    Essa postura também aparece quando as pessoas que estão habituadas a um conjunto de conceitos precisa se adaptar a outros conceitos. Um exemplo: a pessoa que usa Windows (ou Linux) reclamando do botão de maximizar janela do macOS. Ela com certeza vai reclamar e dizer que aquilo está errado. Mas se você lembrar que a primeira empresa a lançar uma interface baseada em janelas para o grande público foi a Apple e só depois o Windows e o Linux surgiram fazendo diferente, quem está “errado” nessa história?

    No meio do desenvolvimento de software, talvez porque computadores eletrônicos que usamos sejam binários, adoramos a segurança das respostas absolutas. Isso é certo. Aquilo é certo. Isso é errado. Aquilo é errado.

    No mundo Python onde eu trabalho tem até um termo (que eu detesto) para isso: pythônico. Todo mundo ouve, mas ninguém sabe o que é.

    Mas o tempo me ensinou que as coisas podem simplesmente “ser”. Nem certas e nem erradas. Provavelmente diferentes, provavelmente similares.

    Indiferente das alternativas se parecerem ou não, uma coisa é certa: cada uma das opções tem os seus pontos favoráveis (prós) e seus pontos desfavoráveis (contras). E, quando escolhemos uma opção, temos que renunciar às outras. É a tragédia (e a beleza) da vida.

    Entrevista de emprego

    Toda essa introdução serviu para falar sobre uma coisa que eu sempre faço quando estou entrevistando um candidato para uma vaga: perguntas específicas com poucas alternativas de respostas onde nenhuma das alternativas é certa ou errada.

    Vou dar um exemplo com a minha famosa pergunta sobre ORMs.

    Um ORM (Object-Relational Mapper) são softwares que servem para conectar o mundo dos objetos em um sistema orientado a objetos aos registros (linhas, tuplas, etc.) de um banco de dados relacional.

    Esse tipo de software é bem complicadinho de se escrever porque o modelo relacional tem algumas diferenças fundamentais com o modelo orientado a objetos e lidar com essas diferenças sempre força o desenvolvedor a fazer certas escolhas em detrimento de outras.

    Não vou detalhar muito essas diferenças e nem as alternativas aqui porque elas não são tão importantes para a pergunta e nem para a resposta, mas recomendo a leitura dos artigos que falam sobre esse tópico. Certamente vai te tornar um programador melhor.

    Quando estou com o candidato eu mando um diálogo parecido com esse:

    — Você sabe o que é um ORM?
    — [se o candidato disser que não, eu pulo para outra pergunta parecida, senão eu prossigo… se o candidato estiver meio nervoso, eu também aviso que a próxima pergunta não tem uma resposta certa e nem errada e que eu só quero entender como ele pensa sobre programação orientada a objetos].
    — Suponha que eu te peça para implementar um ORM em Python para usarmos em nosso sistema e eu te mostre dois ORMs diferentes para você se inspirar (ex. Django, SQLAlchemy). Tudo bem?
    — Sim… entendo…
    — No ORM do Django, se eu quero persistir um objeto no banco de dados, eu faço: modelo.save(). Mas no SQLAlchemy, se eu quero fazer o mesmo, eu faço algo parecido com storage.add(modelo). Ou seja, no Django o método que persiste o modelo no próprio modelo e no SQLAlchemy a responsabilidade de persistir o modelo é do objeto Storage. Qual desses modos você escolheria?

    E aí o candidato teria algumas opções de resposta:

    1. Não sei, preciso estudar mais as opções: boa resposta, mostra que entrei num assunto que ele desconhece, mas que ele tem interesse em se aprofundar.
    2. Eu faria igual ao Django: resposta boa. Mas me leva à linha de contestar a escolha e perguntar se não seria responsabilidade demais para o objeto modelo cuidar das regras de negócio e da persistência dos dados no banco?
    3. Eu faria igual ao SQLAlchemy: resposta boa também. Mas eu contestaria a escolha perguntando se a opção do Django não seria mais simples e conveniente para o uso mais comum onde precisamos somente gravar um objeto no banco sem ter que se preocupar com storages e afins?
      • Algumas vezes recebi boas respostas onde o candidato implementaria o ORM para funcionar no modo SQLAlchemy, mas implementaria um método helper .save() também nos modelos que faria algo do tipo: storage.add(self). Não posso negar que eu ficava bem feliz com essa resposta porque seria a minha resposta (mostrando que ela é a resposta certa 😛).
    4. Eu faria diferente de ambos e faria [assim]: pode ser uma resposta boa aqui, mas, na prática, eu nunca recebi uma alternativa muito consistente nesses casos. Quase sempre foram respostas que mostravam o desconhecimento do candidato sobre o que é e como um ORM funciona.

    Notem que a pergunta é só um ponto de partida para entender como o candidato pensa e faz as suas escolhas quando está programando. Quando ele faz uma escolha e, portanto, uma renúncia, eu apresento sempre um contraponto à escolha dele para compreender se ele fez essa escolha porque ele julga ela a escolha certa e a outra errada ou se ele, de fato, ponderou as diferenças, prós e contras das opções apresentadas para dar a resposta.

    Profissionais que sempre estão refletindo sobre seu próprio trabalho estão sempre em crescimento. Aqueles que estão presos aos seus dogmas e tradições sempre estarão limitados por conceitos colocados por terceiros como sendo o certo.

  • Código Cabuloso

    Código Cabuloso

    Esse artigo é quase um repeteco dos artigos Dicas para um bom programa em Python e o Personal Python Style Guide. Mas aqui mostro como uso o processo de refatoração do código para estudar e entendê-lo.

    Estou tentando entender um código bem intrincado e importante do trabalho… é um código bem crítico que resolve um problema bem difícil (merge de objetos) e foi desenvolvido “sob demanda”, ou seja, fizeram uma versão básica e foram incrementando ele com cenários diversos que foram sendo descobertos com o tempo.

    Todo mundo na empresa tem medo de mexer com esse código porque, apesar dele ter até uma certa cobertura de testes, não sabemos se esses testes realmente cobrem todos os cenários reais.

    Mas preciso entender esse código para fazer uma otimização (ele executa um UPDATE muito demorado no nosso banco de dados e eu preciso remover esse UPDATE).

    Eu não sei onde esse UPDATE acontece porque o código é todo elaborado com execuções tardias (lazy) das operações. Então preciso ler tudo para entender onde essa operação está sendo agendada para execução.

    Como tenho TDAH é muito difícil, para mim, somente ler o código para entendê-lo. Quando o código é curto e simples tudo bem, mas não é esse o caso. O que eu geralmente faço é um processo de refatoração do estilo do código. Não só em termos de formatação (porque tem ferramentas para isso que já rodam no nosso sistema de integração contínua), mas também em estilo estrutural.

    Vou separando cada bloco de refatoração ou função refatorada em um commit em um branch criado especificamente para esse trabalho.

    Uma vez que terminei tudo e entendi o funcionamento do código crio um PR (em modo draft) com uma descrição detalhada do que fiz, da separação em commits, necessidade de revisar, como revisar, etc. para meus colegas de trabalho avaliarem e até mesmo responder algumas das minhas dúvidas. Mas o mais importante: inicio a descrição do PR explicando que a aceitação dele é totalmente opcional e até mesmo não indicada por conta dos riscos envolvidos.

    Para todo esse processo é imprescindível o uso de uma ferramenta de refatoração automática que possibilite renomear identificadores, extrair funções/métodos, inversão de lógica em ifs, etc. Senão o seu trabalho será miserável.

    Munido de todos os requisitos passo a alterar o código da seguinte maneira…

    Nomes melhores

    Entender um código onde os identificadores são chamados obj, data, tmp, etc. é complicado. Ter identificadores com nomes como “foo_json” que tem um dict() e não uma JSON-string também não ajuda muito.

    Renomeie variáveis, funções, métodos, classes, etc. para terem sempre o nome correto. Se estiver difícil escolher um nome para o identificador é porque o código tem outro tipo de problema ou ainda falta compreensão sobre ele.

    Early Return

    Uso early return pattern para reduzir o volume de indentação do código e o embaraçamento dele (tangled). O objetivo é linearizar os fluxos e criar blocos segmentados com lógica de processamento. Sou um “Never Nester Developer”. 😀

    Como já comentei nesse artigo aqui, de modo bem simplificado, o código abaixo:

    def f(c1, ..., cN):
        if c1 and... cN:
            ... faz um monte de coisas ...
            return 1
        return 0

    Vira algo do tipo:

    def f(c1, ..., cN):
        # cenário excepcional 1
        if not c1:
            return 0
    
        ...
    
        # cenário excepcional N
        if not cN:
            return 0
    
        ... faz um monte de coisas ...
    
        return 1

    O código fica mais longo, mas é possível identificar bloco a bloco quais são as condições excepcionais da função em cada bloco.

    Lembrem-se que o objetivo aqui não é eficiência e sim a legibilidade e compreensão do código.

    Ajustes de if‘s, elif‘s e else‘s

    Nessa etapa o objetivo é eliminar o máximo de if‘s, elif‘s e else‘s do código (e adicionar else‘s quando temos algum elif‘s inevitável).

    Uma forma de fazer isso é inicializando valores em certas variáveis e só modificá-los dentro do if. Mas nem sempre isso basta e, em alguns casos, quando cada bloco é muito grande ou tem chamadas de funções, etc. sequer é possível de ser feito.

    Um exemplo bem simplório só para ilustrar o que estou dizendo:

    if cond:
       v = f()
    else:
       v = default

    Vira algo tipo:

    v = default
    if cond:
        v = f()

    Dessa forma deixo no fluxo normal a condição padrão e crio um branch só para tratar de uma excepcionalidade.

    Essa refatoração pode realmente ficar enorme e o resultado também pode ficar pior que o original, logo, use com moderação.

    Outra refatoração que faço não tem relação somente com a compreensão do código, mas até mesmo com o funcionamento correto dele: se você tem if e elif é prudente ter um else. Mesmo que seja para levantar um erro. Afinal, se você pensou em mais de um cenário, o que acontece com aquele cenário que você não pensou?

    Falo sobre isso abordagem aqui e aqui.

    Extração de código

    Outra refatoração que ajuda bastante a melhorar a legibilidade do código é conhecida como Extract Method. Ela possibilita trocar um trecho de código por uma chamada de função que descreve o que esse código faz.

    Para fazer essa refatoração é bem útil ter uma ferramenta que automatize o processo. A IDE que uso no dia a dia oferece essa refatoração, mas é provável que existam plugins para vários outros editores e IDEs.

    Essa é fácil de ilustrar:

    def f(x, y):
        # Verifica se o objeto x é válido
        valid = False
        if x.a and x.a == 0:
            valid = True
    
        if x.compare(y):
            valid = True
    
        if valid:
            ... faz algo ...

    Vira algo assim:

    def is_valid(x, y):
        if not x.a or x.a != 0:
            return False
    
        if not x.compare(y):
            return False
    
        return True
    
    
    def f(x, y):
        if not is_valid(x,y):
            return
    
        ... faz algo ...

    Quando você está lendo o código de f(x,y) você, sabe que o objeto x é validado primeiro e o resto da função só será executado quando o objeto x for válido.

    Exceções devem ser exceptions

    É muito comum ver funções retornando flags (ex. None) quando ela precisa sinalizar um problema, um erro ou uma exceção.

    Considerando que idealmente uma função (ou método) deve retornar sempre objetos de um mesmo tipo, o retorno de None deveria ser algo ruim, certo?

    Quando retornamos None em nossas funções precisamos ficar verificando todos os valores retornados antes de usá-los, ou seja, toda hora vemos os famigerados:

    ret = f()
    if ret:
       ... faz algo ...
    else:
       ... trata o erro ...

    Em linguagens com suporte a exceções podemos usá-las para sinalizar problemas ou… excepcionalidades!

    try:
        ret = f()
    exception UmaExcecaoBemEspecificaQueFPodeGerar:
        ... trata o erro ...
    
    ... faz algo ...

    Essa refatoração melhora a legibilidade do código porque deixa o tratamento da exceção bem perto do código que pode gerar ela. E para isso ser verdade é importante que o bloco try/except realmente seja pequeno e restrito ao trecho onde a exceção pode acontecer.

    Também é importante que a exceção gerada (e tratada) sejam sempre bem específicas para o erro gerado para evitar tratar o erro inadequadamente.

    Usar bem a linguagem

    O código da empresa onde trabalho é escrito em Python e então eu refatoro ele para ficar mais “pythônico” (o que quer que isso signifique para mim).

    Legibilidade é melhor que eficiência nesse momento.

    Prefiro um belo “if-zão” bem legível a uma “ifexpression” toda muvucada.

    Um loop pode funcionar melhor que um comprehension… estou ajustando o código para ler e entendê-lo e não para que ele rode um femtossegundo mais rápido.

    Tipos

    Tipos (e eventualmente anotação de tipos) podem auxiliar na compreensão do código, bem como as ferramentas de refatoração automática.

    Tente padronizar os tipos de parâmetros e retornos das funções. Tente fazer com que eles sempre recebam e retornem objetos dos mesmos tipos. E lembre-se também que None é do tipo NoneType e não do mesmo tipo dos objetos que você está querendo usar. 😜

    Uma função que busca uma pessoa pelo nome:

    def get_pessoa(name):
       pessoas = Pessoa.filter(name=name)
       if not pessoas:
           return None
       return pessoas[0]

    Ficaria assim:

    def get_pessoa(name: str) -> Pessoa:
        pessoas = Pessoa.filter(name=name)
    
        if not pessoas:
           raise PessoaNaoEncontrada(name)
    
        if len(pessoa) > 1:
           raise MultiplasPessoasComNome(name)
    
        return pessoa

    Namespaces para contextualizar

    Quando esbarro com muitos nomes que vem do mesmo módulo, tento refatorar o uso deles para incluir o nome do módulo de origem no namespace:

    from contants import (
        FOO,
        BAR,
        BAZ,
        QUX,
        DUX,
    )
    
    def f(x):
        if x == FOO: ...
        if x == BAR: ...
        if x == BAZ: ...
        if x == QUX: ...
        if x == DUX: ...

    Vira:

    import constants
    
    def f(x):
        if x == constants.FOO: ...
        if x == constants.BAR: ...
        if x == constants.BAZ: ...
        if x == constants.QUX: ...
        if x == constants.DUX: ...

    Dessa forma trago o contexto de qual módulo os identificadores vem.

    Métodos próximos dos objetos

    Esse faço pouco porque o time onde trabalho não curte “Fat Models” do mesmo jeito que eu gosto.

    Mas essencialmente transformo quaisquer funções auxiliares que lidam especificamente com um tipo de objeto em um método do próprio objeto e tiro da frente especificidades que aquele objeto pode encapsular para mim.

    Mover trechos de código e funções que lidam com um tipo específico de objeto como método do próprio objeto.

    def nome_completo(pessoa):
        return f"{pessoa.primeiro_nome} {pessoa.ultimo_nome}"
    
    def etiqueta(pessoa):
        return {
            "nome": nome_completo(pessoa),
            "endereco": pessoa.endereco,
        }

    Fica:

    class Pessoa:
        @property
        def nome_completo(self):
            return f"{self.primeiro_nome} {self.ultimo_nome}"
    
    def etiqueta(pessoa):
        return {
            "nome": pessoa.nome_completo,
            "endereco": pessoa.endereco,
        }

    Assim, ao analisar a função etiqueta() foco especificamente em como ela funciona sem me distrair com código de concatenação de nomes.

    OOP e não DOP

    Dicionários são estruturas de dados tão poderosas em Python que é bem fácil a gente começar a usá-las para tudo em nosso código. Mas isso começa a se tornar um problema com o tempo porque é quase impossível encapsular os dados de um dicionário com o objetivo garantir a consistência deles.

    Enquanto estou fazendo a refatoração de um código, começo a usar namedtuples, dataclasses ou até mesmo uma classe “convencional” (com métodos e tudo) para substituir os dicionários que ficam espalhados pelo código.

    def grava_pessoa(dados_pessoa: dict) -> int:
        if not valida_dados_pessoa(dados_pessoa):
            raise DadosInvalidos(dados_pessoa)
    
        # se tem 'id' já existe no DB
        if dados_pessoa.get("id"):
            return update(dados_pessoa)  # retorna o id
    
        return insert(dados_pessoa)  # retorna o id

    Ficaria mais ou menos assim:

    class Pessoa:
        def __init__(self, nome, ...):
            self.nome = nome
            ...
    
        def valida(self):
            ...  # valida dados do objeto
    
    def grava_pessoa(pessoa: Pessoa) -> int:  # ou Pessoa
        if not pessoa.valida():
            raise DadosInvalidos(pessoa)
        
        if pessoa.id:
            return update(pessoa)  # retorna id ou Pessoa()
    
        return insert(pessoa)  # retorna id ou Pessoa()

    A lógica de validação do objeto não fica me distraindo do que a função grava_pessoa() faz: inserir ou atualizar os dados da pessoa no banco de dados.

    TODO/FIXME

    Uso comentários TODO/FIXME com dúvidas que não consigo solucionar lendo somente o código que estou mexendo e que não posso esquecer de perguntar para algum colega em algum momento no futuro (lembrem que tenho TDAH e esquecerei as dúvidas que tenho).

    # Ignora registro de pessoas afetados pelo bug de migração
    if 32000 > pessoa.id > 10000:
       return

    Provavelmente vira algo assim:

    # TODO (osantana): o código abaixo ainda é necessário mesmo
                       após a migração?
    # Ignora registro de pessoas afetados pelo bug de migração
    if 32000 > pessoa.id > 10000:
       return

    Estilo de código

    Por último, quando não vejo muita coisa para melhorar no código, eu mudo só algumas coisas menores no estilo de código para forçar algum tipo de formatação pelas ferramentas automáticas de formatação.

    # formatador faz algo assim:
    x = funcao_com_nome_longo_e_muitos_parametros(
            a="foo", b="bar", c="baz", d="qux)

    Coloco uma “,” para forçar a indentação abaixo:

    # formatador faz algo assim:
    x = funcao_com_nome_longo_e_muitos_parametros(
        a="foo",
        b="bar",
        c="baz",
        d="qux,
    )

    Conclusão

    Vocês têm alguma outra coisa que você também faça para poder entender algum código mais cabuloso?

  • Como começar em TI

    Como começar em TI

    Esse artigo é uma adaptação do meu vídeo no YouTube.

    Com bastante frequência eu recebo pedidos de dicas de pessoas que pretendem começar a trabalhar em TI. Por conta disso eu reuni aqui várias dicas e recomendações para essas pessoas.

    Tentei ser bem pragmático no plano. O objetivo dele é otimizar o caminho do zero ao primeiro emprego como iniciante. Eu falo só sobre o início do percurso e espero que consiga te preparar para decidir o restante do trajeto por conta própria.

    Várias dicas que eu vou passar aqui foram acumuladas nos meus vários anos de experiência desenvolvendo software e participando de processos seletivos dos “dois lados do balcão”. É…! Já fui candidato para uma vaga várias vezes na vida e também já fui o contratante que escolhia os candidatos.

    As dicas vão ser mais focadas na carreira de programador, mas algumas podem até servir para quem pretende trabalhar com outras áreas da tecnologia. Então se você pretende trabalhar com Data Science ou DevOps, tenta ver se tem alguma dica útil para você também.

    Aprender programação não é Fácil. Mas é possível.

    Bom galera, a primeira dica que vou dar é super importante e é pesada: não romantizem o trabalho de programador. Aprender a programar não! É! Fácil!

    Balde de água fria, né? Foi mal aí… mas eu não poderia começar essas dicas sem mandar a real para vocês.

    Diferente do que vocês vão ver nas propagandas de cursos e nos discursos de “coaches”, não é fácil aprender a programar. Você tem que estar preparado para investir muito tempo e dedicação nesse processo.

    Mas por favor não me entendam mal! Eu não estou dizendo que você não consegue. Todo mundo consegue aprender a programar. Mas não tem como aprender a programar só lendo uns livros e fazendo uns cursos por aí. Cursos caros ou uma faculdade não te tornarão programador. Programar vai.

    Só a prática deliberada da programação é que pode te tornar um programador.

    Um livro ou um curso pode até te levar pelo começo da estrada e te ensinar como usar as ferramentas de programação, mas você vai ter que trilhar esse caminho por conta própria depois disso.

    Como? Escrevendo muito código! Você precisa escrever muito código no processo porque esse é o único caminho que vai te ensinar a programar.

    E vai ter vezes que vai ser frustrante. Outras vezes vai ser cansativo. Outras vezes vai ser desesperador. E algumas vezes vai dar medo. Mas mesmo assim você vai ter que seguir.

    Mó papo de “coach” isso, né? Mas é a real.

    E tem mais! Os primeiros códigos que você escrever vão ser uma tremenda porcaria… e tá tudo bem! É só jogar ele fora e tentar escrever outro melhor. Essa é a beleza do software, você tem matéria-prima infinita para trabalhar…

    Seguindo por esse caminho, quanto mais tempo, foco e disciplina você tiver, mais rápido você se capacita para tentar uma vaga bacana.

    Ah! E não importa o que aquele “guru de finanças e carreira” te disse: você não vai conseguir um emprego de programador sem se esforçar para isso. Provavelmente não vai ser rápido também. E provavelmente o salário não será no nível que eles prometem.

    Não tem mágica e nem milagre. Se fosse fácil, não teria vaga sobrando, certo?

    Tá, mas você deve estar perguntando: “Ok, mas quanto tempo vai levar para eu conseguir um trampo bacana?”.

    Olha… não tenho uma resposta para essa pergunta. Depende muito de quanto tempo você dedicar todo dia para praticar e de um punhado de sorte.
    Mas se você consegue se dedicar umas duas horas por dia, as coisas provavelmente vão caminhar mais rápido do que se você se dedicar duas horas por semana.

    — Mas… dá para otimizar isso?
    — Sim! Dá! Principalmente se você escolher bem os temas para focar seus estudos. Você não pode desperdiçar tempo com coisas que não serão úteis no início da sua carreira.

    Ainda assim o processo todo pode levar um tempo considerável. E por isso eu gostaria de deixar a segunda dica: você não vai ganhar bem no começo!

    Você não vai ganhar bem

    — Orra! Você disse na abertura do vídeo que as vagas pagam bem e agora diz que a gente não ganha bem?
    — Exato. No começo nem todo mundo ganha bem. Pode ser que você dê sorte, mas via de regra não é o que acontece com a maioria dos candidatos.

    As vagas que pagam bem são aquelas para programadores mais experientes. E… óbvio… você não vai ter experiência no começo.

    Pode acontecer que o salário de um iniciante seja maior que a sua renda atual. Nesse caso o seu salário inicial vai “parecer” alto, mas não necessariamente ele “será” alto.

    Por conta dessa situação, eu daria a terceira dica: se você já tem um trabalho, continue nele.

    Mantenha seu emprego

    Se você já está trabalhando em algum lugar, mantenha esse emprego. Não assuma riscos altos sem motivo. Lembre o que eu disse: a vaga pode demorar para chegar.

    Sei que é bem difícil ter ânimo para estudar programação depois de um dia cansativo de trabalho. Se esse for o seu caso, tente organizar seu dia para ter um tempo antes de começar o expediente. Uma horinha por dia já dá um adianto.

    Se você aproveitar esse tempo, mesmo curto, para estudar computação da melhor maneira, as coisas vão funcionar.

    E isso leva à quarta dica: Como estudar computação.

    Estude

    Como mencionei agora a pouco, se você acha que basta fazer alguns cursos para aprender a programar, você não poderia estar mais enganado.

    Um curso de computação, geralmente, tem um conjunto de assuntos limitado e alguns exercícios prontos para você exercitar seus conhecimentos sobre o conteúdo apresentado.

    No fim do curso você receberá um diploma dizendo que você consegue resolver os exercícios do curso. E eu te pergunto: isso te torna um programador desejável?

    O trabalho de um programador é resolver problemas. Problemas com complexidades distintas, tamanhos diferentes, etc. O mundo real é muito mais complicado que o ambiente controlado de um curso de programação.

    Aprender a programar exige que você procure por problemas que podem ser resolvidos com software e se arriscar a escrever seus próprios programas para solucionar eles.

    Esses programas vão funcionar? É provável que, no início, não. Mas trabalhando em um programa ruim atrás de outro você vai entendendo o quê que funciona e o quê que não funciona.

    Você aprende o que é bom e o que é ruim em contextos diferentes, aprende a encontrar bugs no seu software e resolver esses bugs, quebra a cabeça e se desespera até perceber que uma pausa e um pouco de descanso era tudo o que você precisava para terminar o projeto.

    Ou seja, faça alguns cursos, mas, depois que você concluir alguns deles, tente se arriscar por conta própria e desenvolva seus próprios projetinhos.

    Desenvolver esses projetinhos vai te ajudar agora e mais lá na frente quando algum recrutador pedir para você mostrar seu código para ele.

    Se estiver sem ideia para projetos, é só olhar por perto e ver algumas coisas que você precisa no seu dia-a-dia… tipo um controle de despesas… ou no seu trabalho atual… tipo gerar um relatório no Excel.

    Faça programinhas que te ajude em casa, na escola, no trabalho, no seu hobby, … qualquer coisa. E não se limite, planeje um sistema completo com todas as funcionalidades que você gostaria de ver. Mesmo que você não faça a mínima ideia de como desenvolver elas.

    Você também pode procurar por listas com ideias de projetos na Internet:

    Forme uma rede de suporte

    Enquanto você está nesse processo de aprendizado é sempre legal ter a ajuda de alguém. E é aí que vai a quinta dica: converse com outros programadores.

    Tente sempre participar de eventos e encontros sobre as tecnologias que você está estudando. Comece seu networking dentro das comunidades de tecnologia.

    Você evolui como programador conversando com essas pessoas e já vai cavando as suas oportunidades de trabalho desenvolvendo uma rede de relacionamentos no mercado.

    Uma grande dúvida das pessoas que estão começando é sobre qual linguagem escolher? Qual framework? Backend? Frontend? Data Science?

    A minha sugestão é: forme sua rede de suporte primeiro. Comece com as mesmas ferramentas que eles usam. Nesse momento você deveria estar preocupado em aprender a programar e não com o mercado de trabalho ou onde tem mais oportunidades.

    Se você não souber programar em nada, nenhuma oportunidade servirá para você.

    Foco

    E pensando em comunidade, programadores e networking, eu já gostaria de puxar a sexta dica que eu considero uma das mais importantes e é aqui que a gente começa a realmente otimizar o processo: foco em uma única coisa por vez.

    Quando você for pesquisar vagas para trabalhar, você vai notar um mar de opções. Tem vaga para Data Science, Desenvolvedor Junior, Pleno, Sr, DevOps, Security, Tech Lead, Java, Python, PHP, Javascript, Go, etc, etc. Um buzilhão de tecnologias em uma sopa de siglas e termos técnicos que pode deixar qualquer um em pânico. Você se pergunta:

    — Será que eu vou ter que aprender essa p* toda? Eu estou f*!

    E pode piorar! Quando você olha tudo isso, você pode cair na tentação de guiar seus estudos pelo que o mercado está pedindo:

    — Ah! Ouvi que Python é melhor! Ah! Tem muito mais vaga de front! Ah! O esquema agora é Data Science para Machine Learn…

    Isso é um problemão! Lembre que você tem vários fatores limitando o seu desenvolvimento, mas só um deles não tem como mudar: o tempo.

    Melhorar o uso do seu tempo é fator principal para encurtar o período de transição da sua carreira.

    Otimizar o uso do tempo é o fator principal para encurtar o prazo até o novo emprego.

    Você não pode se dar o direito de desperdiçar seu tempo com distrações. Você precisa ser um sniper, e não o Rambo atirando para todo lado com uma metralhadora.

    Então escolha uma única profissão e uma única pilha de tecnologia e mexa só com ela. Escolha ser “Programador de uma única linguagem” e foque nisso.

    — Mas e se eu não gostar de ser programador?
    — Depois você muda. Primeiro você vira alguma coisa. Depois você muda.

    Se você quiser ser cientista de dados, você inicia o processo de migração já em um emprego que paga bem e com conhecimentos de programação que serão muito úteis na sua carreira como cientista de dados. Saber programar é uma habilidade que ajuda em várias profissões em tecnologia.

    Ah! E repito: não se distraia com ofertas de emprego com outras tecnologias. Elas vão aparecer aos montes e você vai achar que escolheu a tecnologia errada sempre que ver qualquer vaga de outra linguagem. Acredite: tem vagas sobrando para todas as tecnologias. Você vai encontrar uma.

    Além disso, conforme você for ganhando experiência com os fundamentos da programação você vai ver que aprender novas tecnologias e linguagens vai ficando cada vez mais fácil e rápido.

    É tudo CRUD1

    Já vimos uma dica para otimizar o tempo e a próxima dica, a sétima, é sobre otimização dos estudos: para se tornar um programador não é necessário dominar todo o campo da computação. Até porque isso é impossível.

    Se você dominar bem o básico de alguns tópicos principais, já é possível desenrolar vários tipos de problemas de trabalho e, com isso, conseguir preencher uma vaga como programador.

    Tem uma brincadeira entre programadores que diz que no fim das contas todos os programas são CRUD1. É uma super simplificação, mas não é totalmente mentira… a maioria do software é isso mesmo e saber fazer CRUD você já pode trabalhar com TI.

    Os tópicos que te preparam para construir softwares completos podem variar um pouco dependendo do tipo de vaga que você está procurando, mas se você aprender a:

    • Programar em uma linguagem de programação como Python, JS, PHP, Go ou Java;
    • Modelar software Orientado a Objetos (ou software funcional em uma linguagem funcional);
    • Usar algum framework dessa linguagem;
    • Modelar tabelas em bancos de dados relacionais e fazer queries SQL;
    • Usar e criar APIs/Websites (ao menos) com protocolo HTTP/REST;
    • Usar ferramentas de desenvolvimento como Editor de Texto ou IDEs, git, Docker e ter familiaridade com terminal Linux/Mac;

    … você já está pronto para as batalhas e já consegue preencher bem a grande maioria das vagas para programador Junior do mercado.

    Parece muita coisa, né? E é haha 🤷🏻‍♂️

    Mas se você consegue lidar com esses tópicos, você já cobre a grande maioria dos conhecimentos necessários para desenrolar o trampo. Fazer CRUDs e tal…

    E também não precisa ser “mestre” em todos esses tópicos. Sabendo preparar um arroz com feijão com cada um deles já tá de bom tamanho. No início! Para evoluir na carreira tem que dominar bem todos eles e mais alguns outros.

    Outro conhecimento que é muito útil e importante, mas não tem relação com computação, é o inglês. Saber ler em inglês é a coisa que mais vai te ajudar a estudar isso aí tudo. Não é uma obrigação porque dá para “quebrar galho” com tradutores automatizados, mas, como o nome diz, isso é só um “quebra galho”.

    No fim desse artigo eu coloquei algumas indicações de estudo e leitura. Infelizmente nem todas as indicações estão em português e boa parte delas é recomendação de leitura porque é o meu jeito favorito de estudar. Se você conhecer outros conteúdos em outros formatos, deixa aí nos comentários.

    Muito bem, todas as dicas agora serviram para te ajudar a otimizar o processo de aprendizado, mas e a vaga? E o emprego?

    Aplique para as vagas

    Pois bem, a oitava dica é: aplique para as vagas. Só isso mesmo… brincadeira… Aplique para todas as vagas que você achar interessante. Mesmo para aquelas que você não atende aos requisitos. Se a vaga pede algum tipo de teste prático, é ainda mais legal.

    Cuide só para não aplicar para vagas demais e acabar atrasando o desenvolvimento dos testes práticos. Sempre que eu estou aplicando para muitas vagas, eu gerencio o fluxo das atividades em um quadro no Trello para não me perder nas tarefas. No artigo Conseguindo um emprego em TI eu falo muito mais detalhadamente sobre esse assunto.

    Se você estiver se sentindo confiante, também pode tentar pegar alguns servicinhos tipo freelance em sites dedicados à isso. Tem vários e como nunca usei nenhum deles não vou indicar nenhum específico. Busque no Google e procure referências com alguém que tenha trabalhado com esses sites.

    Outra coisa para tomar cuidado é: tem umas poucas empresas ‘falcatruas’ que colocam trabalho de verdade como se fossem testes. Se o teste é fechado e os caras não são muito transparentes sobre o que farão com ele: desconfie.

    É importante lembrar que, mesmo que você não passe no processo, você treinou e ainda pode receber um feedback da empresa com dicas para te ajudar na priorização dos estudos.

    Ah! E você vai reprovar em muitos processos. Sei que é difícil, mas, não desanime. E, se der certo e você for contratado, já sabe, conta pra gente!

    Se puder, faça (ou termine a) faculdade

    A última dica é bônus porque, diferente das outras, essa pode não servir para todo mundo: se você está na faculdade, continue. Mesmo se ela não for de TI.

    É muito comum, em TI, trabalhar com programação mesmo sem ter diploma. Por conta disso, tem muito “guru” recomendando que você abandone a faculdade para focar em estudar programação.

    Se você não consegue se manter na faculdade por algum motivo, tudo bem. Mas se não for esse o caso, continue. As estatísticas mostram que pessoas formadas tem mais oportunidades e renda maior no mercado. Para quê abrir mão disso, não é?

    Então essas são as dicas para você que quer mudar de área. Boa sorte na jornada e se precisar de ajuda manda nos comentários.

    1. Sistema Operacional e Ferramentas (Linux/MacOS, editor de textos, git, terminal, shell e linha de comando, etc)
      1. https://osantana.me/tutorial-linux/
      2. Livro: Programação Shell Linux – https://amzn.to/3GJroMK
    2. Linguagem (dê preferência em linguagens que você tenha amigos que programem. Se não tiver nenhuma vá de Python ou JS porque a quantidade de material gratuito é enorme)
      1. Livro: Introdução à Programação com Python: https://amzn.to/3NfMyEy
      2. Canal Youtube: https://www.youtube.com/c/dunossauro
      3. Livro: Fluent Python https://amzn.to/38LU5fu (intermediário/avançado)
    3. Modelagem e programação Orientada a Objetos
      1. Livro: Object Oriented Python: https://amzn.to/3FbNlTT (Python)
      2. Livro: Building Skills OO Design Book: https://slott56.github.io/building-skills-oo-design-book/build/html/index.html (Python/gratuíto)
      3. Livro: Sams Teach Yourself Object Oriented Programming in 21 Days https://amzn.to/3MkaiX6 (Java)
      4. Livro: Head First Object-Oriented Analysis and Design https://amzn.to/3NQZikZ (Java)
      5. Livro: Fundamentos do Desenho OO com UML https://www.estantevirtual.com.br/livros/meilir-page-jones/fundamentos-do-desenho-orientado-a-objeto-com-uml/3383501357 (avançado/fora-de-impressão)
    4. Web/HTTP/REST API:
      1. https://developer.mozilla.org/pt-BR/docs/Web/HTTP – Tutorial HTTP Web API da MDN (tem vários outros tutoriais bacanas sobre Web nesse site… dá uma passeada nele…)
      2. https://restfulapi.net – site que fala sobre APIs REST (HTTP). A parte que fala sobre hipermedia/HATEOAS você pode pular porque ninguém usa isso  
      3. https://github.com/Developer-Y/cs-video-courses#web-programming-and-internet-technologies – cursos completos sobre programação web
      4. https://www.slideshare.net/osantana/a-web-uma-api essa é uma apresentação que eu fiz online (infelizmente não tenho video dela). Ela é um pouco mais avançada mas acho que o comecinho dela pode ajudar.
      5. https://www.slideshare.net/osantana/contruindo-um-framework-web-de-brinquedo-s-com-python – essa é uma outra apresentação que fiz onde eu crio um framework web em Python. Essa é avançada mesmo. Deixe ela por último.
    5. SQL e modelagem de Banco de Dados Relacional
      1. Livro: Introdução a Sistemas de Bancos de Dados https://amzn.to/3xcgU5t (livro muito completo e não fala de nenhum banco de dados específico)
      2. Livro: PostgreSQL Up & Running https://amzn.to/3ah8iRW (esse foi o que li para aprender mas tem outros livros que também parecem bons da editora Novatec e Casa do Código)
    6. Programar, programar e programar mais
      1. https://github.com/practical-tutorials/project-based-learning#python – ideias de projeto para colocar em prática o que aprendeu
    7. Continuar estudando pra sempre
      1. https://roadmap.sh – Esse site aqui tem vários roadmaps de carreira. Siga pelo de Backend e depois pelo de Python. Os roadmaps são assustadoramente grandes mas não se desespere porque não precisa ir atrás de tudo aquilo (nem eu sei tudo o que está neles). Siga as caixinhas com um “check” roxo e foque em 5 grandes tópicos. Foque só neles e não se distraia com outras linguagens ou tecnologias nesse momento.

    [1] CRUD é uma sigla para Create (Criar), Read (Ler), Update (Atualizar), Delete (Remover) que são as operações básicas que fazemos em qualquer sistema de cadastro.

    Imagem destacada: (c) 2013 http://www.exampapersplus.co.uk/

  • How I (do) Code Review

    How I (do) Code Review

    Este post está em inglês porque compartilho ele com meus colegas de trabalho que não leem em português.

    When I am reviewing your code, I love to learn and help you with proposals that I believe that should improve it. When you read my reviews, keep in mind that:

    1. The code is yours. You can disagree with me, and I am completely fine with that.
    2. My comments are about your code and not about you.
    3. When I ask something, I do it to understand the full context of your work to write better proposals.
    4. I am dumb and make mistakes (a lot of them). I tried my best and failed? Just point me the problem and I will fix it.
    5. I am not a native English speaker, and code review is mostly made with limited text messages. If you don’t understand or feel bad reading something that I wrote, it is my fault and I apologize for that in advance. I probably wrote something in a bad way and I would be grateful if you point me out these things. It will help me to improve.

    I have four types of comments:

    1. Recommendation / Suggestions (see more about this below)
    2. Questions. I can ask questions to:
      • Get additional information about the context (Is it a critical bug? Something urgent? Worth a bigger refactoring?);
      • Understand the issue that you are solving.
    3. Code Suggestion. For small things that I can edit directly during the review process.
    4. 💀 Dead code is dead. We use VCS/git, and we don’t need to commit commented code 🙂

    When I am reviewing the code, I have three levels of recommendations (and an off-by-one level 😛):

    1. Ignore: I see something that should be improved, but I know that it would lead to a massive refactoring that it’s not worth it. Sometime it will generate a proposal for the team that leads to an RFC or a card on the board. Occasionally, I just ignore and keep on 🙂
    2. Format: Things that Black & Ruff should fix automatically for us. Regularly, I use the 💄 emoji to point out these suggestions.
    3. Style: Coding style. Things that go beyond code formatting but are still something that should improve, at least, code readability. Those recommendations are completely personal, and It is not fair to force it into the team. I can also use the 💄 emoji in these comments. Some things that I (emphasis) (dis)like:
      • I prefer return-early pattern;
      • I don’t like to use dict() for data structures;
      • I “hate” elif‘s and else‘s (I am a OOP-guy and that polymorphism-thing 🙂);
      • I am scared of if/elif with no else;
      • I don’t like to return None as an exceptional return (unless I am coding in C or Go);
      • I don’t like to use flag variables to control the flow of the code.
      • [Some other things that I will add here when I remember].
    4. Fix: these recommendation points for real problems in the code.

  • Conseguindo um emprego em TI

    Conseguindo um emprego em TI

    Esse artigo é uma adaptação do meu vídeo no YouTube.

    Neste artigo, vou falar sobre como conseguir um emprego em uma empresa de tecnologia. Vou colocar aqui algumas dicas partindo da visão de quem já esteve dos dois lados do balcão: o lado de alguém que já procurou uma vaga e o lado de quem já contratou profissionais de desenvolvimento de software.

    Também vou trazer alguns tópicos aqui para vocês terem atenção no momento em que vocês forem participar de um processo seletivo.

    Primeiro de tudo é preciso dizer que mesmo eu, que já tenho uma experiência grande em TI, ainda falho em alguns processos seletivos.

    Quando participa de um processo seletivo e não passa nele, a causa nem sempre é relacionada com a nossa capacidade técnica ou intelectual de trabalhar para aquela empresa.

    Geralmente, isso acontece quando a gente não tem um alinhamento cultural, alinhamento de posicionamentos ou outras características. Então, não passar em um processo seletivo não significa que você falhou, significa que você tem pontos que você pode melhorar ou aperfeiçoar. Sempre que você reprovar em um processo seletivo, entenda isso como uma oportunidade de melhorar para a próxima tentativa e não como uma incapacidade.

    Preparação

    Uma coisa importante para participar de processos seletivos é estar sempre se preparando, estudando, vendo quais tecnologias as empresas estão usando e acompanhando o mercado de vagas, entre outros.

    Quem está participando de processos seletivos também precisa manter sua presença na internet bem atualizada, ou seja, manter seu perfil no LinkedIn e no GitHub sempre bonito, organizado, com seus projetos mais importantes em destaque. Não se esqueça de deixar seus contatos disponíveis para os recrutadores conseguirem te encontrar mais fácil.

    Mantenha sempre seu currículo atualizado, revise sempre, peça recomendações para seus colegas de trabalho, atualize seus cursos, treinamentos e as atividades que você tem feito ultimamente. Isso sempre será útil quando um recrutador bater o olho no seu perfil.

    É legal manter sempre uma wishlist, uma lista de desejos, de empresas que você acha bacana. Empresas com um produto bacana, que usam tecnologias nas quais eu tenho interesse, que possuem uma cultura alinhada com seus valores, etc. Eu tenho uma lista dessas empresas que eu gostaria de trabalhar.

    Crie essa lista de desejos, mas entenda o momento da sua carreira. Se você é um iniciante e a empresa contrata pessoas mais experientes, é importante estar ciente de que o momento certo para tentar uma vaga ainda não chegou. Mas não deixe isso te intimidar também. Às vezes, é legal correr um certo risco. Vou falar um pouco mais sobre isso mais adiante.

    Processos Seletivos

    Quando estamos precisando muito de um emprego, é interessante participar de vários processos de forma simultânea e, para isso acontecer de forma saudável, é preciso tomar alguns cuidados:

    1. Priorize os processos de modo a conseguir dar a atenção necessária a todos eles;
    2. Limite o número de processos simultâneos (o meu limite é 3, o seu pode ser maior ou menor). Processos com testes práticos podem pedir projetos que exijam um tempo grande;
    3. Qualidade é mais importante que quantidade;
    4. Organize os processos seletivos em um Kanban.

    Faça um quadro no Kanban e crie um calendário dedicado. O Kanban pode ter as seguintes colunas:

    • Contactar – empresas que você ainda precisa contactar para uma vaga.
    • Entrevista Inicial – processos que já tem uma data/hora para a entrevista inicial.
    • Teste técnico – processos que já tem um teste prático com prazo para realização e finalização.
    • Entrevista Complementar – processos com outras entrevistas complementares.
    • Aguardando Resposta – processos finalizados onde você espera por uma resposta da empresa.
    • Aprovado – a vaga é sua!
    • Reprovado – a vaga ainda não é sua. Peça feedback do contratante e anote o motivo da rejeição. Se a empresa possibilitar um novo processo futuro, é bom anotar a data aqui.

    E se você está com uma dúvida entre duas ou mais empresas, tente manter a negociação com as duas empresas no mesmo pé, se você se sente confortável com isso. Lembre-se de que é legal informar para todos os recrutadores que você está participando de outros processos em paralelo.

    Entrevista

    Treine bastante para as entrevistas. As orientações são bem simples:

    1. Jamais minta. Omitir é permitido. Mas recrutadores são ótimos em trazer omissões para superfície.
    2. Tenha explicações claras e objetivas para eventuais “problemas” no seu curriculum:
      • Períodos de permanência muito curtos em empresas;
      • Intervalos vazios no curriculum;
    3. Algumas empresas ainda fazem perguntas pessoais para os candidatos (principalmente no Brasil). Gostaria de dizer que você não precisa responder elas, mas eu estaria mentindo. Se você não quiser responder algo, tente preparar uma resposta bem limitada.
    4. Passe a impressão de que você está no processo seletivo porque quer trabalhar na empresa. Falar mal de oportunidades anteriores sempre pode causar problemas.
    5. Fale sempre de suas experiências sem emitir juízo de valor. No lugar de “o processo lá era muito ruim e por isso os prazos estouravam”, prefira usar “era muito difícil cumprir os prazos lá. O processo de priorização era muito simples e era centralizado na gestão”. Note que você não qualifica os problemas, mas traz os fatos. O recrutador vai saber interpretar esses fatos e ver os problemas.
    6. Se pedirem a sua opinião sobre uma experiência anterior, é importante evitar julgamentos de valor ou qualificações rasteiras. No lugar de “eu acho que a gestão era uma porcaria e por isso as prioridades eram uma bagunça”, prefira usar “eu acho que era bem difícil entender bem as prioridades do time porque o gestor não colocava elas com clareza”.
    7. Atraia a atenção do recrutador para os pontos positivos do seu curriculum. Se ele pergunta algo sobre um projeto problemático, é importante responder e tentar complementar a resposta com um contraponto de outro projeto onde o problema não existia. Exemplo: “Vi aqui que você trabalhou só em um projeto nessa empresa, você teve alguma outra experiência além dessa?” e você responde: “Sim, eu trabalhei só nesse projeto porque ele era importante para a empresa e eu conhecia ele muito bem. Então era muito difícil para eles me colocarem em outros projetos… entretanto, na empresa XPTO eu me envolvi em vários projetos que precisavam ser criados… “.

    Entrevista Técnica ou Teste Prático

    Do mesmo jeito que recomendo treino para entrevistas, é interessante investir um tempo treinando para as entrevistas técnicas ou para os testes práticos.

    Treine algoritmos, estruturas de dados, linguagens, framework, POO, banco de dados, etc. Desenvolva projetos, brinque em sites de desafios técnicos, leia livros, e assim por diante. Programação é uma atividade de prática deliberada. Você não aprende a programar sem praticar.

    Nas entrevistas técnicas valem as mesmas dicas da entrevista convencional que mencionei acima. Mas vale alguns complementos:

    1. Alguns recrutadores fazem algumas perguntas que não possuem resposta correta ou possuem várias respostas satisfatórias. O objetivo dele não é a resposta e sim o desenvolvimento dessa resposta. Como você pensou para chegar a ela.
    2. Pense em voz alta. O recrutador não lê a sua mente e o seu raciocínio oferece dicas valiosas para ele te avaliar bem (ou te dar um feedback mais completo em caso de rejeição).
    3. Toda solução para um problema tem aspectos positivos e negativos. Ter consciência disso vai te ajudar bastante.

    Aprovação e Feedback

    Terminando as entrevistas e os testes a gente chega na fase final de aprovação e feedback. Nessa etapa é comum acertar os últimos detalhes sobre a vaga, empresa e candidato. Coisas como cargo, salário, carga horária, time, projetos, etc. entram na conversa.

    Algumas empresas oferecem um momento para que o candidato faça perguntas. Prepare-se de antemão para essa oportunidade. Monte uma lista de perguntas que demonstre o seu interesse na empresa, no produto, no mercado, na vaga, no time, nos projetos, nos processos, nas finanças, etc. Pesquise sobre a empresa e as tecnologias usadas na Internet e anote umas perguntas.

    Se a empresa não seguir adiante com o processo, é legal você solicitar um feedback técnico do recrutador. Peça, educadamente, indicações de material de estudo, quais pontos precisam ser melhorados, quando você poderá aplicar para uma vaga novamente, etc.

    Por fim, é importante lembrar que, às vezes, o salário não é tudo. É legal pesar isso. Se você é uma pessoa que gosta muito de dinheiro, realmente buscando isso, vá atrás de empresas grandes. Mas se você está buscando uma empresa mais familiar, onde você vai ter um ambiente de trabalho mais legal, não vai ter tanto estresse, às vezes vale a pena pesar um pouco isso na balança.

    Enfim, essas são algumas dicas que eu acredito que podem te ajudar na sua busca por um emprego em uma empresa de tecnologia. Espero que você tenha sucesso em sua jornada. Boa sorte!

  • Temperos de Arquitetura

    Temperos de Arquitetura

    Já faz alguns anos que eu estou trabalhando com modelagem de sistemas. Em alguns lugares chamam isso de “arquitetura”, mas uma colega arquiteta (CREA e “talz”) me explicou que não é muito adequado usar a palavra “arquitetura” para definir esse trabalho. Como não sou o especialista e nem estou interessado em me aprofundar nesse tipo de discussão resolvi aceitar os argumentos e não usar mais “arquitetura”.

    Minha forma de trabalho é muito intuitiva e baseada em experiências práticas com coisas que deram certo e coisas que não funcionaram bem. Muitas coisas que fiz usando pura intuição se mostraram, mais adiante, como coisas que já existiam e já eram muito estudadas. Só eu é que ignorava.

    Capa do livro Designing Data-Intensive Applications

    Estou lendo um livro chamado “Designing Data-Intensive Applications” (Martin Kleppmann) que descreve vários desafios e soluções para desenvolvimento de sistemas distribuídos e conforme vou avançando na leitura vou sendo surpreendido com coisas que eu já fiz sem nem saber que aquilo tinha um nome. 🙂

    Nesse artigo eu vou listar algumas técnicas que sempre uso para modelar sistemas em que estou trabalhando. É um ‘braindump’ de técnicas listadas sem nenhuma pretensão, estrutura, ou ordem de importância.

    Provavelmente não é uma lista completa também. Uma coisa é ter um repertório de técnicas para usar no dia-a-dia. Outra coisa é lembrar de todas elas para escrever um artigo. Sou péssimo para lembrar das coisas.

    As dicas de modelagem que apresentarei abaixo podem ser subdividas em duas categorias: Princípios e Práticas. Vamos dar uma olhada nelas.

    Princípios

    Os princípios que uso no meu trabalho são mais abstratos e servem para orientar minhas escolhas em um nível de abstração mais alto.

    Menos é mais

    Quando estou trabalhando na modelagem de um sistema eu gosto muito de limitar minhas ferramentas. Eu trabalho melhor com a restrição do que com a abundância. Eu também gosto de ser um pouco conservador nessa hora.

    Um exemplo de conservadorismo e restrição é: se eu preciso oferecer uma API para ser usada publicamente eu provavelmente especificarei uma API HTTP REST. É simples, todo mundo conhece e sabe usar, tem ferramentas infinitas para tudo e é bem elegante.

    Outro exemplo desse conservadorismo aparece sempre que eu preciso armazenar dados em algum lugar. As chances de eu escolher um banco de dados relacional (PostgreSQL, é claro 😉 ) para a solução é enorme. Eu escolho SQL porque esse modelo está “por aí” a décadas e tem um número incontável de pessoas usando. E servidores de banco de dados evoluíram absurdamente nos últimos anos.

    Se os prazos são um pouco mais apertados (quando eles não são?) eu também escolho ferramentas que eu tenho um domínio maior. Exemplo: se eu precisar desenvolver uma solução distribuída é bem provável que uma linguagem com o perfil de Elixir tenha o ‘fit’ perfeito. Mas eu não conheço Elixir tanto quanto conheço Python. Então é provável que eu vá de Python. Com Django porque eu conheço melhor também.

    Se essa escolha se mostrar equivocada (nunca aconteceu) a gente planeja a troca.

    Quando chega aquela hora de dizer o nome das ferramentas e tecnologias que vamos usar eu também dou preferência para serviços gerenciados por terceiros à serviços que eu tenha que manter eu mesmo.

    KISS

    O princípio KISS (Keep It Super Simple*) é bastante antigo e a primeira vez que ouvi falar dele foi quando estava aprendendo a usar Unix. Antes de usar o Unix eu costumava usar o (MS|DR|Novell|PC)-DOS. No DOS, quando eu precisava mostrar o conteúdo de um arquivo eu fazia:

    C:\> TYPE README.TXT

    Se o arquivo fosse muito grande ele ia rolar a tela até o final e, diferente de hoje onde conseguimos rolar a janela para ver o que aconteceu, não conseguíamos ver o que estava no início do arquivo. Para ver o conteúdo de um arquivo pausando tela por tela a gente fazia:

    C:\> TYPE README.TXT /P

    E pronto. Notem que o comando TYPE sabia mostrar o conteúdo do arquivo e pausar de tela em tela. Quando passei a trabalhar em um Unix (no estágio) me ensinaram que o comando usado para mostrar o conteúdo de um arquivo era o cat. Então fui lá e mandei um:

    % cat README.TXT
    cat: README.TXT: No such file or directory
    % cat README  # ops! aprendendo que o filesystem do Unix é case-sensitive! :)

    E então o conteúdo do arquivo despencou a rolar pela tela. Ótimo! Aprendi a ver o conteúdo do arquivo! Agora preciso ver qual o parâmetro para ele parar de tela em tela…

     % man cat

    … e nada… Pensei: “mas que bela porcaria esse sistema, hein?”. Foi quando o meu supervisor de estágio chegou e falou: “No Unix os comandos fazem só uma coisa. E fazem bem essa coisa. Se você quer pausar a saída do cat (ele falou monoespaçado assim mesmo 😛 ) você precisa jogar a saída dele pro more. Assim ó…”. E digitou:

     % cat README | more

    Pronto. Depois descobri que essa filosofia era chamada de KISS.

    Essa história toda serve para ilustrar o que eu faço quando estou modelando um sistema: tento manter cada componente (seja ele um pacote, classe, microsserviço, etc) muito simples.

    Esse princípio também pode ser chamado de “Single Responsibility Principle” (a letra “S” em SOLID). Eles dizem basicamente a mesma coisa mas o “Single Responsibility” formaliza mais o seu significado.

    Quando cada um desses componentes é simples e tem uma única responsabilidade eles inevitavelmente serão também mais coesos. E coesão é algo desejável em um bom componente.

    Baixo acoplamento e Alta coesão

    Um dos melhores livros de programação orientada à objetos que li é o “Fundamentals of object-oriented design in UML” (Meilir Page-Jones). Nesse livro ele bate bastante na tecla de que um bom “objeto” (componente, pacote, etc) precisa ter as seguintes características:

    Baixo acoplamento

    Se você precisa fazer uma alteração simples em apenas um único comportamento da sua aplicação, quantos componentes diferentes do código você precisa mexer? A resposta para essa pergunta fala bastante sobre o acoplamento da sua aplicação.

    O baixo acoplamento é desejável porque ele faz com que sua aplicação fique mais fácil de ser mantida e de ser estendida com novas funcionalidades. O melhor método de se chegar ao baixo acoplamento é por meio do processo de refactoring** constante.

    Tentar desenvolver código com baixo acoplamento logo de largada é difícil, demorado, e pode te aprisionar em todo tipo de problema relacionado à early abstraction. Então encare o “baixo acoplamento” como objetivo e não como requisito.

    Alta coesão

    Esse conceito é um pouquinho mais complicado de explicar, mas o significado do adjetivo “coeso” pode nos dar uma dica. Coeso, segundo alguns dicionários, significa:

    1. Que se relaciona através da coesão, por meio da lógica, de forma harmônica: fala coesa, proposta coesa, atitude coesa.
    2. Intimamente unido; ligado com intensidade.
    3. Disposto de maneira equilibrada, proporcional; ajustada. (figurado)
    4. Seguindo um raciocínio lógico, com nexo; coerente. (figurado)
    5. Sinônimos: coerente, harmônico, ajustado.

    Sei que avaliar um componente sob essa perspectiva é muito subjetivo e pode significar coisas diferentes para pessoas diferentes.

    Como eu faço isso? Eu olho para o código do componente e procuro por todo tipo de coisa que não deveria estar ali. Pergunto-me porque aquilo não deveria estar ali e penso em algum jeito de mover essa parte para um local mais adequado.

    Dividir para conquistar

    Na nossa vida de programador a gente está sempre resolvendo problemas. E problemas tem tamanho. Tem problema pequeno, médio, grande, … Resolver problemas pequenos costuma ser (nem sempre é) mais fácil do que resolver problemas grandes.

    Por isso um dos skills mais importantes que um bom programador precisa ter é a de dominar a arte de quebrar problemas.

    A dica aqui é simples de passar, mas difícil de dominar. Se está difícil resolver um problema:

    1. Pare
    2. Dê uns passos para trás
    3. Olhe para o problema e busque por pontos de quebra
    4. Quebre o problema
    5. Tente resolver uma parte
    6. Se funcionar: profit!
    7. Se não funcionar: volte para o passo 1.

    Trabalhei muito com comércio eletrônico ao longo na minha vida e, nesse contexto, sempre encontrei diversos tipos de problemas para resolver. Um desses problemas é: gerenciamento de produtos.

    É um problemão… tem questões de marca, especificações, preço, estoque, venda, promoções, kits, recomendações, etc. Não dá para resolver isso tudo de uma única vez e, mesmo se a gente dividir cada uma dessas coisas em várias, os problemas resultantes podem continuar gigantes.

    Mas vou falar sobre um fluxo básico: eu sou vendedor (seller) de um canal (channel) de marketplace e cadastro um produto no site para vender. Só que tem outro vendedor que vende o mesmo produto.

    Primeira vez que modelei esse problema eu fiz: SellerProduct e ChannelProduct. Tinha uma instância de SellerProduct para cada vendedor com seu respectivo estoque, e preço e um ChannelProduct no canal apontando para um desses produtos de um desses vendedores. A gente dizia que um desses vendedores estava ganhando a “buy box”.

    Quando o primeiro vendedor cadastrou o produto ele informou que aquele produto era bivolt. O segundo vendedor falou que esse mesmo produto era 220V. Que informação eu coloco no site? Quem está certo?

    Outro problema: o vendedor #1 é de Recife e o vendedor #2 é de POA o preço dos dois é igual e eles têm estoques parecidos. O cliente de Maceió chega no site para simular o valor do frete. É bem provável que o frete do vendedor #1 seja melhor, mas quem ‘ganhou a buy box’ foi o vendedor #2. Como calcular todos os fretes de todos os vendedores e escolher o melhor rapidamente? Perdemos a venda?

    Como vocês podem notar a modelagem não está dando conta do recado. Precisamos repensar ela. Talvez a gente precise quebrar esse problema ainda mais.

    Nesse processo a primeira coisa que ficou clara para gente é que “Produto” significa muitas coisas diferentes para pessoas e contextos diferentes.

    Para o vendedor um produto é “um item no seu estoque”. Ou um “SKU em seu portfólio”. Para uma marca/fabricante/importador um produto é “algo que ele produz com certas características”. Para o canal de venda um produto é “algo que eu estou ofertando”. E para o cliente o produto é “algo que ele compra”.

    Entenderam o raciocínio? O super problema “gerenciamento de produtos” precisa ser quebrado em problemas menores:

    1. Gestão de Portifólio (SKU e estoque)
    2. Gestão de Catálogo (características de um produto)
    3. Gestão de Ofertas/Distribuição (anúncios e vitrines)

    E cada um desses sub-problemas ainda pode passar por mais um processo de quebra.

    Lazy Preoccupation

    Esse princípio foi adicionado mais recentemente ao meu repertório. E eu mesmo que dei esse nome (então nem adianta procurar ele na internet 😛 ). Esse princípio deriva da minha experiência de trabalho com metodologias ágeis de desenvolvimento de software.

    O princípio da preocupação tardia (lazy preocupation) é: resolve o problema que tem para resolver agora e deixa os problemas futuros para serem resolvidos no futuro.

    Parece bobo de tão óbvio (e é), mas é muito interessante ver como eu ainda falho na aplicação desse princípio em certas ocasiões.

    A questão aqui é: se você já está trabalhando na solução do menor problema possível (ver tópico anterior) e está funcionando é provável que o mesmo aconteça com os futuros problemas quando chegar a vez deles serem resolvidos.

    Quando o futuro chegar, também, é muito provável que sua compreensão sobre o domínio do problema já esteja mais evoluída e solucionar ele fique até mais fácil.

    E mesmo nos casos onde isso não acontece e a solução do problema anterior trava a solução do problema futuro é só dar uns passos para trás e tentar outra abordagem. Agora você vai conseguir fazer isso de forma muito mais efetiva porque já tem conhecimentos complementares para te guiar.

    Também aplico esse princípio para lidar com questões de modelagem vs. implementação. Quando estou criando a modelagem de um sistema eu tento não me preocupar com características de implementação. Qual banco de dados vou usar? A API vai ser REST ou gRPC? Vou usar serviços ‘serverless’? Isso vai ficar lento?

    Esse tipo de preocupação, logo no começo, atrapalha demais o foco no problema e na modelagem na solução abstrata dele. O melhor momento para pensar na implementação da solução é no momento em que você for implementar ela.

    Práticas

    Abrace as falhas e as hostilidades

    Programadores tentam escrever softwares sem falhas. Eles estudam para melhorar suas habilidades e produzir código com mais qualidade. Aprendem a fazer testes automatizados tanto para melhorar o desenho das suas implementações (TDD) quanto para garantir que o software funcione conforme o esperado. O problema está aí “conforme esperado”. O que isso significa exatamente? O que acontece quando um software falha? E quando isso acontece de forma “inesperada”? É possível escrever um software infalível? Não. Não é.

    Se não é possível escrever software infalível porque a gente ainda escreve software esperando que o melhor aconteça? E porque a gente tenta esconder essas falhas dos clientes desse software?

    Acho que isso acontece porque a gente, como programador, considera a falha de um sistema como uma falha pessoal. Algo que é responsabilidade nossa. “Como eu não pensei nesse cenário? Como sou burro!”, não é mesmo?

    Ok… Mas se todos concordamos que é impossível escrever um software porque nos culpamos pelas falhas? Se todo sistema falha não seria melhor aceitar essas falhas de forma mais natural? Expor elas sempre que acontecerem? Falhar o mais rápido possível ao invés de segurar uma situação insustentável por mais tempo e aumentar o estrago?

    Quando estou desenhando uma solução eu sempre carrego a premissa de que todos os componentes que estou escrevendo ou usando vão falhar em algum momento.

    Resguardar todos esses pontos para garantir de que nenhum dessas partes irá causar um dano muito grande em caso de falha também é muito difícil. Talvez seja fundamentalmente impossível garantir isso (halting problem).

    Quanto mais queremos proteger e acrescentar redundâncias e proteções à nossa solução, mais custo e complexidade vamos adicionando nela… o que eu faço então?

    Eu sempre projeto sistemas que estejam prontos para continuar funcionando mesmo em cenários de falhas pontuais. Não dá para proteger todos os pontos, logo, se alguns componentes específicos falharem a coisa vai despencar completamente. À esses pontos então eu acrescento uma camada de redundância e uma camada de monitoramento e alertas mais rigorosos. Pronto.

    Na minha apresentação sobre a arquitetura de uma das empresas onde eu trabalhei os nossos “Calcanhares de Aquiles” eram:

    • PaaS (Heroku) – se o Heroku caísse dava bem ruim. Não tinha redundância então era só monitoramento mesmo. Também usávamos o PostgreSQL deles. Mas nesse caso tinha uma fina camada de redundância (dentro do próprio Heroku que não é o ideal).
    • AWS SNS – se esse falhasse despencava todas as operações ‘online’ da empresa. Só monitoramento.
    • AWS SQS – se esse caísse a gente perderia dados em um nível muito grave. Então tinha monitoramento e sempre garantia um bom número de workers para esvaziar essas filas.
    • AWS – Se a AWS inteira caísse… bom… ficaria complicado haha 🙂 Mas nesse caso a Internet inteira estaria com problemas.
    • DNS – Esse é sempre um problema para a Internet inteira.

    Qualquer outra coisa que ficasse fora do ar além dessas causaria alguma degradação ao sistema, mas ele se reestabeleceria com a normalização dos serviços. Tem mais detalhes sobre essa arquitetura nesses links aqui:

    Nessa arquitetura, se um serviço falhasse ele retornava um código de falha. Se fosse um erro 5XX era um erro 5XX e ponto final. Não é vergonha retornar um 500 Internal Server Error se de fato um Erro Interno no Servidor (Internal Server Error dã!) aconteceu, oras! Não precisamos “passar pano” para erro de servidor. Se o servidor estivesse fora do ar para manutenção? 503! Se tivesse lento? Timeout! E assim vai.

    Quando a gente retornava um erro para o cliente (ex. worker) ele pode decidir como lidar com aquele erro. Tentar outra vez? Descartar a mensagem? Guardar o erro em um log? Não importa. Ele vai saber que aquela operação não aconteceu. Agora imagina se o servidor falhou na operação e retornou um 200 Ok dizendo que tá tudo sobre controle?

    Então abracem as falhas. Os erros. Tratem seus serviços como falíveis e vocês vão sempre desenhar soluções mais robustas.

    Idempotência é sua amiga

    Eu disse no tópico anterior que se aconteceu uma falha no seu serviço você tem que deixar isso claro para seu cliente, certo? Mas não custa nada dar uma mãozinha para ele se recuperar dessa falha depois.

    Vamos supor que tenho um worker que pegou uma mensagem de uma fila e precisa mandar 15 requests para uma API baseado nessa mensagem. Ele manda o primeiro e “ok”. Manda o segundo e… “ok”. Manda o terceiro e “ERRO!”. Tento mandar o quarto e… “ERRO!” e assim vai até o fim.

    O que eu faço com os requests que falharam? Tento outra vez? E se continuarem a falhar? O que eu faço?

    Em teoria você precisaria de um lugar para “anotar” quais requests falharam e quais tiveram sucesso em algum lugar para retentar só aqueles requests que falharam? Mas o worker não guarda estado. Ele só pega mensagem de uma fila e procede com os requests.

    Criar um sistema só para guardar os requests que precisam ser feitos é muito complexo. E se esse sistema também ficar fora do ar? Entenderam o drama?

    Pois bem. Fizesse a tal API ser idempotente? Se você repetir um request que já aconteceu antes ela responderia algo tipo: 304 Not Modified ou 303 See Other (que são 2 códigos HTTP de sucesso?).

    Se sua API for implementada desse jeito o seu worker pode falhar completamente a transação (devolvendo a mensagem para a fila) porque quando ele precisar repetir essa operação ele vai repetir exatamente os mesmos 5 requests. E nenhuma informação vai ficar duplicada ou faltando do lado da API.

    Tente sempre fazer com que suas interfaces privilegiem operações idempotentes (mesmo que para isso precise fazer umas concessões aos padrões de POST e PATCH).

    HTTP é rei e a Web é uma API

    Duas das coisas mais difíceis em computação, para mim, é dominar a arte secreta de se escrever bons protocolos e boas linguagens de programação. Só os grandes gênios da computação conseguem fazer isso bem feito.

    Um dos protocolos mais elegantes que já vi é o HTTP. Ele é simples, poderoso, compreensível, escalável, bem conhecido e tem ótimas implementações disponíveis para todo mundo.

    Com a explosão no surgimento de APIs REST foi possível mostrar que o protocolo HTTP, por si só, permite implementar um número gigantesco de soluções mesmo sendo um protocolo muito básico com apenas um punhado de métodos (GETPOSTPUTPATCHDELETE, etc).

    É isso mesmo: APIs REST tem somente esses métodos definidos por RFCs. Então aqueles ‘verbos’ nas URLs (ex. /user/subscribe) da sua API são, no mínimo, uma licença poética 🙂

    O que eu acho interessante nessa “limitação” é justamente isso: ela me força a refletir melhor sobre os objetos (ou documento, ou resource, ou …) expostos na minha API. É uma limitação que me força a pensar uma solução que consiga funcionar na simplicidade do protocolo.

    Coisas como gRPC abrem esse leque de opções absurdamente e, apesar de facilitar o desenvolvimento e a entrega do produto final, exige muita cautela por parte do desenvolvedor para não criar um monstrengo de API com dezenas de métodos diferentes. E é aqui onde a gente reafirma o princípio do “Menos é mais” que listei lá em cima. Já interfaces com GraphQL tem um propósito muito específico: navegar por graphos e não deveriam ser abusadas para outros usos (até porque APIs GraphQL usam o protocolo HTTP de forma bem… estranha…).

    Na minha apresentação “A Web é uma API” eu ilustro alguns conceitos que demonstram como a Web já é uma API inerentemente REST e no Toy, um framework de brinquedo, eu experimento esse conceito:

    Event Sourcing / Fire and Forget

    Event Sourcing é um modelo de arquitetura de software que casa super bem com o princípio da Lazy Preocupation e com o princípio do Menos é mais. Adotar esse modelo também permite que a gente trabalhe com APIs mais “burras” (simples/KISS) e, como veremos adiante, isso é desejável.

    Em arquiteturas mais “tradicionais” é comum à um serviço comandar operações, ou seja, o serviço determina e chama as operações que precisam ser executadas indiferentemente de uma sequência específica (sync) ou não (async).

    Um sistema que de gerenciamento de pedidos de um site de comércio eletrônico, por exemplo, ao receber um novo pedido, precisa gravar esses dados em um banco de dados, mandar um e-mail para o consumidor para confirmar o recebimento do pedido, avisar o sistema de fulfillment que tem um pedido novo que precisa ser preparado, etc, etc.

    Note que nesse modelo o sistema de gerenciamento de pedidos precisa distribuir todas essas tarefas para sistemas externos e isso cria uma dependência de todos esses sistemas no sistema de gestão de pedidos.

    Imaginemos que, no futuro, esse mesmo sistema de pedidos precise executar uma operação que envia os dados desse pedido para um novo subsistema de BI. Você vai implementar/implantar esse sistema de BI e vai ter que tambémadicionar uma chamada para ele no sistema de gestão de pedidos. Notaram o acoplamento aparecendo aqui?

    Em uma arquitetura baseada em eventos (event sourcing) o sistema de gestão fica responsável apenas por registrar o novo pedido e avisar que tem um “pedido novo” à quem possa interessar publicando esse evento em um tópico (ou subject) de um barramento de mensagens (publish).

    Se um sistema de fulfillment tem interesse nesse novo pedido (bem provável) ele só assina (subscribe) o tópico sobre novos pedidos e faz o que tem que ser feito em cada novo pedido.

    Usar um sistema de filas persistentes para assinar esse tópico é bastante prudente porque facilita o processamento desses eventos mesmo em cenários de falhas de workers ou instabilidades.

    Quando o nosso sistema de BI estiver implantado é necessário apenas conectar ele ao sistema de pedidos através de outra assinatura ao tópico de novos pedidos e, desse modo, o sistema de gesrenciamento de pedidos nem precisa tomar conhecimento desse novo sistema. É só disparar e esquecer. Fire and Forget.

    APIs burras e autônomas, workers espertos e dependentes

    Para que a gente considere uma API HTTP boa ela precisa apresentar um conjunto muito grande qualidades. Características como robustês, estabilidade e performance são só algumas dessas qualidades.

    Quando uma API tem muitas responsabilidades e faz muitas coisas é bem provável que sua complexidade cresça demais e entregar essas qualidades começam a se tornar um desafio bem grande.

    E se as APIs delegassem essa complexidade para outros agentes da arquitetura? E se elas fizessem menos coisas? E se elas fizessem só o básico de validação de dados, armazenassem os dados que precisam ser armazenados e avisassem que essa operação aconteceu?

    Uma API que sabe validar, guardar e notificar o que aconteceu entrega tudo o que uma API precisa fazer. Mas ela precisa ser capaz de fazer isso sozinha. Se a validação de uma informação precisar de algum dado externo esse dado precisa ser “injetado” nessa API. Mesmo que isso cause uma duplicação (desnormalização).

    Se a função de uma API se restringir à essas 3 operações fica muito fácil implementar uma API simples, rápida, robusta e estável porque o código dela será muito simples. Quase nenhuma regra de negócio incorporada ao seu funcionamento.

    As regras de negócio mais complexas podem ficar em workers que estarão processando os eventos de uma fila (que assina um tópico) e terão todo o tempo do mundo para aplicar essas regras, fazer consultas em outras APIs, e gerar um resultado que será postado em outra API, na API originária, ou até mesmo em um tópico do barramento de mensagens.

    Idealmente um worker deve gerar uma única saída (ex. POST) para cada evento de entrada. Essa regra só pode ser violada nos casos onde a API que receberá essas saídas múltiplas for idempotente. Caso contrário você pode ter problemas com duplicação de registros.

    Deixe as decisões para quem detém mais contexto

    Quando a gente aplica o princípio da Lazy Preoccupation é bastante comum perceber que quanto mais um fluxo de processamento se adentra pelo sistema, mais informação de contexto ele carrega.

    Isso significa que quando você recebe uma entrada no sistema ela tem apenas os dados informados ali. Conforme esses dados vão passando por outros sistemas ele pode ser enriquecido com informações adicionais e essas informações adicionais são muito úteis para tomar decisões importantes no fluxo das suas regras de negócio.

    Então evite ao máximo lidar com problemas complexos logo no início dos fluxos. Se tá difícil resolver um problema em um determinado ponto desse fluxo é melhor deixar ele adentrar um pouco mais (lazy preoccupation) para que ele absorva mais dados de contexto que podem ajudar nas suas decisões relacionadas às regras de negócio.

    Interfaces e protocolos guiam implementações

    A sigla API significa “Application Programming Interface” e a palavra importante aqui é “Interface” em contraponto à palavra “Implementation”. Alterar implementações é fácil. Alterar interfaces não.

    Quando eu altero uma implementação difilcilmente eu quebro um sistema. Mas se alteramos uma interface é quase certo que teremos problemas.

    Então, ao desenvolver um sistema novo, dedique bastante tempo nessa etapa. Técnicas como TDD ajudam você a transitar por esse estágio quando não conhecemos muito bem o domínio do problema que estamos lidando.

    Comece a implementar o sistema somente depois que você estiver confortável com o desenho das interfaces e modelos expostos por ela.

    Append-only for the rescue

    Os sistemas modernos que operam em uma escala muito grande invariavelmente precisam ser implementados usando modelos distribuídos e, em sistemas distribuídos, você vai acabar tendo que lidar com locks e implementações de mutexes. Muitas vezes o seu banco de dados é quem vai ter que dar cabo dessas operações mas o fato é que os locks e mutexes estarão lá para ferir a performance do seu sistema.

    Uma forma de diminuir (e até mesmo evitar) algumas dessas travas é substituir operações de update por operações de insert no seu banco de dados. Quando atualizamos um registro em uma tabela o banco de dados vai ‘travar’ esse registro para escrita bloqueando outras transações que queiram fazer o mesmo***. Isso pode penalizar severamente a performance do seu sistema. Para o banco de dados, fazer um insert, é muito mais simples e as ‘travas’ vão ser usadas somente para atualização de índices.

    Mas isso tem um custo muito alto no processo de recuperar essas informações. No lugar de recuperar apenas um registro do banco de dados você precisa buscar todas as operações relacionadas àquele registro e consolidar elas para obter a informação necessária. Muito ineficiente.

    Para amenizar essa ineficiência é possível utilizar o design pattern CQRS (Command and Query Responsibility Segregation) que segrega as responsabilidades de escrita e leitura em dois segmentos diferentes do seu banco de dados permitindo escritas rápidas que serão ‘projetadas’ assíncronamente em um registro consolidado para consulta futura.

    Notas

    * Hoje traduzem esse acrônimo como “Keep It Super Simple” por causa da carga ruim que a palavra “Stupid” da tradução original (Keep It Simple Stupid) carrega.

    ** Refactoring é aperfeiçoar um pedaço do seu código sem alterar seu comportamento. O que garante que esse comportamento não está sendo alterado é um conjunto de testes automatizados. Ou seja, se não tem teste automatizado ou se o comportamento do software muda não é um refactoring. Se acontecer de algum teste quebrar durante um refactoring é interessante tentar entender se isso acontece porque seu código está mal implementado (ex. testando mais a implementação do que o comportamento) ou se a mudança está realmente mudando o comportamento do código. Vamos usar as palavras corretas para refactoring e para reescrita.

    *** Esse assunto é um tanto mais complexo que isso e recomendo a leitura do livro “Designing Data-Intensive Applications” (Martin Kleppmann) para mais detalhes sobre esse assunto.