Category Archives: Arquitetura

12 Best System Design Interview Resources

Most of these courses also answer questions I have shared here.

  1. DesignGuru’s Grokking System Design Course: An interactive learning platform with hands-on exercises and real-world scenarios to strengthen your system design skills.
  2. Codemia.io: This is another great platform to practice System design problems for interviews. It has more than 120+ System design problems, many of which are free and it also has a proper structure to solve them.
  3. ByteByteGo: A live book and course by Alex Xu for System design interview preparation. It contains all the content of System Design Interview book volumes 1 and 2 and will be updated with volume 3 which is coming soon.
  4. Exponent: A specialized site for interview prep especially for FAANG companies like Amazon and Google, They also have a great system design course and many other materials that can help you crack FAAN interviews
  5. “System Design Interview” by Alex Xu: This book provides an in-depth exploration of system design concepts, strategies, and interview preparation tips.
  6. “Designing Data-Intensive Applications” by Martin Kleppmann: A comprehensive guide that covers the principles and practices for designing scalable and reliable systems.
  7. LeetCode System Design Tag: LeetCode is a popular platform for technical interview preparation. The System Design tag on LeetCode includes a variety of questions to practice.
  8. “System Design Primer” on GitHub: A curated list of resources, including articles, books, and videos, to help you prepare for system design interviews.
  9. Educative’s System Design Course: An interactive learning platform with hands-on exercises and real-world scenarios to strengthen your system design skills.
  10. High Scalability Blog: A blog that features articles and case studies on the architecture of high-traffic websites and scalable systems.
  11. YouTube Channels: Check out channels like “Gaurav Sen” and “Tech Dummies” for insightful videos on system design concepts and interview preparation.
  12. InterviewReddy.io: This site has been created by Gaurav Sen, an ex-Google engineer, and popular YouTuber and creator of the System Design simplified course. If you are aiming for a FAANG interview, you can also check this website.

Fonte: https://dev.to/somadevtoo/top-3-strategies-for-scaling-microservices-architecture-1m46

Design Patterns

É muito comum que, ao desenvolver um software, alguns desafios acabem surgindo no meio do caminho. No entanto, conforme é possível adquirir experiência ao longo do tempo, torna-se mais fácil determinar quais problemas são mais recorrentes, mesmo que em projetos inteiramente diferentes. Por isso, o Design Patterns é indispensável.

Os padrões que podem ser identificados com a prática levam o nome de “Padrões de Projetos”. Ao nos familiarizarmos com eles, o desenvolvimento de determinados sistemas acaba se tornando muito mais fácil do que era esperado e a chance de acerto é ainda maior.

Com a aplicação do Design Patterns, fica ainda mais fácil conseguir alcançar resultados satisfatórios e que não tragam problemas a curto ou a longo prazo para o seu projeto. É por isso que, neste artigo, você poderá encontrar:

O que são Design Patterns?

Dentro da área de desenvolvimento de software, os Design Patterns são padrões de projetos que podem ser vistos como uma solução geral, atribuindo maior segurança aos programadores e programadoras. Geralmente, eles são aplicados em alguns problemas específicos que ocorrem ao longo da montagem da estrutura de um software ainda em andamento.

É válido ressaltar que esse tipo de projeto não é aplicado em qualquer parte do código da programação e não pode ser utilizado com a famosa fórmula “Ctrl + c/ Ctrl + v”. Ele deve ser visto como um modelo a se seguir para que um determinado problema seja resolvido em alguma situação específica.

Os Design Patterns são a melhor maneira de solucionar problemas comuns por meio de uma pessoa programadora da área de TI, principalmente quando é necessário projetar as peças de um software em uma determinada aplicação ou sistema. Os padrões são capazes de implicar a orientação dos objetos constituintes do programa desenvolvido ou seu estado mutável.

Sabendo o que é Design Patterns, fica muito mais fácil para que a pessoa consiga sair de uma situação incômoda e passe a enxergar os seus projetos de desenvolvimento com muito mais flexibilidade e facilidade.

Origem: conheça a história do Design Patterns

Entre os anos de 1977 e 1979, o arquiteto Christopher Alexander escreveu, em seus livros (Notes on the Synthesis of Form, The Timeless Way of Building e A Pattern Language), um padrão que precisa ter, dentro de um ideal, algumas características. São elas:

  • Generalidade: essa característica aponta que todo padrão deve permitir, de alguma maneira, a construção de outros tipos de realizações a partir de uma base.
  • Equilíbrio: prega que, quando um determinado padrão é usado em uma aplicação, o equilíbrio oferece a razão. Sendo assim, há uma restrição envolvida para cada passo dado no projeto de criação do software.
  • Encapsulamento: a característica de encapsulamento coloca em uma “cápsula” um determinado problema ou uma solução já definida. É válido lembrar que ele pode ser independente, específico e, também, formulado de uma maneira objetiva.
  • Abstração: os padrões estabelecidos representam uma abstração da experiência empírica ou, também, de um conhecimento aplicado no cotidiano.
  • Combinatoriedade: há uma hierarquia entre os padrões. Aqueles de níveis mais altos podem ser formados ou relacionados com padrões que têm problemas de nível mais baixo.
  • Abertura: um determinado padrão precisa permitir uma extensão para chegar a níveis mais baixos de detalhes.

Retomando a história dos Design Patterns, no ano de 1987, dois programadores (Kent Beck e Ward Cunningham) dispuseram os primeiros padrões de projeto para a área voltada à ciência da computação. Dessa maneira, conseguiram apresentar alguns padrões capazes de construir as aplicações comerciais dentro da linguagem Smalltalk.

Ainda assim, a força dos padrões só chegou a ganhar popularidade no ano de 1995, quando o livro Design Patterns: Elements of Reusable Object-Oriented Software foi publicado. Os autores ficaram conhecidos como “Gangue dos Quatro” e, após esse incentivo, muitas outras obras foram lançadas a respeito.

Características de um padrão

Além das características que se aplicam aos padrões, Alexander também definiu alguns formatos para que a descrição de um padrão pudesse ter. Dessa maneira, foi possível facilitar o trabalho de muitas pessoas desenvolvedoras e, ainda por cima, permitir que elas tivessem mais sucesso nos trabalhos que desejavam iniciar.

Essas descrições tinham como base os seguintes conceitos:

  1. Nome: descrição voltada para solução, indo além do problema e do contexto.
  2. Exemplo: caracterizado por uma ou mais figuras, diagramas ou descrições capazes de ilustrar um protótipo de aplicação.
  3. Contexto: capaz de descrever as situações sob as quais os padrões estão se aplicando.
  4. Problema: possibilita a descrição das forças e, também, das restrições envolvidas e como elas interagiam entre si;
  5. Solução: tem relacionamentos estáticos e regras muito dinâmicas. Era capaz de descrever a construção de artefatos levando em consideração um padrão e, também, usando citações de variações e formas capazes de ajustar as soluções de acordo com as circunstâncias nas quais elas se encontravam.

Para quem servem os Design Patterns? Onde são usados?

Agora que já é possível saber o que é Design Patterns, está na hora de compreender como ele funciona na prática e como pode ser de grande ajuda na hora de compor um softwareAinda que possa parecer uma fórmula pronta, esses padrões estão mais para pequenas saídas para problemas corriqueiros no sistema.

Em boa parte dos casos, ele é visto como uma solução reutilizável para os problemas que aparecem no seu sistema, mas precisam estar dentro de um contexto único. Eles servem como um guia para o programador ou programadora, permitindo que o tempo não seja perdido em um determinado tópico, resolvendo-o o mais rápido possível.

Em resumo, eles são utilizados dentro do processo de criação de um software ou programa, visando melhorar a sua qualidade e, ainda por cima, fazer com que a pessoa responsável tenha muito mais tempo para focar em partes relevantes do seu projeto. Assim, é possível deixar de lado os pequenos erros que poderiam atrapalhar a funcionalidade.

De uma maneira geral, os Design Patterns servem como uma boa referência para aqueles e aquelas que estão começando agora ou, simplesmente, desejam ter mais facilidade na criação de seu trabalho. Podem ser usados a qualquer momento, desde que a pessoa compreenda bem onde encaixá-los e quando incluí-los.

Gang Of Four Design Patterns

O Gang Of Four Design Patterns foi criado por 4 autores, sendo eles Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. Esse padrão de design foi apresentado, pela primeira vez, no livro “Design Patterns: Elements of Reusable Object-Oriented Software”, no ano de 1994. Por meio dele, é possível ter uma base de padrões do campo de design para objetos.

