Conjunto de perguntas sobre programação
Um histórico sobre mim
Um conjunto de perguntas que vou responder com a minha experiência atual.
O que é Solid Para você?
Para mim S.O.L.I.D é um conjunto de práticas que juntas, facilitam o processo de escrever testes automáticos, unitários e de integração. Regras como Simple Responsability (S) Principle permitem escrever testes de unidade mais eficientes, dando ênfase na unidade. Outros como Dependency Inversion Principle (D) junto com Liskov Substitution Principle (L) permitem trocar algumas das dependencies com versões Fakes ou Mocks, permitindo maior variedade de testes.
No final, as regras do SOLID ajudam a garantir uma aplicação testável, enquanto aumenta a quantidade de códigos focados em negócio, com fluxos simples de entender e extender.
Por que o Princípio da Responsabilidade Única é importante?
Uma classe ou uma função com uma única responsabilidade garante que este código seja capaz de ser facilmente reutilizável, e torna possível deduzir sua responsabilidade apenas com o nome da função.
Porém, o uso excessivo deste princípio pode ocasionar em códigos complexo de ser lido, uma vez que é necessário pular entre várias funções para deduzir uma parte do programa.
Ao meu ver, o deve-se evitar ao máximo Overengineering, já que “Unica Responsabilidade” é um conceito as vezes, complexo de se definir. Existem lugares e motivos para este princípio, onde a maior motivação deve ser legibilidade, deve-se prezar por um fluxo claro do código, sem muitos ramos.
Acredito que este princípio deve ser usado juntamente com o desenvolvimento de Funções Puras, livres de efeitos colaterais, e resultados sempre determinísticos. O que permite funções bastante reutilizáveis.
O que é inversão de controle, Como isso se relaciona com Injeção de Dependência.
inverter o controle, ou inverter a dependência, agrega em designar a implementação de algo a uma classe superior, ouse seja, imaginemos o componente A como um botão de login, ao ser clicado, será aplicado uma lógica de Login. Porem, a lógica de chamar a caso de uso de login, está integrado ao botão, assim, impedindo de qualquer forma reutilizar este botão.
Poremos então, seguir um modelo mais genérico, criar um componente Button, que permite passagem de uma parâmetro como onclick, que dispara o evento de click para uma função externa, e esta, lida com o evento. Criamos então, um componente mais genérico, reutilizável, e testável. Alem de separar a lógica de Login, do botão, enfatizando o Princípio S.
A injeção de dependência, trata-se de uma estratégia para fornecer essas dependências, as registrando como padrão. Isso cria um cenário mais simples para nós desenvolvedores, onde não precisamos nos preocupar com a injeção de serviços necessários para o fluxo da aplicação, seja um Provider, Repository ou Service.
O uso de Injeção de Dependência é muito útil, mas o princípio de inversão de dependência é algo muito mais amplo e poderoso, a injeção é apenas uma das implementações.
Por que interfaces são importantes?
Interfaces garantem modularização, contratos e APIs para acesso de um recurso específico. intefaces adicionam a possibilidade de troca de implementações. Um caso claro é ter vários provedores de Email, com várias implementações, com o objetivo de garantir disponibilidade, pois se um serviço cai, basta trocar a implementação.
Com essa flexilidade nas trocas de implementação, é fácil criar Mocks, pois basta variar a implementação, mas ainda respeitando a implementação.
Outro bom uso de interfaces é em conjunto com o Interface Segregation Principle, onde é preferível ter várias pequenas interfaces à uma grande interface, em casos onde um serviço pode implementar apenas alguns, mas não todas as funções da interface.
Pensemos no seguinte caso, você começa desenvolvendo sua aplicação dotnetusando Entity Framework, um framework que garante velocidade na comunicação com o banco de dados. Confirme a aplicação cresce, a ineficiência das queries de consulta do EF torna-se um problema, mas a performance de escrita ainda é bastante aceitável.
Neste caso, podemos segregar uma interface de repository, separando as ações de leitura e consulta. Como uma IProductQueryRepository e uma IProductCommandRepository, desta forma garantimos velocidade de desenvolvimento, enquanto refatorando apenas uma menor parte do software. Mas é fundamentável que as interfaces estejam bem construídas, e sejam agnósticas a implementação.
Um caso onde não concordo, é o uso de interfaces em cenários onde nunca haverá outras implementações, ou caso o cenário varie, será sempre na mesma classe concreta. Em casos como Services, que representam a regra de negócio, como são únicas, é complexo achar um cenário onde sua implementação possa variar sem parecer um erro.
O único ponto vantajoso para uso de interfaces em classes de Service, é garantir melhor testabilidade, uma vez que podemos usar um Mock, nos cenários onde uma service requer outra. Mas existem diversas outras estratégias para limitar esses acessos, tais como um MediatR ou criar APIs internas para comunicação entre módulos. Em relação aos testes, vem surgindo uma técnica chamada Interceptors, mas ainda são um recurso experimental, mas promete uma boa mudança na execução de testes de unidade.
Qual sua opinião sobre microserviços e Monólitos
Para mim, Monólitos Modulares devem ser a escolha padrão no desenvolvimento de software, a menos que alguma condição importante seja alcançada. Por exemplo:
- Requisitos de escalabilidade: Requisitos de performance, como escalar isoladamente um modulo da aplicação, sem gerar mais custos para outros. Deve ser levado em consideração, apenas com métricas que garantem que isso seja verdade. E neste caso, apenas o modulo causador deve ser migrado, o resto da aplicação deve continuar como está.
- Organização de pessoas: Onde é complicado para a liderança, gerenciar muitas pessoas dentro de um mesmo projeto, em casos onde separar por
squads, ou grupos focados em determinada ação ou módulo dentro da aplicação. Cada uma “isolada”, no sentido que as interações com outros times é mais restrita. - Requisitos de Linguagem / Framework: Onde existe um requisito já desenvolvido em outra linguagem incompatível com o estado atual da aplicação, por
nmotivos, tais como escolha de arquitetura, facilidade do desenvolvimento, ou performance.
Prefiro aplicações modulares focadas em monolito, e que somente após um dos casos acima, migrem para uma solução com vários serviços. Ajuda a reduzir as “peças móveis” de um sistema, facilitando a extensão e manutenção.
Na minha opinião, o uso de microserviços para segregar responsabilidade, sem se preocupar com os pontos destacados acima é um tiro no pé! Gerador de débito técnico.
A escolha de uma aplicação com design de microserviços É mais complexa se comparada com Monólitos, manter a cooperação de tantas partes, sem “Erros de Build”, gera cenários mais complexos de lidar. Assim, como a falta de Transações, daí surgem soluções como o Saga Pattern.
O que é CI e CD, Você já usou e por que é importante?
CI (Continious Integration) é o ato de rotar uma pipeline que garanta integração da aplicação, como rodar diversos tipos de testes, unitários, de integração, testes de carga, análise de qualidade de código, ou até mesmo, procura de riscos de segurança já catalogados. Garantindo que o código já anteriormente entregue ainda funciona, é seguro, e regras para posterior manutenção ainda existem. Deve ser feito antes de todo Deploy, e caso falhe, deve impedir que novo código chegue a produção, trata-se de um guardrail que garante qualidade mínima de entregas.
CD (Continious Deployment) É a parte da pipeline responsável por preparar a aplicação para publicação, como geração de imagens de container, preparação dos serviços necessários para aplicar a mudança.
Profissionalmente, nunca trabalhei com a parte do CI, pois nunca ouve um ambiente de testes, apenas lidei com a parte de empacotar a aplicação e deploy.
Mas lidei com esses processos de CI descritos acima em projetos de pessoais.
Como você reage às pessoas que criticam seu código/documentos
Fico extremamente empolgado, pois particularmente, adoro discutir sobre soluções e diversas formas de resolver um problema. Erro bastante, até por isso tenho diversos projetos pessoais, onde exploro várias formas de errar.
Caso alguém encontre uma falha em algum processo que fiz, gostaria de discutir melhores soluções.
As críticas mais duras, sempre veem de mim mesmo.
Conte-me sobre alguns dos seus projetos de hobby que você desenvolveu no seu tempo livre
Abaixo uma lista de projetos desenvolvidos por mim mesmo com maior valor agregado.
Aether Cup Lives
Uma aplicação que periodicamente captura lives na twitch sobre um determinado evento de uma comunidade de Call of Duty. A cada procura, salva os dados sobre as transmissões, como número de espectadores, tempo ao vivo, título, jogador etc. Com esses dados gerei métricas sobre o desempenho de cada Streamer, alem de entender a fama do evento em geral.
Após um tempo maior, era foi feito uma query nas lives já encerradas, onde foram coletadas métricas como: totais de visualizações, título, tempo transmitido. Esses dados foram salvos em uma tabela do Postgres (estava aprendendo na época).
A aplicação também permitia acesso do último scan de lives ativas feitas até o momento. Com essas livestreams, fiz um bot de Discord que compartilhava os Streamers ativos no momento no servidor do Evento, servindo como forma de divulgação.
No final, usei minhas métricas para gerar uma lista de usuários que participaram do evento, para uma sessão de menção honrosa aos participantes.
O bot de discord foi desenvolvido em Python, enquanto a API responsável por resgatar os valores em Ruby. Bancos usados foram Postgres e MongoDB.
- Omem
- Urri
- TodoList overengenirring
- Meu blog
- Noite
- Diversos projetos efêmeros discartáveis
Se seu banco de dados estava sob muita pressão, quais as primeiras coisas que você pode considerar para agilizar-lo?
A primeira coisa sempre vai ser ver as métricas, não da pra identificar o problema sem um exame certo?
Problemas de Leitura
Caso o problema seja leitura excessiva.
- Verificar se as queries podem ser mais eficientes, reduzir número de Joins, e dados recuperados.
- identificar as queries que mais geram problema, e examinar se um índice resolve este problema.
- Caso não exista, possibilidade de adicionar um Cache (tal como Redis, ou Memcached) para cachear requisições.
- Em alguns casos, verificar a viabilidade de
Desnormalização de dados, caso aplicável. Com a troca de mais dados redundantes por mais performance de leituras. Como é uma alteração que efetivamente altera o schema do banco, deve ser feia com maiores cuidados - Escalar verticalmente recursos de leitura, em prol de maiores capacidades.
- Criar réplicas de apenas leitura, gerando mais Connection Pools, a troca é de replicação de dados por performance, também adiciona a complexidade de Consistência Eventual, onde dados eventualmente podem estar temporariamente inconsistentes, levando em conta a demora para sincronizar as múltiplas réplicas.
- Usar outro banco para gerar dados Aggregados, e separar a leitura da escrita, conceitos como
Command Query Responsibility Segregation (CQRS). Dados aggregados devem conter todos os dados necessários para operações de domínio em um único objeto. Como requer maior esforço, deve ser uma última alternativa.
Problemas de escrita
Em casos, onde a escrita de dados é gargalo da aplicação
- Filas para processamento assíncrono em batches.
- Em caso de problemas de update devido a lock, usar RowVersion, uma estratégias que torna possível verificar se o estado da linha ainda é válido. Reduzindo e muito, o número de dados travados.
- Escala vertical dos recursos de escrita.
- Escolha de outro banco de dados.