O padrão de design nada mais é do que aquela solução que, em algum momento, será reutilizada para resolver um problema que é comumente encontrado em um software. Dessa forma, sempre que for necessário, haverá um modelo disponibilizado para burlar algum empecilho na programação.

Os padrões de design Gang Of Four Design Patterns são seguidos por algumas categorias que, logo abaixo, serão definidas, facilitando a vida de quem programa. Sendo assim, aproveite para aprender um pouco mais e desenvolver maior desenvoltura acerca do tema.

Creational Design Patterns (Design Patterns de Criação)

O Creational Design Patterns, em tradução livre “Design Patterns de Criação” é um padrão de criação que precisa lidar com a criação de objetos. Sendo assim, será necessário encontrar maneiras de resolver os problemas de design decorrentes da criação de algum objeto.

De uma maneira geral, este padrão de Design Patterns tem como principal objetivo construir o objeto e sua referência. Além disso, o grupo se mostra inteiramente relevante para sustentar o princípio que é considerado essencial, referindo-se à programação de uma interface e não de suas implementações.

Neste padrão, é possível encontrar:

  1. Abstract Factory: capacidade de criar famílias de objetos on the fly com uma maior flexibilidade;
  2. Builder: capacidade de construir o produto a partir de um passo-a-passo;
  3. Factory Method: capacidade de criação de objetos on the fly com uma maior flexibilidade;
  4. Prototype: permite que haja a criação de novos objetos a partir de uma cópia do modelo original ou um protótipo;
  5. Singleton: permite a centralização de compartilhamento de recursos.

Structural Design Patterns (Design Patterns de Estrutura)

No design de estrutura, será necessário utilizar os padrões estruturais com o principal objetivo de facilitar a finalização do projeto. Por meio dele, será possível identificar uma maneira bem mais simples de realizar os relacionamentos entre entidades sem necessariamente se deparar com algum erro no meio do caminho.

Outro ponto importante, que pode ser facilmente observado por quem estuda sobre Design Patterns, é que essa categoria trata da relação direta entre os objetos e como eles interagem entre si, visando formar um objeto maior e mais complexo.

Este padrão é composto por:

  1. Adapter: essa funcionalidade possibilita o plugue do conteúdo em um sistema;
  2. Composite: por meio dessa funcionalidade, é possível tratar todos os objetos de uma maneira justa;
  3. Bridge: possibilita a separação de implementações de abstrações em prol da flexibilidade;
  4. Proxy: o proxy faz com que um determinado objeto represente outro;
  5. Decorator: essa funcionalidade incrementa outras funcionalidades de uma maneira bem mais dinâmica;
  6. Facade: facilita a utilização de subsistemas considerados mais complexos;
  7. Flyweight: permite o compartilhamento de pequenos recursos visando economizar um pouco mais de espaço.

Behavioral Patterns (Design Patterns de Comportamento)

Por fim, ainda há o Design Patterns de Comportamento, muito utilizado para definir padrões comportamentais. Por meio desse tipo de produção, é possível identificar padrões de comunicação que são comuns entre objetos e que podem ser capazes de dar continuidade aos padrões anteriormente estabelecidos.

Além disso, eles ainda são responsáveis por uma comunicação direta entre os objetos, principalmente no que diz respeito aos termos de responsabilidade e de algoritmo.

Neste padrão, estão inseridos:

  1. Chain of Responsibility: por meio dessa funcionalidade, há a possibilidade de repassar as requisições, evitando uma dependência entre um objeto e um determinado receptor e o solicitante. Dessa maneira, outros objetos que estão na mesma cadeira poderão ter a oportunidade de tratar essa determinada solicitação;
  2. Command: capacidade de transformar requisições em objetos;
  3. Interpreter: possibilidade de definir uma gramática e um interpretador;
  4. Iterator: capacidade de percorrer um determinado conjunto de dados, sem levar em consideração a sua implementação;
  5. Mediator: capaz de simplificar os relacionamentos complexos;
  6. Memento: possibilidade de externalizar estados sem, necessariamente, quebrar o encapsulamento;
  7. Observer: possibilidade de realizar o compartilhamento de alguns recursos de uma forma mais inteligente;
  8. State: pode ser considerado extremamente importante para simplificar a troca de estados internos de alguns objetos;
  9. Strategy: possibilita a separação dos dados em algoritmos para que sejam reutilizados;
  10. Template Method: define algoritmos com capacidade de extensão;
  11. Visitor: determina uma nova operação para uma classe, mas sem alterá-la.

Outros Design Patterns além do Gang Of Four

É importante relatar que o conceito de padrão de um projeto foi criado por volta dos anos 70, pelo arquiteto Christopher Alexander, como já mencionado anteriormente. Por meio de seus conhecimentos acumulados em uma mesma obra, foi possível estabelecer alguns padrões a serem seguidos pelas pessoas programadoras.

Além do Design Patterns Gang Of Four, também é possível encontrar outros tipos como, por exemplo, o Car Connection, Positive Outdoor e o Connected Buildings. São tantos os modelos que, quando colocados lado a lado, é possível somar mais de 125 padrões. Por um lado, é benéfico para quem programa, já que podem ter mais opções na hora de resolver algum problema de software.

No entanto, quando qualquer tipo de padrão referido se relaciona com o GOF (Gang Of FOur), é válido lembrar que ele atende a um formato específico, sendo ele:

  • Nome;
  • Objetivo;
  • Motivação;
  • Estrutura;
  • Aplicabilidade;
  • Consequências;
  • Implementações;
  • Usos conhecidos;
  • Padrões relacionados.

Dicas de aplicação na prática

Para quem está iniciando no âmbito do Design Patterns, é importante levar em consideração que esse tipo de aplicação pode ser bastante desafiadora em seus princípios, além de apresentar um desenvolvimento de software um pouco mais complexo do que aquele que foi aprendido anteriormente. Sendo assim, é necessário estar atento a todos os detalhes.

Mesmo que não exista uma técnica muito simples para se inserir nesse mundo tecnológico dos códigos, é necessário ressaltar que algumas dicas podem ser suficientes para reduzir a carga de trabalho. Ainda assim, para quem deseja seguir nesse caminho, qualquer pequeno esforço é válido.

Não é necessário usar padrões em todos os projetos

É muito comum que os iniciantes acreditem que todo aplicativo precisa usar padrões de um determinado projeto. No entanto, é importante lembrar que isso não é necessário. Ainda que os padrões sejam muito úteis para solucionar problemas conhecidos, é preciso entender que usá-los acrescenta certa complexidade ao trabalho.

Sendo assim, caso um programador ou programadora esteja escrevendo um projeto pequeno, levando em consideração um script que será usado em apenas uma situação não crítica, aplicar padrões de projeto pode acabar se tornando um verdadeiro exagero e, consequentemente, uma perda de tempo.

Não use vários padrões de uma vez só

Mais um erro cometido por iniciantes quando o assunto é Design Patterns está diretamente relacionado com utilizar vários padrões dentro de um único projeto. Isso não só aumentará a complexidade do desenvolvimento do software como também poderá acrescentar alguns erros que, posteriormente, precisarão ser resolvidos.

Sendo assim, a melhor dica é tentar desenvolver o hábito de identificar determinados padrões que podem ser usados dentro de um mesmo sistema. Assim, será fácil perceber que será preciso usar mais alguns padrões do que outros. Por isso, alguns padrões se tornarão os seus favoritos e mais corriqueiros no projeto.

Prossiga com calma

Para trabalhar com o Design Patterns, é necessário ter muita paciência. Cada passo dado representa uma nova experiência vivida. Por meio do GOF, será possível assimilar muitas coisas e, consequentemente, adquirir desenvoltura para conseguir finalizar os seus projetos sem, necessariamente, realizar tantas consultas a ele.

Ter em mente todos os padrões estabelecidos no Design Patterns pode acabar se mostrando um processo gradual e um tanto lento, além de ser contínuo. Por isso, é importante que, antes de mais nada, a pessoa possa se sentir à vontade com os padrões GOF para que, em seguida, comece a usar padrões do catálogo P do EAA.

Quanto mais exercitar, melhor

Assim como tudo na vida, o aprendizado vem com o treinamento. É por isso que, se você deseja se aprofundar no Design Patterns, precisará reservar um bom tempo do seu dia para exercitar os códigos e padrões pré-estabelecidos. Lembrar padrões não acontecerá de uma hora para a outra.

Uma boa dica de exercício é começar a criar dicas práticas para os padrões utilizados em um projeto. Depois, basta anotá-las com a ajuda de alguma figura ou palavra-chave. Assim, ficará muito mais fácil memorizar os padrões e todos os conceitos que estão atrás dele.

Busque especialistas

Existem muitos profissionais que já estão formalmente acostumados com a utilização do Design Patterns. Sendo assim, é necessário buscá-los caso você também queira se especializar e sair da categoria de iniciante para especialista. Estudar o código escrito por alguém com experiência é um bom primeiro passo.

Dessa forma, será possível compreender como os desenvolvedores ou desenvolvedoras estão aplicando os padrões e como você conseguirá aprender por meio de técnicas individuais. Existem muitos fóruns na internet sobre o assunto e, também, alguns cursos que podem ser muito bem aproveitados pelos programadores com sede de conhecimento.

Entenda manutenção de código

É importante lembrar, principalmente para as pessoas que estão iniciando, que os princípios e padrões SOLID não serão úteis, apenas, durante o desenvolvimento inicial de sua base de código. Isso porque elas poderão se mostrar inteiramente úteis ao estender ou manter uma base de código que já existe.

É por isso que, para quem está se aperfeiçoando no Design Patterns, seja por conta própria ou com a ajuda de especialistas, precisa utilizar a manutenção de códigos como um dos pilares da programaçãoAdote boas práticas e comece a realizar exercícios diariamente para que, enfim, seja possível se tornar um especialista na área.

Por que estudar esse tipo de Design?

Um dos principais motivos para estudar esse tipo de Design está na facilidade que ele oferece. Certo dia, algumas pessoas começaram a perceber que tinham os mesmos problemas durante o processo de programação. Por isso, desenvolveram alguns padrões que fossem capazes de reverter a situação para tornar o projeto mais assertivo.

Ainda que os padrões do Design Patterns GOF tenham sido escritos há algumas décadas, aprender cada vez mais sobre os softwares é extremamente importante. Isso porque os sistemas costumam funcionar como se, de fato, fossem organismos vivos. Por isso, para ser um bom programador ou uma boa programadora, é preciso acompanhar as transformações e se adaptar a elas.

É válido lembrar, também, que nem todos os padrões estabelecidos no GOF são bem aceitos dentro das comunidades de desenvolvimento. Por isso, abusar no uso de padrões dentro de um mesmo projeto pode acabar complicando ainda mais a situação. A supervalorização de qualquer coisa deixa a sua manutenção complexa.

Quais os benefícios de usar os Design Patterns?

Existem muitos benefícios que rondam a utilização dos Design Patterns, mas um deles, com toda a certeza, é o mais relevante: a agilidade. Para quem está desenvolvendo um projeto, códigos que possam ajudar a solucionar um problema rapidamente podem ser muito bem-vindos a qualquer momento.

Além disso, esses padrões determinados são ótimos para ajudar a organizar e manter os projetos em ordem. Eles se baseiam diretamente em um baixo acoplamento entre classes, além de levar em consideração a padronização estabelecida em um código. Cada dia mais surgem técnicas facilitadas para auxiliar o programador.

Depois de compreender melhor o que é Design Patterns, como ele funciona e porque é necessário aprender, é possível observar que esse padrão foi criado para facilitar a vida dos programadores e programadoras. Sendo assim, torna-se indispensável buscar conhecimento acerca da área para solucionar os problemas dos projetos de forma simples, assertiva e ágil.

Entendendo SOLID

Entendendo S.O.L.I.D.: Fundamentos para um Design de Software Eficaz

Introdução

Os princípios S.O.L.I.D. são um conjunto de diretrizes para o design de software orientado a objetos que ajudam a fazer sistemas mais compreensíveis, flexíveis e mantíveis. Criados e popularizados por Robert C. Martin (também conhecido como “Uncle Bob”), esses princípios têm como objetivo melhorar a legibilidade do código, facilitar a manutenção e permitir que o software evolua com menos riscos de introduzir bugs. Este eBook explora cada um dos princípios com detalhes, auxiliando desenvolvedores a aplicá-los efetivamente em projetos sem recorrer a exemplos de código.

1. Princípio da Responsabilidade Única (Single Responsibility Principle – SRP)

Definição e Importância

O Princípio da Responsabilidade Única estabelece que uma classe deve ter uma, e somente uma, razão para mudar. Isso significa que cada classe deve ser responsável por uma única parte da funcionalidade fornecida pelo software. A ideia central é que acoplar múltiplas responsabilidades em uma única classe aumenta o risco de alterações em uma área afetarem outras, levando a bugs e dificuldades na manutenção.

Aplicações Práticas

  • Modularidade: Promove a divisão clara de funcionalidades, facilitando o entendimento e a localização de funcionalidades específicas.
  • Manutenção Facilitada: Reduz complexidade, permitindo alterações sem afetar áreas não relacionadas.
  • Teste Unitário Simplificado: Cada classe, com sua responsabilidade única, torna-se mais fácil de testar.

2. Princípio do Aberto/Fechado (Open/Closed Principle – OCP)

Definição e Importância

O Princípio do Aberto/Fechado declara que o software deve ser aberto para extensão, mas fechado para modificação. Isso significa que a funcionalidade de uma classe deve ser estendida sem alterar seu código existente, promovendo a criação de sistemas que podem crescer com o tempo sem sacrificar estabilidade.

Aplicações Práticas

  • Extensibilidade: Permite adicionar novas funcionalidades através de novas classes ou métodos, preservando o comportamento do código existente.
  • Segurança do Código: Minimiza os riscos de modificar código testado e em produção.
  • Evolução do Projeto: Facilita a adaptação a requisitos futuros sem reescrever componentes existentes.

3. Princípio da Substituição de Liskov (Liskov Substitution Principle – LSP)

Definição e Importância

Segundo o Princípio da Substituição de Liskov, objetos de uma classe devem poder ser substituídos por objetos de suas subclasses, sem alterar a correção do programa. Este princípio assegura que uma classe derivada pode ser usada no lugar de sua classe base sem comprometer a funcionalidade.

Aplicações Práticas

  • Polimorfismo: Garante que o comportamento do sistema permaneça consistente usando subclasses.
  • Flexibilidade no Design: Facilita a implementação de novas funcionalidades através de hierarquias de classes.
  • Consistência e Robustez: Evita a violação de contratos estabelecidos por classes base, promovendo sistemas mais confiáveis.

4. Princípio da Segregação de Interface (Interface Segregation Principle – ISP)

Definição e Importância

O Princípio da Segregação de Interface sugere que os clientes não devem ser forçados a depender de interfaces que não utilizam. Isso implica que é melhor criar interfaces mais específicas e focadas, ao invés de usar interfaces “gordas” e genéricas.

Aplicações Práticas

  • Clareza: Assegura que as interfaces estão claramente definidas e são relevantes para seus consumidores.
  • Redução de Dependências: Minimiza o acoplamento entre componentes do sistema, permitindo maior flexibilidade para mudanças.
  • Manutenabilidade: Torna o sistema mais fácil de refatorar e evoluir sem modificar desnecessariamente classes não relacionadas.

5. Princípio da Inversão de Dependência (Dependency Inversion Principle – DIP)

Definição e Importância

O Princípio da Inversão de Dependência afirma que módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações, que não devem depender de detalhes. Este princípio incentiva o uso de interfaces ou classes abstratas para desacoplar componentes do sistema.

Aplicações Práticas

  • Desacoplamento: Facilita o isolamento de diferentes camadas do software, promovendo uma arquitetura mais limpa.
  • Testabilidade: Simplifica o processo de teste, permitindo a substituição fácil de implementações reais por mocks ou stubs.
  • Flexibilidade e Reutilização: Torna mais fácil substituir componentes sem alterar o código existente.

Conclusão

Os princípios S.O.L.I.D. são fundamentais para a construção de software robusto, escalável e fácil de manter. Ao aplicar esses princípios, os desenvolvedores podem criar sistemas mais modulares e adaptáveis às mudanças, garantindo que os investimentos em software sejam protegidos ao longo do tempo. Em um cenário de desenvolvimento contínuo, onde mudanças e requisitos novos são constantes, compreender e aplicar S.O.L.I.D. é essencial para garantir a longevidade e o sucesso dos projetos de software.

Próximos Passos

Para aprofundar seu conhecimento e aplicar eficazmente os princípios S.O.L.I.D., considere as seguintes ações:

  • Estudo Continuado: Explore livros e cursos que abordam design de software orientado a objetos.
  • Discussões e Comunidades: Participe de fóruns e grupos de discussão para trocar experiências e aprender com outros profissionais.
  • Prática Constante: Almeje implementar esses princípios em projetos reais, buscando sempre revisar e refatorar o código para melhor aderência aos princípios.

Espero que este eBook sirva como um guia útil e inspirador em sua jornada no desenvolvimento de software de alta qualidade!

Padrões GoF

Padrões de Design do GoF

Categorias Principais

  1. Padrões Criacionais: Focam na criação de objetos
    • Singleton: Garante uma única instância de uma classe
    • Factory Method: Cria objetos sem especificar a classe exata
    • Abstract Factory: Cria famílias de objetos relacionados
    • Builder: Constrói objetos complexos passo a passo
    • Prototype: Cria novos objetos clonando existentes
  2. Padrões Estruturais: Lidam com composição de classes e objetos
    • Adapter: Converte interface de uma classe em outra esperada
    • Bridge: Separa uma abstração de sua implementação
    • Composite: Compõe objetos em estruturas de árvore
    • Decorator: Adiciona responsabilidades a objetos dinamicamente
    • Facade: Fornece interface simplificada para um subsistema
    • Flyweight: Compartilha objetos para economizar memória
    • Proxy: Fornece um substituto para outro objeto
  3. Padrões Comportamentais: Descrevem comunicação entre objetos
    • Chain of Responsibility: Passa uma solicitação ao longo de uma cadeia de handlers
    • Command: Transforma uma solicitação em um objeto independente
    • Interpreter: Implementa uma linguagem especializada
    • Iterator: Acessa elementos de uma coleção sequencialmente
    • Mediator: Reduz dependências diretas entre objetos
    • Memento: Captura e restaura o estado interno de um objeto
    • Observer: Define uma dependência um-para-muitos entre objetos
    • State: Permite que um objeto altere seu comportamento
    • Strategy: Define uma família de algoritmos intercambiáveis
    • Template Method: Define o esqueleto de um algoritmo
    • Visitor: Separa um algoritmo de uma estrutura de objetos

Referência

Livro: “Design Patterns: Elements of Reusable Object-Oriented Software” Autores: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides Publicado em 1994

Princípio Fundamental

Os padrões de design visam tornar o código mais flexível, reutilizável e manutenível, resolvendo problemas recorrentes no desenvolvimento de software orientado a objetos.

Design Interview 80-20

𝟭. 𝗦𝗰𝗮𝗹𝗮𝗯𝗹𝗲 𝗗𝗮𝘁𝗮 𝗦𝘁𝗼𝗿𝗮𝗴𝗲
• Relational vs. NoSQL: Know when to use SQL vs. NoSQL databases.
• Partitioning: Vertical and horizontal partitioning (sharding). Understand trade-offs.
• Indexing: Covering indexes, primary vs. secondary indexes.
• Consistency Models: Strong, eventual, causal.

𝟐. 𝐂𝐚𝐜𝐡𝐢𝐧𝐠
• Client-side vs. Server-side Cache: Understand where caching should happen.
• Caching Strategies: Write-through, write-back, write-around.
• Distributed Cache: Redis, Memcached.
• Cache Eviction Policies: LRU, LFU, etc.

𝟑. 𝐋𝐨𝐚𝐝 𝐁𝐚𝐥𝐚𝐧𝐜𝐢𝐧𝐠
• Horizontal Scaling: Why and how to horizontally scale services.
• Load Balancing Techniques: Round-robin, consistent hashing.
• Reverse Proxy: Understand how to use Nginx, HAProxy.

𝟒. 𝐀𝐬𝐲𝐧𝐜𝐡𝐫𝐨𝐧𝐨𝐮𝐬 𝐏𝐫𝐨𝐜𝐞𝐬𝐬𝐢𝐧𝐠
• Message Brokers: Kafka, RabbitMQ. When to use queues vs. streams.
• Event-Driven Architecture: Benefits of decoupling and event sourcing.
• Task Queues: For delayed jobs or retries.

𝟓. 𝐃𝐚𝐭𝐚𝐛𝐚𝐬𝐞 𝐑𝐞𝐚𝐝 𝐚𝐧𝐝 𝐖𝐫𝐢𝐭𝐞 𝐒𝐜𝐚𝐥𝐢𝐧𝐠
• Read Scaling: Master replication, read replicas.
• Write Scaling: Challenges with partitioning for writes, leader-election.
• CAP Theorem: Consistency, Availability, or Partition tolerance may be compromised.

𝟲. 𝗗𝗶𝘀𝘁𝗿𝗶𝗯𝘂𝘁𝗲𝗱 𝗦𝘆𝘀𝘁𝗲𝗺𝘀 𝗖𝗼𝗻𝗰𝗲𝗽𝘁𝘀
• Consensus Algorithms: Paxos, Raft.
• Conflict Resolution: Last Write Wins, CRDTs, vector clocks for data reconciliation.

𝟳. 𝗥𝗲𝗹𝗶𝗮𝗯𝗶𝗹𝗶𝘁𝘆 𝗮𝗻𝗱 𝗙𝗮𝗶𝗹𝗼𝘃𝗲𝗿
• Redundancy: Active-passive vs. active-active configurations.
• Health Checks.
• Retries and Circuit Breakers: How to protect systems from cascading failures.

𝟴. 𝗖𝗗𝗡𝘀 (𝗖𝗼𝗻𝘁𝗲𝗻𝘁 𝗗𝗲𝗹𝗶𝘃𝗲𝗿𝘆 𝗡𝗲𝘁𝘄𝗼𝗿𝗸𝘀)
• Static Content Delivery: Why use a CDN, how does it work?
• Caching at the Edge: How CDNs improve latency for end users.

𝟵. 𝗔𝗣𝗜 𝗗𝗲𝘀𝗶𝗴𝗻 𝗮𝗻𝗱 𝗥𝗮𝘁𝗲 𝗠𝗮𝗻𝗮𝗴𝗲𝗺𝗲𝗻𝘁
• REST vs. GraphQL: Difference and practical use-cases for each.
• Pagination and Filtering: Strategies for efficiently fetching data.
• API Versioning: Best practices for evolving APIs.
• Throttle Requests: Why rate limiting is essential, algorithms like token bucket, leaky bucket.

𝟭𝟬. 𝗦𝗲𝗮𝗿𝗰𝗵 𝗦𝘆𝘀𝘁𝗲𝗺𝘀
• Indexing: Building and maintaining indexes for fast search.
• Full-Text Search Engines: ElasticSearch, Azure AI Search.
• Ranking and Relevance: Basic understanding of how scoring works.

𝟭𝟭. 𝗠𝗼𝗻𝗶𝘁𝗼𝗿𝗶𝗻𝗴 𝗮𝗻𝗱 𝗢𝗯𝘀𝗲𝗿𝘃𝗮𝗯𝗶𝗹𝗶𝘁𝘆 𝗮𝗻𝗱 𝗦𝗲𝗰𝘂𝗿𝗶𝘁𝘆
• Metrics Collection: Prometheus, Grafana.
• Distributed Tracing: OpenTelemetry, Sentry.
• Centralized Logging.
• Authentication and Authorization: OAuth, JWT.
• Encryption: Data in transit vs. data at rest.

If you master these 11 areas, you’ll be ready for most system design interviews thrown at you.

Clean Code – 7 princípios

Conheça as 7 principais princípios do Clean Code

No livro do Uncle Bob foram listadas algumas boas práticas para se obter um código limpo. Os principais são:

1 – Nomes são muito importantes

A definição de nome é essencial para o bom entendimento de um código. Aqui, não importa o tipo de nome, seja ele:

  • Variável;
  • Função;
  • Parâmetro;
  • Classe;
  • Método.

Ao definir um nome, é preciso ter em mente 2 pontos principais:

  1. Ele deve ser preciso e passar logo de cara sua ideia central. Ou seja, deve ir direto ao ponto;
  2. Não se deve ter medo de nomes grandes. Se a sua função ou parâmetro precisa de um nome extenso para demonstrar o que realmente representa, é o que deve ser feito.

2 – Regra do escoteiro

Há um princípio do escotismo que diz que, uma vez que você sai da área em que está acampando, você deve deixá-la mais limpa do que quando a encontrou.

Trazendo a regra para o mundo da programação, a regra significa deixar o código mais limpo do que estava antes de mexer nele.

3 – Seja o verdadeiro autor do código

O ser humano é acostumado a pensar de forma narrativa , portanto, o código funciona da mesma forma. Logo, ele é uma história e, como os programadores são seus autores, precisam se preocupar na maneira com que ela será contada.

Em resumo, para estruturar um código limpo, é necessário criar funções simples, claras e pequenas. Existem 2 regras para criar a narrativa via código:

  1. As funções precisam ser pequenas;
  2. Elas têm de ser ainda menores.

Não confunda com os termos “nome” e “função”. Como dissemos no primeiro princípio, nomes grandes não são um problema. Já as funções precisam ser as menores possíveis.

4 – DRY (Don’t Repeat Yourself)

Esse princípio pode ser traduzido como “não repita a si mesmo”. Essa expressão foi descrita pela primeira vez em um livro chamado The Pragmatic Programmer e se aplica a diversas áreas de desenvolvimento, como:

  • Banco de Dados;
  • Testes;
  • Documentação;
  • Codificação.

O DRY diz que cada pedaço do conhecimento de um sistema deve ter uma representação única e ser totalmente livre de ambiguidades. Em outras palavras, define que não pode existir duas partes do programa que desempenhem a mesma função.

5 – Comente apenas o necessário

Esse princípio afirma que comentários podem ser feitos, porém, se forem realmente necessários. Segundo Uncle Bob, os comentários mentem. E isso tem uma explicação lógica.

O que ocorre é que, enquanto os códigos são constantemente modificados, os comentários não. Eles são esquecidos e, portanto, deixam de retratar a funcionalidade real dos códigos.

Logo, se for para comentar, que seja somente o necessário e que seja revisado juntamente com o código que o acompanha.

6 – Tratamento de erros

Tem uma frase do autor Michael Feathers, muito conhecido na área de desenvolvimento, que diz que as coisas podem dar errado, mas, quando isso ocorre, os programadores são os responsáveis por garantir que o código continuará fazendo o que precisa.

Ou seja: saber tratar as exceções de forma correta é um grande e importante passo para um programador em desenvolvimento.

7 – Testes limpos

Testar, na área de programação, é uma etapa muito importante. Afinal, um código só é considerado limpo após ser validado através de testes – que também devem ser limpos.

Por isso, ele deve seguir algumas regras, como:

  • Fast: O teste deve ser rápido, permitindo que seja realizado várias vezes e a todo momento;
  • Independent: Ele deve ser independente, a fim de evitar que cause efeito cascata quando da ocorrência de uma falha – o que dificulta a análise dos problemas;
  • Repeatable: Deve permitir a repetição do teste diversas vezes e em ambientes diferentes;
  • Self-Validation: Os testes bem escritos retornam com as respostas true ou false, justamente para que a falha não seja subjetiva;
  • Timely: Os testes devem seguir à risca o critério de pontualidade. Além disso, o ideal é que sejam escritos antes do próprio código, pois evita que ele fique complexo demais para ser testado.

 

O Clean Code é um conceito que veio para ficar. Afinal, seus princípios solucionam com eficácia um dos principais problemas que grande parte dos projetos de sistemas enfrentam: a manutenção.

Clean Code

Clean Code

No universo da programação, frequentemente nos deparamos com o termo: Clean Code ou Código Limpo.

Mas o que exatamente é um “código limpo”? Quais características são necessárias para obtê-lo?

Escrever um código limpo significa escrever códigos de um jeito que conseguimos entendê-lo sem complicação.

Isso não apenas simplifica a manipulação do código, mas também facilita a colaboração entre o time. No fim das contas, todo desenvolvimento e manutenção do sistema também se torna mais fácil.

De acordo com “Uncle Bob”, em seu livro “Código Limpo: Habilidades Práticas do Software Ágil”, existem algumas boas práticas fundamentais para alcançar a clareza do código.

Vamos conhecê-las, a seguir:

Utilizar os princípios SOLID:

O Clean Code e os princípios SOLID compartilham o objetivo de melhorar a qualidade do software, tornando-o legível, organizado, extensível e fácil de manter.

Neste episódio do #HipstersPontoTube sobre Clean Code e Solid, o host Paulo Silveira bate um papo com o Alberto Sousa para discutir se é necessário seguir essas práticas à risca para desenvolver um bom projeto.

Possuir nomes significativos

Nomes descritivos ajudam a entender a finalidade de uma parte do código sem a necessidade de comentários explicativos.

Para ilustrar, considere o código a seguir:

public static double conv(double tC) {
    double tF = (tC * 9 / 5) + 32;
    return tF;
}

Temos que nos esforçar para entender o que o código acima faz. Podemos melhorar o entendimento apenas adicionando nomes significativos para as variáveis e para o método:

public static double converterCelsiusParaFahrenheit(double temperaturaCelsius) {
    double temperaturaFahrenheit = (temperaturaCelsius * 9 / 5) + 32;
    return temperaturaFahrenheit;
}

Agora, fica claro qual é o propósito do código, sem a necessidade de se lembrar de fórmulas ou realizar pesquisas adicionais. Isso economiza tempo e evita confusões desnecessárias.

Priorizar o uso de funções pequenas

Escrever métodos ou funções pequenas e focadas em uma única tarefa é fundamental para manter o código claro e seguir o princípio da responsabilidade única (SRP).

Para ilustrar, considere o código a seguir:

public class Main {

    public static void main(String[] args) {
        int[] numeros = {1, 2, 3, 4, 5};

        int soma = 0;
        for (int numero : numeros) {
            soma += numero;
        }

        double media = (double) soma / numeros.length;

        if (media > 3) {
            System.out.println("A média é maior que 3");
        } else {
            System.out.println("A média é menor ou igual a 3");
        }
    }
}

Apesar do uso de nomes descritivos, a legibilidade poderia ser melhorada dividindo as tarefas em funções distintas, cada uma com sua descrição. Por exemplo:

public class Main {

    public static void main(String[] args) {
        int[] numeros = {1, 2, 3, 4, 5};

        int soma = calcularSoma(numeros);
        double media = calcularMedia(numeros);
        verificarEMostrarResultado(media);
    }

    public static int calcularSoma(int[] numeros) {
        int soma = 0;
        for (int numero : numeros) {
            soma += numero;
        }
        return soma;
    }

    public static double calcularMedia(int[] numeros) {
        return (double) calcularSoma(numeros) / numeros.length;
    }

    public static void verificarEMostrarResultado(double media) {
        if (media > 3) {
            System.out.println("A média é maior que 3");
        } else {
            System.out.println("A média é menor ou igual a 3");
        }
    }
}

Embora o código tenha ficado maior, ganhamos em legibilidade e segmentação. Qualquer pessoa que precise alterar a maneira como a média é exibida à pessoa usuária, só precisa modificar o método verificarEMostrarResultado.

Isso demonstra como funções pequenas podem facilitar a manutenção e a compreensão do código.

Evitar comentários desnecessários

O código deve ser autoexplicativo, com nomes significativos e estrutura lógica clara. Comentários excessivos podem tornar o código poluído e difícil de manter.

Para ilustrar, considere o código a seguir:

public class R {
    private double w;
    private double h;

    // Método para calcular a área
    public double calc() {
        return w * h;
    }
}

Os nomes curtos para as variáveis dificultam o entendimento, fazendo necessário o uso de comentários no nosso código, deixando o nosso código sujo. Então, podemos resolver isso adicionando nomes descritivos e removendo os comentários:

public class Retangulo {
    private double largura;
    private double altura;

    public Retangulo(double largura, double altura) {
        this.largura = largura;
        this.altura = altura;
    }

    public double calcularArea() {
        return largura * altura;
    }
}

Pronto, perceba como facilitou o entendimento. Qualquer pessoa desenvolvedora que ler este código consegue assimilar o que cada parte faz.

Evitar complexidade

A complexidade desnecessária pode aumentar a chance de erros e tornar o código difícil de manter. Um exemplo de código complexo para fazer algo simples, como somar dois números, seria:

public void soma() {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Digite o primeiro número: ");
        String num1String = scanner.nextLine();

        System.out.print("Digite o segundo número: ");
        String num2String = scanner.nextLine();

        boolean validInput = false;
        double num1 = 0;
        double num2 = 0;

        while (!validInput) {
            num1 = Double.parseDouble(num1String);
            num2 = Double.parseDouble(num2String);
            validInput = true;
        }

        double soma = num1 + num2;

        System.out.println("A soma dos números é: " + soma);

        scanner.close();
    }

Repare que é feita uma verificação da entrada, para só depois convertê-la em double.

Poderíamos simplesmente considerar que é esperado que o usuário digite um double e fazer o tratamento de exceções relacionado a isso:

import java.util.InputMismatchException;
import java.util.Scanner;

public class SimpleSumWithErrorHandling {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        try {
            System.out.print("Digite o primeiro número: ");
            double num1 = scanner.nextDouble();

            System.out.print("Digite o segundo número: ");
            double num2 = scanner.nextDouble();

            double soma = num1 + num2;

            System.out.println("A soma dos números é: " + soma);
        } catch (InputMismatchException e) {
            System.out.println("Erro: Por favor, digite números válidos.");
        } finally {
            scanner.close();
        }
    }
}

Executamos a lógica desejada de forma rápida e fácil de compreender. Imagine que precisamos somar agora 3 variáveis, ao invés de duas.

É mais fácil modificar o segundo código do que o primeiro. O segundo código é um exemplo de código limpo.

Fazer o mínimo de argumentos

Funções e métodos devem ter o mínimo possível de argumentos. Isso melhora a legibilidade e a facilidade de uso.

No exemplo a seguir, note como cadastrar uma pessoa colaboradora é complexo ao passar muitos parâmetros:

public static void cadastrarFuncionario(String nome, int idade, String cargo, double salario, String endereco, String cidade, String cep, String telefone, String email) {
        // Lógica de cadastro do funcionário aqui...

    }

Ao chamar esse método, é difícil entender qual parâmetro utilizar em que lugar, podendo confundi-los, por exemplo.

Uma boa alternativa seria criar uma classe para representar o funcionário, outra para o endereço, e mais uma para o contato. Assim, faremos a divisão:

public static void cadastrarFuncionario(Funcionario funcionario, Endereco endereco, Contato contato){
}

Dessa forma, conseguimos agrupar as informações para que seja possível usar menos argumentos. Essa é uma boa prática

Evitar código com repetição

A repetição torna o código difícil de manter, pois quando há mudanças necessárias elas precisam ser aplicadas em múltiplos lugares.

Então, extraia código repetido em funções ou métodos para promover a reutilização e a manutenção eficiente.

Um exemplo disso:

public static void main(String[] args) {
        int numero1 = 5;
        int numero2 = 7;

        // Cálculo do fatorial para o primeiro número
        int resultado1 = 1;
        for (int i = 1; i <= numero1; i++) {
            resultado1 *= i;
        }
        System.out.println("Fatorial de " + numero1 + ": " + resultado1);

        // Cálculo do fatorial para o segundo número
        int resultado2 = 1;
        for (int i = 1; i <= numero2; i++) {
            resultado2 *= i;
        }
        System.out.println("Fatorial de " + numero2 + ": " + resultado2);
    }

Como calculamos o fatorial mais de uma vez, extraímos o código para uma função, evitando repetições de cálculo no código:

public static void main(String[] args) {
        int numero1 = 5;
        int numero2 = 7;

        // Cálculo e impressão do fatorial para o primeiro número
        calcularEImprimirFatorial(numero1);

        // Cálculo e impressão do fatorial para o segundo número
        calcularEImprimirFatorial(numero2);
    }

    // Função para calcular e imprimir o fatorial de um número
    public static void calcularEImprimirFatorial(int numero) {
        int resultado = 1;
        for (int i = 1; i <= numero; i++) {
            resultado *= i;
        }
        System.out.println("Fatorial de " + numero + ": " + resultado);
    }

Dessa forma, se precisarmos calcular novamente o fatorial de outro número, não precisaremos repetir código. Basta chamar a função de calcular fatorial novamente. Isso facilita o desenvolvimento.

Vantagens de deixar o código limpo

Ao implementar cada um desses princípios e práticas, você não apenas irá melhorar a qualidade do seu código, mas também facilitará a compreensão, a manutenção e a colaboração no desenvolvimento de software. Tudo isso que discutimos aqui resume uma frase dita por Martin Fowler:

“Qualquer tolo pode escrever código que um computador pode entender. Bons programadores escrevem código que humanos podem entender”.

A programação não se trata apenas de fazer a máquina funcionar, mas também de criar soluções que sejam compreensíveis e colaborativas.

SOLID

O que é SOLID?

O acrônimo SOLID representa os cinco princípios que facilitam o processo de desenvolvimento — o que facilita a manutenção e a expansão do software.

Estes princípios são fundamentais na programação orientada a objetos e podem ser aplicados em qualquer linguagem que adote este paradigma.

Os 5 princípios são:

  • S — Single Responsibility Principle (Princípio da responsabilidade única)
  • O — Open-Closed Principle (Princípio Aberto-Fechado)
  • L — Liskov Substitution Principle (Princípio da substituição de Liskov)
  • I — Interface Segregation Principle (Princípio da Segregação da Interface)
  • D — Dependency Inversion Principle (Princípio da inversão da dependência)

Origem dos princípios SOLID

O primeiro indício dos princípios SOLID apareceu em 1995, no artigo “The principles of OoD” de Robert C Martin, também conhecido como “Uncle Bob”.

Nos anos seguintes, Robert se dedicou a escrever mais sobre o tema, consolidando esses princípios de forma categórica.

E, em 2002, lançou o livro “Agile Software Development, Principles, Patterns, and Practices” que reúne diversos artigos sobre o tema.

Agora que você já sabe o significado da sigla e a origem dos princípios, é importante dar um passo para trás para compreender o paradigma da Programação Orientada a Objetos (POO). Afinal de contas, como você já sabe, através da POO é possível aplicar os princípios SOLID.

Princípio da Responsabilidade Única (S – Single Responsibility Principle)

Para entender o princípio da responsabilidade única, vamos pensar no desenvolvimento de um gerenciador de tarefas. Vamos começar com o seguinte código:

public class GerenciadorTarefas {

    public String conectarAPI(){
        //...
    }
    public void criarTarefa(){
        //...
    }

    public void atualizarTarefa(){
        //...
    }

    public void removerTarefa(){
        //...
    }

    public void enviarNotificacao(){
        //...
    }

    public void produzirRelatorio(){
        //...
    }

    public void enviarRelatorio(){
        //...
    }

}

Problemática

Tente enumerar todas as funções que a classe GerenciadorTarefas tem. Ela é responsável por lidar com todas as operações das tarefas em si e também está consumindo uma API, enviando notificações para pessoas usuárias e ainda gerando relatórios da aplicação.

Pense na Orientação a Objetos. Um objeto gerenciador de tarefas deveria enviar e-mails e gerar relatórios? Não! Um gerenciador de tarefas gerencia as tarefas, não e-mails ou relatórios.

Solução

Para resolver esse problema vamos criar classes diferentes, cada uma representando uma função.

Nossa classe GerenciadorTarefas terá apenas o código relacionando a operação com tarefas. Outras operações estarão em outras classes. E cada classe será responsável por uma parte diferente da aplicação.

Assim, teremos a classe GerenciadorTarefas refatorada:

public class GerenciadorTarefas {

    public void criarTarefa(){
        //...
    }

    public void atualizarTarefa(){
        //...
    }

    public void removerTarefa(){
        //...
    }
}

Assim, vamos criar uma classe para consumir uma API externa, outra classe para enviar notificações e uma última classe para lidar com os relatórios.

public class ConectorAPI {

    public String conectarAPI() {
        //...
    }

}
public class Notificador {

    public void enviarNotificacao() {
        //...
    }

} 
public class GeradorRelatorio {
    public void produzirRelatorio(){
        //...
    }

    public void enviarRelatorio(){
        //...
    }

}

Talvez você se pergunte se as classes não são pequenas demais. Nesse caso, não estão. Cada classe reflete exatamente a responsabilidade que ela tem.

Se precisarmos adicionar algum método, por exemplo, relacionado ao consumo da API, vamos saber exatamente em qual parte do código devemos ir. Ou seja, fica muito mais fácil alterar o que for preciso.

Definição do Princípio da Responsabilidade Única

Em resumo, o princípio da responsabilidade única diz que: “Cada classe deve ter um, e somente um, motivo para mudar.”

Se uma classe tem várias responsabilidades, mudar um requisito do projeto pode trazer várias razões para modificar a classe. Por isso, as classes devem ter responsabilidades únicas.

Esse princípio pode ser estendido para os métodos que criamos também. Quanto mais tarefas um método executa, mais difícil é testá-lo e garantir que o programa está em ordem.

Uma dica para aplicar o princípio na prática é tentar nomear suas classes ou métodos com tudo que eles são capazes de fazer.

Se o nome está gigante, como GerenciadorTarefasEmailsRelatorios, temos um sinal de que o código pode ser refatorado.

Vantagens de aplicar o Princípio da Responsabilidade Única

Existem vários benefícios ao aplicar esse princípio, principalmente:

  • Facilidade para fazer manutenções
  • Reusabilidade das classes
  • Facilidade para realizar testes
  • Simplificação da legibilidade do código

Princípio Aberto-Fechado (O – Open Closed Principle)

Para entender o Princípio Aberto-Fechado (a letra O da sigla), vamos pensar que estamos trabalhando no sistema de uma clínica médica.

Nessa clínica, existe uma classe que trata das solicitações de exames. Inicialmente, o único exame possível é o exame de sangue. Por isso, temos o código:

public class AprovaExame {
    public void aprovarSolicitacaoExame(Exame exame){
        if(verificaCondicoesExameSangue(exame))
            System.out.println("Exame aprovado!");
    }
    public boolean verificaCondicoesExameSangue(){
        //....
    }
}

Agora, precisamos incluir uma nova funcionalidade ao sistema: a clínica vai começar a fazer exames de Raio-X. Como incluir isso no nosso código?

Uma alternativa seria verificar qual o tipo de exame está sendo feito para poder aprová-lo:

public class AprovaExame {
    public void aprovarSolicitacaoExame(Exame exame){
        if(exame.tipo == SANGUE){
            if(verificaCondicoesExameSangue(exame))
                System.out.println("Exame sanguíneo aprovado!");
        } else if(exame.tipo == RAIOX) {
            if (verificaCondicoesRaioX(exame))
                System.out.println("Raio X aprovado!");
        }

    }
    private boolean verificaCondicoesExameSangue(){
        //....
    }

    private boolean verificaCondicoesRaioX(){
        //....
    }
}

Problemática

A princípio parece tudo certo, não é mesmo? Nosso código executa normalmente e conseguimos adicionar a funcionalidade corretamente.

Mas, e se além de raio-x, a clínica passasse a fazer também ultrassons? Seguindo a lógica, iríamos adicionar mais um if no código e mais um método para olhar condições específicas do exame.

Essa definitivamente não é uma boa estratégia. Cada vez que incluir uma função, a classe (e o projeto como um todo) vai ficar mais complexa.

Por isso, é necessário uma estratégia para adicionar mais recursos ao projeto, sem modificar e bagunçar a classe original.

Solução

Nesse cenário, o projeto compreende vários tipos de aprovação de exames. Assim, podemos criar uma classe ou uma interface que representa uma aprovação de forma genérica.

A cada tipo de exame fornecido pela clínica, é possível criar novos tipos de aprovação, mais específicos, que irão implementar a interface. Assim, podemos ter o código:

public interface AprovaExame{
    void aprovarSolicitacaoExame(Exame exame);
    boolean verificaCondicoesExame(Exame exame);

}
public class AprovaExameSangue implements AprovaExame{
    @Override
    public void aprovarSolicitacaoExame(Exame exame){
            if(verificaCondicoesExame(exame))
                System.out.println("Exame sanguíneo aprovado!");

    }
    @Override
    boolean verificaCondicoesExame(Exame exame){
        //....
    }
}
public class AprovaRaioX implements AprovaExame{
    @Override
    public void aprovarSolicitacaoExame(Exame exame){
        if(verificaCondicoesExame(exame))
            System.out.println("Raio-X aprovado!");

    }
    @Override
    boolean verificaCondicoesExame(Exame exame){
        //....
    }
}

Agora, como a interface representa a aprovação de um exame, para incluir mais um recurso ou mais um tipo de exame, basta criar uma nova classe que implementa a interface AprovaExame. Essa classe vai representar como o novo exame é aprovado.

Repare que sempre será possível implementar a interface AprovaExame ao adicionarmos recursos. Essa interface, no entanto, não muda. Estamos estendendo-a, mas não alterando.

Definição do Princípio Aberto-Fechado

Assim, é possível definir o Princípio Aberto-Fechado como: “entidades de software (como classes e métodos) devem estar abertas para extensão, mas fechadas para modificação”.

Ou seja, se uma classe está aberta para modificação, quanto mais recursos adicionarmos, mais complexa ela vai ficar.

O ideal é adaptar o código não para alterar a classe, mas para estendê-la. Em geral, isso é feito quando abstraímos um código para uma interface.

Aplicando o Open-Closed, é possível deixar o nosso código semelhante ao mundo real, praticando de maneira sólida a orientação a objetos.

Pense em um caminhão: toda a sua implementação, como motor, bateria e cabine é fechada para modificação.

Vantagens de aplicar o Princípio Aberto-Fechado

Ao aplicar esse princípio, é possível tornar o projeto muito mais flexível. Adicionar novas funcionalidades torna-se uma tarefa mais fácil.

Além disso, os códigos ficam mais simples de ler. Com isso tudo, o risco de introduzir bugs diminui de forma significativa.

Além disso, esse princípio nos faz caminhar diretamente para a aplicação de alguns padrões de projeto, como o Strategy.

Assim, alinhamos várias boas práticas de desenvolvimento. O resultado disso é um código cada vez mais limpo e organizado.

Princípio de Substituição de Liskov (L – Liskov Substitution Principle)

Para entender o Princípio de Substituição de Liskov (a letra L da sigla), vamos pensar no seguinte cenário: o desenvolvimento de um sistema de uma faculdade.

Dentro do sistema, há uma classe-mãe Estudante, que representa um estudante de graduação, e a filha dela, EstudantePosGraduacao, tendo o seguinte código:

public class Estudante {
    String nome;

    public Estudante(String nome) {
        this.nome = nome;
    }

    public void estudar() {
        System.out.println(nome + " está estudando.");
    }
}
public class EstudanteDePosGraduacao extends Estudante {

    @Override
    public void estudar() {
        System.out.println(nome + " está estudando e pesquisando.");
    }
}

Para adicionar a funcionalidade entregarTCC() ao sistema, basta colocar esse método na classe Estudante O código fica assim:

class Estudante {
    String nome;

    public Estudante(String nome) {
        this.nome = nome;
    }

    public void estudar() {
        System.out.println(nome + " está estudando.");
    }

    public void entregarTCC(){
    //…
    }

}

Problemática

Você provavelmente já percebeu algo errado no código. Normalmente, estudantes de pós-graduação não entregam TCCs.

Só que a classe EstudanteDePosGraduacao é filha de Estudante, e portanto, deve apresentar todos os comportamentos dela.

Uma alternativa seria sobrescrever o método entregarTCC na classe EstudanteDePosGraduacao lançando uma exceção.

No entanto, continuaria sendo problemático: a classe EstudanteDePosGraduacao ainda não teria os comportamentos iguais aos de Estudante.

O ideal é que, nos lugares que estiver a classe Estudante, seja possível usar uma classe EstudanteDePosGraduacao, já que, pela herança, um estudante de pós-graduação é um estudante.

Solução

A solução para este problema é modificar a nossa modelagem. Podemos criar uma nova classe EstudanteDeGraduacao, que também herdará de Estudante. Essa classe terá o método entregarTCC:

public class EstudanteDeGraduacao extends Estudante {
    public void estudar() {
        System.out.println(nome + " está estudando na graduação.");
    }

    public void entregarTCC() {
    //…
    }
}

Repare que, dessa forma, nossas classes representam melhor o mundo real. Não estamos forçando uma classe a fazer algo que ela originalmente não faz.

Além disso, se precisarmos utilizar uma instância de Estudante, podemos passar, sem medo, uma instância de EstudanteDeGraduacao ou de EstudanteDePosGraduacao.

Afinal de contas, essas classes conseguem executar todas as funções de Estudante — mesmo tendo funções mais específicas.

Definição do Princípio da Substituição de Liskov

Quem propôs o Princípio da Substituição de Liskov, de maneira formal e matemática, foi Bárbara Loskov.

No entanto, Robert Martin deu uma definição mais simples para ele: “Classes derivadas (ou classes-filhas) devem ser capazes de substituir suas classes-base (ou classes-mães)”.

Ou seja, uma classe-filha deve ser capaz de executar tudo que sua classe-mãe faz. Esse princípio se conecta com o polimorfismo e reforça esse pilar da POO.

É importante notar também que, ao entendermos esse princípio, passamos a nos atentar mais para o código: caso um método de uma classe-filha tenha um retorno muito diferente do da classe-mãe, ou lance uma exceção, por exemplo, já dá para perceber que algo está errado.

Se no seu programa você tem uma abstração que se parece com um pato, faz o som de um pato, nada como um pato, mas precisa de baterias, sua abstração está equivocada.

Vantagens de aplicar o Princípio da Substituição de Liskov

Aplicar esse princípio nos traz diversos benefícios, especialmente para ter uma modelagem mais fiel à realidade, reduzir erros inesperados no programa e simplificar a manutenção do código.

Princípio de Segregação de Interface (I – Interface Segregation Principle)

Para entender o Princípio de Segregação da Interface, imagine que estamos trabalhando com um sistema de gerenciamento de funcionários de uma empresa.

Vamos criar uma interface, conforme o código abaixo:

Interface Funcionário
public interface Funcionario {

    public BigDecimal salario();
    public BigDecimal gerarComissao();

}

Repare que criamos a interface para estabelecer um “contrato” com as pessoas que são funcionárias dessa empresa. N

esse contexto, o código a seguir descreve duas classes que fazem referências a duas profissões nessa empresa: Vendedor e Recepcionista.

Ambas usam a interface Funcionario e, portanto, devem implementar os métodos salario() e gerarComissao().

Classe Vendedor
import java.math.BigDecimal;

public class Vendedor implements Funcionario {

    @Override
    public BigDecimal salario() {
    }

    @Override
    public BigDecimal gerarComissao() {
    }

}
Classe Recepcionista
import java.math.BigDecimal;

public class Recepcionista implements Funcionario{

    @Override
    public BigDecimal salario() {
    }

    @Override
    public BigDecimal gerarComissao() {
    }

}

Problemática

Analisando o código acima, faz sentido uma pessoa que possui o cargo de vendedora ou recepcionista ter salárioSim! Afinal, todos nós temos boletos para pagar.

Seguindo esta mesma linha, faz sentido uma pessoa com cargo de vendedor ou recepcionista ter comissãoNão!.

Para uma pessoa que tem o cargo de vendedora, faz sentido. Mas para a pessoa que tem o cargo de recepcionista, não faz sentido.

Ou seja, a classe Recepcionista foi forçada a implementar um método que não faz sentido para ela. Embora ela seja funcionária dessa empresa, esse cargo não recebe comissão.

Portanto, podemos perceber que este problema foi gerado por temos uma interface genérica.

Solução

Para resolver isso, é possível criar Interfaces específicas. Ao invés de ter uma única interface Funcionário, podemos ter duas: Funcionario e Comissionavel.

Interface Funcionário
import java.math.BigDecimal;

public interface Funcionario {
    public BigDecimal salario();
}

Repare que mantemos a interface Funcionario, mas retiramos o método gerarComissao() a qual é específico de algumas pessoas, para adicioná-lo em uma nova interface FuncionarioComissionavel:

Interface Comissionável
import java.math.BigDecimal;

public interface Comissionavel{
    public BigDecimal gerarComissao();
}

Agora, a pessoa que possui o direito de ter comissão irá implementar a interface Comissionavel, um exemplo disso é a classe Vendedor:

Vendedor
import java.math.BigDecimal;

public class Vendedor implements Funcionario, Comissionavel{

    @Override
    public BigDecimal salario() {
    }

    @Override
    public BigDecimal gerarComissao() {
    }

}

Agora, a classe Recepcionista pode implementar a interface Funcionario sem ter a obrigação de criar o método gerarComissao():

Recepcionista
import java.math.BigDecimal;

public class Recepcionista implements Funcionario{
    @Override
    public BigDecimal salario() {
    }
}

Definição do Princípio da Segregação da Interface

Conforme analisamos o código acima, podemos perceber que:

Devemos criar interfaces específicas ao invés de termos uma única interface genérica.

E é justamente isto que Princípio da Segregação da Interface diz: “Uma classe não deve ser forçada a implementar interfaces e métodos que não serão utilizados”.

Vantagens de aplicar o Princípio da Segregação da Interface

Seguir o Princípio da Segregação da Interface ajuda a promover a coesão e a flexibilidade em nossos sistemas, tornando-os fáceis de manter e estender.

Princípio da Inversão de Dependência (D – Dependency Inversion Principle)

Para compreender o Princípio da Inversão de Dependência (letra O da sigla) imagine que estamos trabalhando em uma startup de e-commerce e precisamos desenvolver o sistema de gerenciamento de pedidos.

Sem conhecer o Princípio da Inversão de Dependência, é bem provável que vamos desenvolver uma classe PedidoService semelhante ao código abaixo:

Classe PedidoService
public class PedidoService {
    private PedidoRepository repository;

    public PedidoService() {
        this.repository = new PedidoRepository();
    }

    public void processarPedido(Pedido pedido) {
        // Lógica de processamento do pedido
        repository.salvarPedido(pedido);
    }
}

Problemática

Aparentemente, o código parece estar certo. No entanto, se um dia precisar alterar o armazenamento deste pedido para um outro lugar (por exemplo, uma API externa), vai precisar de mais de uma classe para resolver o problema.

Afinal, a classe PedidoService está diretamente acoplada à implementação concreta da classe PedidoRepository.

Solução

Para resolver este problema, podemos criar uma interface para a classe de acesso ao banco de dados e injetá-la na classe `PedidoService´.

Dessa forma, nós estamos dependendo de abstrações e não de implementações concretas.

Interface PedidoRepository
public interface PedidoRepository {
    void salvarPedido(Pedido pedido);
}
Classe PedidoService
public class PedidoService {
    private PedidoRepository repository;

    public PedidoService(PedidoRepository repository) {
        this.repository = repository;
    }

    public void processarPedido(Pedido pedido) {
        // Lógica de processamento do pedido
        repository.salvarPedido(pedido);
    }
}

Deste modo, conseguimos fazer com que a classe de alto nível (PedidoService) seja independente dos detalhes de implementação da classe de baixo nível (PedidoRepository).

Definição do Princípio da Inversão de Dependência

Princípio da Inversão de Dependência diz: “dependa de abstrações e não de implementações concretas”.

Assim, é recomendado que os módulos de alto nível não dependam diretamente dos detalhes de implementação de módulos de baixo nível.

Em vez disso, eles devem depender de abstrações ou interfaces que definem contratos de funcionamento. Isso promove maior flexibilidade e facilita a manutenção do sistema.

Vantagens de aplicar o Princípio da Inversão de Dependência

A adesão ao Princípio de Inversão de Dependência promove a flexibilidade e a extensibilidade dos nossos sistemas.

Isso faz com que seja mais fácil fazer testes de unidade e construir códigos mais robustos e duradouros.

𝟭𝟴 𝗦𝗸𝗶𝗹𝗹𝘀 𝗙𝗼𝗿 𝗦𝗼𝗳𝘁𝘄𝗮𝗿𝗲 𝗘𝗻𝗴𝗶𝗻𝗲𝗲𝗿𝘀

1 – Programming Languages: Mastery in Python, Java, C++, crucial for coding.
2 – Algorithms & Data Structures: Core problem-solving tools for coding.
3 – SDLC (Software Development Life Cycle): Knowledge from planning to software maintenance.
4 – Version Control: Expertise in Git for code collaboration.
5 – Debugging & Testing: Identifying, fixing bugs, and testing code.
6 – Databases: Operate databases like MySQL, MongoDB efficiently.
7 – Operating Systems: Insights into memory, processes, and file systems.
8 – Networking Basics: Understanding TCP/IP, DNS, and HTTP for web apps.
9 – Cloud Computing: Use AWS, Azure for app deployment and management.
10 – CI/CD (Continuous Integration/Continuous Deployment): Automate tests and deployment with pipelines.
11 – Security Practices: Secure apps against common vulnerabilities.
12 – Software Architecture: Designing robust software structures.
13 – Problem-Solving: Tackle complex software issues effectively.
14 – Communication: Clear verbal and written interaction with teams.
15 – Project Management: Plan and monitor software projects.
16 – Machine Learning: Know-how in ML algorithms for AI projects.
17 – AI (Artificial Intelligence): Use tools like ChatGPT to speed up development.
18 – Continuous Learning: Stay updated with technological advancements.