Code Monkey home page Code Monkey logo

partiu-codar's Introduction

partiu-codar's People

Contributors

brendacosta avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

partiu-codar's Issues

Clean Code: Reduzindo custos com desenvolvimento e manutenção

Desenvolver sistemas com grande longevidade e fácil manutenção é um desafio comum enfrentado pela maioria dos times de engenharia de software. Isso requer mais que conhecimento técnico em padrões ou tecnologias do momento. Envolve a capacidade de tomar decisões durante o processo que impactará na experiência do próprio desenvolvedor que irá dar a manutenção no sistema.

Enquanto maior parte dos esforços são direcionados para a parte inicial do desenvolvimento de um novo sistema, a atenção com aspectos que facilitarão a manutenção no futuro continuará sendo negligenciada.

Passamos a maior parte do tempo estudando e lendo código escrito por outros desenvolvedores e muitas vezes, nos deparamos com trechos difíceis de entender, fazendo com que a maior parte do tempo investido seja apenas para a compreensão do que com a evolução de implementações.

Neste artigo, você conhecerá algumas técnicas de Clean Code que ajudam a reduzir os custos de desenvolvimento e manutenção e que também equilibram o tempo de compreensão com o tempo de evolução de novas implementações no código.

O que é Clean Code e por que aplicar suas técnicas?

O próprio nome já diz, mas de acordo com o livro, Clean Code, escrito por Robert C. Martin, “é um conjunto de atributos de qualidade aliado a boas práticas que garantem que um código possibilite fácil manutenção em todos os aspectos” . Resumindo, é um código simples, eficiente, testável, fácil de entender, de se manter, evoluir e reaproveitar independente de quem escreveu – seja júnior, pleno, sênior, desenvolvedor, analista, engenheiro ou arquiteto.

Mas por que aplicar as técnicas de Clean Code durante o desenvolvimento e manutenção?

  • Ter um código limpo;
  • Ajudar a reduzir a dívida técnica;
  • Ter um time mais produtivo, incluindo novos membros;
  • Vantagem competitiva na velocidade de desenvolvimento em relação ao concorrente;
  • Obter ainda mais valor no futuro;
  • Prevenir o software de se tornar legado pelo caos na manutenção.

Os motivos listados acima poupam incontáveis horas gastas com recursos que poderiam estar focados em construir uma nova solução, implementando melhorias que gerem valor para o negócio e para o próprio time de desenvolvimento, de forma rápida ou pensando nas próximas funcionalidades.

Também evitará que o produto tenha dívidas técnicas impagáveis, ou seja, impedindo que no futuro, mais horas de trabalho sejam gastas em itens técnicos que já deveriam ter o mínimo de qualidade garantida. Em outras palavras, retrabalho.

À medida que essa confusão aumenta, a produtividade diminui a quase zero. E é isso que as técnicas do Clean Code nos ajudam a evitar. Nos fazendo refletir sobre o impacto financeiro gerado e sobre a relação entre produtividade, qualidade e manutenção.

6 das muitas técnicas de Clean Code que fazem a diferença

1 – Regra do escoteiro

Não basta escrever um código bom, é preciso melhorá-lo cada vez que o revisitarmos, pois qualquer código limpo pode ficar sujo com o tempo, por isso é importante ter a constante prática de melhoria contínua.

Isso não se limita ao mesmo trecho do código que foi desenvolvido ou alterado, e sim a qualquer parte dele. Não precisa ser algo grande, pode ser a troca do nome de uma variável, constante, método, função, classe ou eliminar uma pequena duplicidade de código.

Por que utilizar? Quando isso se torna um hábito não há espaço para bagunça, para fazer as coisas de qualquer jeito independente do prazo ou pressão que ocorra. É uma questão de mentalidade e cuidado com a qualidade do trabalho realizado.

Essa prática evita que horas extras sejam dedicadas no futuro só para entender o que é e como foi feito.

2 – Use nomes a partir do domínio da solução

Tudo que for escrito no código será lido por outros desenvolvedores e todos os envolvidos no projeto precisam estar na mesma página.

Na prática, se algum analista de negócio ou responsável por um departamento da empresa fala de um termo específico em suas orientações de como quer que o sistema funcione, os mesmos devem ser utilizados quando o código for escrito e isso deve ser feito sem adaptações.

Por que utilizar? Isso facilitará para outros desenvolvedores o que significa aquele termo. Por exemplo, pense em um sistema que está sendo desenvolvido para um banco, e o termo “francesinha” é mencionado pelas pessoas envolvidas no projeto, sendo que o termo represente o extrato de movimentação de títulos mantidos na carteira de cobrança bancária. A maioria das pessoas envolvidas com o contexto bancário está habituada com este termo.

Agora imagine o cenário em que o desenvolvedor acha o termo cômico ou não gosta dele e resolve escrever outro termo para substituir, como “XPTO” e implementa isso nas telas do sistema e no código. Provavelmente os usuários que utilizam o app e estão acostumados com o termo ficarão sem entender o que significa “XPTO”, pois terão a sensação de que a funcionalidade “francesinha” deixou de existir. E não só os usuários, mas também nas reuniões que envolvem entendimento das regras de negócio que utilizam o termo para desenvolvimento de outras funcionalidades. E novos desenvolvedores que atuarem no projeto, pois serão aculturados com o termo errado que não é conhecido pelos envolvidos.

Usar nomes a partir do domínio da solução, ajuda o time a não perder tempo inventando termos não utilizados pela área de negócio e evita que desenvolvedores percam tempo tentando tirar dúvidas sobre o que significa enquanto poderiam estar focados em evoluir o que já existe com base no entendimento comum sobre os termos específicos da área de negócio.

3 – Use nomes passíveis de busca

“Usar nomes passíveis de busca” é uma técnica que está diretamente conectada a técnica de “Usar nomes a partir do domínio da solução”.

Durante o desenvolvimento do código-fonte é comum desenvolvedores buscarem atalhos para driblar a necessidade de pensar e dar nomes. Aliás, boa parte do desenvolvimento de sistemas consiste em “dar nome aos bois códigos”.

E usar nomes passíveis de busca é uma técnica que orienta a determinar que os nomes utilizados no código de um software sejam em variáveis, funções, classes, métodos, arquivos (trechos de código que são modificados com frequência), que sejam nomes fáceis de pesquisar. De forma que tenham uma identificação própria ou represente alguma informação de forma lógica. Diferente de usar apenas letras, códigos ou abreviações que só quem desenvolveu determinado trecho de código consegue entender.

Por que utilizar? Essa técnica facilita a busca durante a manutenção. Os desenvolvedores que precisam realizar uma alteração facilmente saberão identificar o que faz determinados trechos de código pelo nome que foi dado e pelo que representa no contexto. Reduzindo a quantidade de tempo necessária para entender o que cada trecho faz ou o que significa.

Um exemplo comum, que pode representar de forma prática quanto tempo se perde não usando nomes passíveis de busca é o código que tem uma série de variáveis, funções, classes, métodos e arquivos com abreviações de termos verbosos (grandes) e letras como “i”, “a”, “j” que são utilizadas.

Agora, imagine quanto tempo o desenvolvedor que dará manutenção em um código cheio de letras e siglas que não representam nenhum conceito de negócio levará para realizar a entrega esperada pela área de negócio simplesmente porque “não deu nome aos bois códigos”? Gastos com horas e horas podem ser poupadas com essa técnica.

4 – Evite o mapeamento mental

Outra técnica que orienta o cuidado com os nomes dados nos códigos. Todo nome deve ser significativo e claro, seja de variável, funções, classes, arquivos, ou etc. Isso significa que devem ser óbvios.

O mapeamento mental faz com que desenvolvedores que tem contato com o código tentem adivinhar o que significa cada trecho. Inclusive o significado de siglas, letras, números perdidos e nomes que só fazem sentido na cabeça de quem escreveu o código.

Por que utilizar? Pelo mesmo motivo que usar nomes a partir do domínio da solução e usar nomes passíveis de busca facilitam a procura e a compreensão por parte de quem está dando manutenção. Isso reduz a quantidade de horas gastas com tentativas de entendimento do código que sofrerá alterações a curto, médio e longo prazo. Essa quantidade de horas não gasta, é uma grande economia, podendo em alguns casos economizar uma ou mais sprints.

5 – Evite repetição

Essa técnica está diretamente ligada a um princípio da própria programação, o DRY (Don’t Repeat Yourself). Como o próprio nome da técnica e o princípio afirmam: Evite repetição.

Ou seja, evitar a repetição de trechos de código e permitir que sejam reutilizados por meio de estruturas que possam ser usadas com pouco esforço, seja por meio de uma função, método, classe, arquivo etc.

Por que utilizar? A repetição de código gera um efeito colateral que aumenta exponencialmente o consumo de horas em uma manutenção.

Como? Ao alterar uma funcionalidade ou módulo que possui diversos códigos repetidos e espalhados fazendo a mesma coisa, vai exigir que quando uma regra ou trecho for alterado, os outros também sejam em diversos outros lugares. Em sistemas de médio e grande porte isso é um verdadeiro caos, pois quanto mais pontos duplicados houver, mais consumo de hora será necessário para alterá-los.

Com isso serão perdidas horas com:

  • Pesquisa dos trechos duplicados;
  • Alteração da mesma regra em vários pontos do sistema;
  • Pesquisa dos trechos que não foram atualizados por esquecimento;
  • Investigação da causa de um problema gerado por um trecho de código que foi copiado e duplicado sem avaliar seus detalhes

Evitar repetição de código ajuda a reduzir o tempo de desenvolvimento e manutenção através do reaproveitamento, que é garantido conforme o código é modularizado, reduzindo o tempo de horas gastas com pesquisas das duplicações, acabando com o risco de gerar bugs a partir da prática de copiar e colar sem especializar o trecho, prevenindo bugs causados por esquecimento de uma atualização do trecho repetido em cada parte do sistema.

6 – Use constantes ao invés de números mágicos

É comum encontrar em métodos e outros trechos de código números fixos, sendo usados muitas vezes para comparação, cálculo, ou como limite numérico. Porém um número sozinho pode não expressar por si só o que representa no código. Isto é o que chamamos de número mágico.

Os números mágicos podem atrapalhar o entendimento geral de um trecho de código por não explicitar uma lógica de negócio ou o que ele representa. Imagine encontrar no código um número 1, 3 ou 7 perdidos e não saber o que eles significam? Agora imagina o tempo gasto para entender isso, ou o tempo buscando disponibilidade de alguém da área de negócio para talvez ajudar a entender algo a respeito? Esse tempo poderia ser otimizado com o uso de constantes e o esforço estaria inteiramente destinado a evolução da funcionalidade associada a este código.

Por que utilizar? Substituir números mágicos por constantes ajuda a reduzir custos com manutenção de 3 maneiras:

  • O que o número representa fica explícito no código, bastando apenas olhar o nome da constante;
  • O próprio uso de uma constante mostra que aquele valor não mudará em tempo de execução, ele é sempre o mesmo, ou seja, fixo, só mudará por uma necessidade de negócio (quando, e se acontecer);
  • Caso o número seja utilizado mais de uma vez no código ou em outros arquivos, a mesma constante pode ser utilizada por meio do reaproveitamento.

Utilizar constantes ajuda não somente por exprimir melhor a utilidade de tal número, também a detectar facilmente onde o número é utilizado. Também verificando se há outros números associados e entendendo o que representam em cada contexto, além de permitir o reaproveitamento.

Como essas técnicas de Clean Code ajudam a reduzir os custos na prática?

Aumentando a eficiência na manutenção ou evolução de funcionalidades através de práticas que reduzem o tempo e esforço necessário de entendimento e implementação do “que” e do “como” será feito no código.

A produtividade dos times de engenharia de software também está associada à qualidade técnica dos projetos. E quanto menor a qualidade técnica, maior é o custo de manutenção.

De acordo com o artigo gerado a partir do estudo Clean Code: On the Use of Practices and Tools to Produce Maintainable Code for Long-Living Software, a escolha de procedimentos e práticas é mais cultural do que problema técnico.

Tendo isso em mente, o time que prioriza a adoção de práticas que aumentam a eficiência da manutenção e pensam no longo prazo, tendem a poupar futuras modernizações e dificuldades de evolução em funcionalidades existentes.

A aplicação do Clean Code incentiva uma atuação proativa com zelo pela qualidade e focada em longo prazo. E seus resultados também podem ser comprovados através de uma comparação realizada com cenários que envolvem sistemas em que essas técnicas não foram aplicadas.

Clean Code investimento evolutivo

Sistemas de sucesso geralmente são medidos pela sua capacidade de adaptação e extensibilidade para permitir uma rápida evolução frente as necessidades de negócio e de mercado. Infelizmente a maioria dos sistemas são construídos sem que boas práticas e técnicas que facilitam a manutenção sejam consideradas.

Quanto mais bem sucedido é o sistema, por mais tempo será mantido e menos problemas enfrentará.

Em vista disso, para garantir que a manutenção de sistemas não se torne um grande problema para as empresas, trazendo custos com horas de manutenção que poderiam ser revertidas em evolução com foco no negócio, se faz necessária a aplicação de técnicas de Clean Code através de um processo contínuo e incremental.

Os ganhos da aplicação dessas técnicas podem ser comprovados e medidos de forma empírica no dia a dia conforme evolução dos sistemas que estão sendo desenvolvidos. E no final, o que se gasta financeiramente com manutenção, será investido com evolução.

Referências

  • Martin, Cecil Robert. Clean Code. 2008.
  • Clean Code: On the Use of Practices and Tools to Produce Maintainable Code for Long-Living Software

Sobre o Autor

Kaique Prazeres é Arquiteto de Soluções na Operação Nacional da Programmers Beyond IT e é um ferrenho defensor de boas práticas e qualidade de software com foco em redução de custos e aumento de longevidade de sistemas.

Algoritmo de bolo simples

image

INGREDIENTES

MODO DE PREPARO

  • Bata as claras em neve e reserve.

  • Misture as gemas, a margarina e o açúcar até obter uma massa homogênea.

  • Acrescente o leite e a farinha de trigo aos poucos, sem parar de bater.

  • Por último, adicione as claras em neve e o fermento.

  • Despeje a massa em uma forma grande de furo central untada e enfarinhada.

  • Asse em forno médio 180 °C, preaquecido, por aproximadamente 40 minutos ou ao furar o bolo com um garfo, este saia limpo.

Testes Unitários: do Mock ao Arrange Act & Assert

Introdução

Com a alta na demanda de novos sistemas de informações e estes requerendo a implementação de regras de negócio, não há duvidas que as entregas precisam ter qualidade técnica. Para maximizar a qualidade delas, ou seja, do código desenvolvido, existe um tipo de teste denominado Teste de Unidade.

Veremos nas próximas seções deste artigo um pouco da teoria dos testes de unidade, também conhecido como testes unitários, e também aprenderemos a implementar um padrão conhecido como AAA (Arrange Act Assert), o qual tem como objetivo estruturar e organizar os testes em questão.


1. Testes de Unidade

Testes de Unidade ou Unitário é um teste responsável por cobrir ou validar uma unidade do código, por exemplo, um método de uma classe específica. Esses métodos retornarão um resultado e o teste verificará se é o resultado esperado. [VALENTE, 2020]

A seguir, um exemplo de um teste unitário escrito em C# no framework de teste chamado XUnit:


  [Fact]
  public void Verificar_Tamanho_Maximo_Da_Senha()
  {
      int idUsuario = 1;
      int tamanhoSenhaEsperado = 8;
      var senha = usuarioService.GerarSenha(idUsuario);
      Assert.Equal(tamanhoSenhaEsperado, senha.Length);
  }

No código acima, por enquanto, é possível identificar três pontos:

A) Unidade sendo testada: Método GerarNovaSenha;

B) Retorno esperado pelo teste: tamanhoSenhaEsperado;

C) Verificação do resultado esperado com o resultado da unidade: Assert.Equal(tamanhoSenhaEsperado, senha.Length);

Para o teste ser aprovado, o resultado esperado, na variável tamanhoSenhaEsperado, precisa ser igual ao resultado retornado pela unidade testada.


Frameworks de Teste: são tecnologias construídas para a implementação dos Testes Unitários. [VALENTE, 2020]

Exemplos: XUnit, NUnit.


1.1 Benefícios e quando escrevê-los

Acredito que seja interessante abordar esses dois assuntos na mesma seção, pois na minha opinião um complementa o outro.

O principal benefício de implementá-los é o de encontrar bugs. Esta ação pode ocorrer na fase de desenvolvimento, pois assim será mais difícil que os usuários finais encontrem problemas com a versão de produção. [VALENTE, 2020].

Outro benefício importante é que os testes em questão, funcionam como uma proteção contra regressões no código. Regressões é quando há uma implementação nova ou uma modificação de uma determinada parte do código e acaba impactando no funcionamento, ou seja, aquilo que funcionava deixou de funcionar. [VALENTE, 2020]

Também, além dos benefícios citados anteriormente, enquanto estamos desenvolvendo testes unitários, estamos de certa forma, documentando o código, pois ao estudar o teste é possível entender o comportamento da unidade (classe ou método) que ele está testando. [VALENTE, 2020]

Destaquei os principais benefícios dos testes unitários e acredito que eles mesmos denunciam o quão importante é a adoção em um software desenvolvido. Além disso, não preciso dizer “quando você deve escrevê-los” os próprios benefícios já os dizem.


1.2 Mock

É praticamente inevitável falar de testes de unidade sem falar do nosso amigo Mock e o seu importantíssimo papel no desenvolvimento desses testes.

Para começo de conversa, a palavra mock é uma palavra em inglês e segundo o dicionário Cambridge ela possui mais de um significado, e o que mais se encaixa para o nosso contexto é este: “artificial, but similar to the original”, ou seja, artificial, mas similar ao original.

Se fizermos uma rápida tradução no Google, encontraremos o seguinte resultado:



Mais uma vez, o que mais se encaixa no nosso contexto é: falso ou simulado.

Em termos técnicos, dentro do ambiente de testes de unidade, “mockar” significa simular o comportamento de uma dependência. Esta dependência pode ser, por exemplo, uma interface injetada na classe do método que precisa ser testado.


Existe alguma ferramenta de Mock? Para a nossa alegria, sim! Você não precisa criar um projeto mega mirabolante chamado “Mock”, alguém já fez isso por você, e felizmente, existe mais de uma opção para a plataforma .NET, e segue os que eu conheço:

Moq e Rhino Mocks.


Você pode estar se perguntando, “por que mockar? Faça o teste com todas as dependências, isso é desnecessário”. Veja as imagens abaixo e eu te explico o motivo:

A) Preciso testar o método chamado “GerarNovaSenha” da classe UsuarioService;



Método que precisa ser testado.


B) O objetivo do teste é o resultado da regra de negócio, ou seja, gerar uma nova senha do tipo GUID. Portanto, a dependência com o repositório chamado _usuarioRepository, não é importante no atual contexto.

C) Vimos no item B que temos uma dependência desnecessária para o resultado e portanto, devemos aplicar um mock nela no nosso teste, como exibido na imagem abaixo:



A Dependência precisa ser mockada.


D) Ao mockar a dependência citada, o framework vai simular o comportamento dela, e assim, evitando um bug no seu teste. Portanto, caso você opte por não aplicar o mock, quando o teste atingir a chamada do repositório no método sendo testado, ocorrerá uma exception e seu teste falhará.

D.1) Além de aplicar o mock, você precisa informar ao framework qual método da dependência ele precisa simular, e isso é feito através do “Setup”. Nessa programação, informamos o método para simular com seus parâmetros, que podem ser simulados ou não, e o retorno desejado com o “ReturnsAsync”, que também pode ser algo simulado ou não.



Informando qual método precisa simular, seu parâmetro e o retorno esperado


D.2) Como no contexto desse teste não é importante o comportamento do _usuarioRepository, informei qualquer parâmetro e esperei qualquer retorno, usando a sintaxe It.IsAny.

D.3) Usei o “ReturnsAsync” porque o meu método GetById é assíncrono, portanto o seu retorno será assíncrono.


É importante destacar que, em muitos casos, você precisará do retorno do método “mockado” ou precisará enviar determinados parâmetros para esse método, pois isso implicará no resultado esperado pelo teste. Exemplo:



Método com a validação do e-mail.



O Retorno no Setup está chamando um método e este será o retorno simulado esperado.



Depurando o teste.



Dá uma olhada no e-mail que veio! Deve jogar uma exception.



O Teste passou, porque o Assert já esperava a exception, conforme mostram as setas.


Espero que com essa pequena introdução do conceito de Mock, você possa ter conhecido e entendido um pouco mais sobre esse nosso amigão dos testes unitários.


2. Padrão AAA: Arrange, Act e Assert

Dificilmente um desenvolvedor de software não passe pelo amargo e doce caminho dos códigos sem Clean Code, organização e estrutura. Saiba que isso acontece nas melhores famílias.

Nós, profissionais do código de máquina, temos a obrigação de “codar” o mais “clean code” possível e nisso está incluso os Testes de Unidade.

Existem técnicas para deixar a estrutura dos seus testes mais organizadas, mas gostaria de destacar uma muito conhecida, o padrão Arrange, Act e Assert, ou melhor AAA.

Antes, vamos conhecer os significados:

A) Arrange → Organizar

É onde ocorre a inicialização dos objetos e a definição dos valores dos dados que serão passados para a unidade que será testada.

B) Act → Agir

É a seção onde o teste da unidade ocorre, utilizando os dados e objetos declarados na seção Arrange

C) Assert → Declarar

É a última seção, porém não é a menos importante!. É onde verifica se a ação do método testado tem o comportamento esperado.

Fonte: https://docs.microsoft.com/pt-br/visualstudio/test/unit-test-basics?view=vs-2022


2.1 Exemplos do AAA

Na imagem abaixo, temos um exemplo da organização e aplicação do padrão. Note que foram colocados comentários pois fazem parte do uso desse padrão.



Na próxima imagem, uma versão ainda mais organizada:



Uso de constantes e trechos abstraídos.


Em alguns casos, é possível juntar o Act e o Assert em uma única linha:



Act e Assert na mesma linha.


No caso da imagem acima, está sendo feito a execução do teste da unidade e logo após a verificação do retorno com o Assert.ThrowsAsync (Uma exception como retorno esperado).


Eu disse acima que os comentários fazem parte do padrão, porém, particularmente, após a aplicação e organização, esses comentários podem ser removidos, pois não é recomendável ter comentários em um código, pois podem ficar obsoletos. Tome cuidado com isso!


Conclusão

Espero que através desta introdução sobre os assuntos abordados neste texto, eu tenha conseguido conscientizar os leitores sobre a grande importância da aplicação de testes unitários. Preocupe-se, de uma maneira saudável, com a qualidade do seu código e das suas entregas, pois o cliente precisa estar satisfeito e também algum dia alguém fará manutenção nele.

Obrigado e até a próxima!

Márcio C. Monzón


Referências Bibliográficas

https://docs.microsoft.com/pt-br/visualstudio/test/unit-test-basics?view=vs-2022 . Acessado no dia 29/07/2022.

VALENTE, Marco Tulio. Engenharia de Software Moderna: Prinípios e práticas para desenvolvimento de software com produtividade. 2020.

Cambridge: International Dictionary of English

Artigo Original: https://medium.com/@marcio.pcmonzon/testes-unit%C3%A1rios-do-mock-ao-arrange-act-assert-2c5f29bd304c


Sobre o Autor

Márcio C. Monzon é Desenvolvedor de Software na Programmers Beyond IT. Pós Graduado em Práticas de Metodologias Ágeis e Bacharel em Sistemas de Informação. Um profissional que está em constante aprendizado e defende um desenvolvimento de software sustentável e com qualidade.

Os 4 Pilares da Programação Orientada a Objetos com Exemplos Práticos

Os 4 Pilares da Programação Orientada a Objetos com Exemplos Práticos

Este "artigo" técnico é um complemento a uma publicação que realizei no Linkedin com o intuito de abordar de forma resumida e objetiva os 4 pilares da orientação a objetos sem entrar no mérito dos detalhes de uma abordagem mais aprofundada ou filosófica sobre o tema.

Minha intenção com a publicação do Linkedin foi atingir os desenvolvedores juniores, iniciantes em programação, desenvolvedores que não foram bem em entrevistas técnicas neste tema ou tiveram algum branco (esquecimento) quando confrontado com as perguntas e quem tem dificuldade de entender esses 4 pilares e como aplicá-los na prática. (Link da publicação aqui)

Aviso Importante: Algumas linguagens orientada a objetos ou que oferecem suporte possuem mecanismos próprios que permitem explicitar em código alguns dos pilares do paradigma. Por isso, para ilustrar os exemplos, a linguagem utilizada é o C#.
A exemplo do polimorfismo. Em linguagens como PHP e Javascript ainda não é possível realizar sobrecarga e/ou sobreposição de métodos de forma explicita e simples. Enquanto que no C#, Java e outras linguagens, isso é mais transparente.
De forma alguma o objetivo aqui é afirmar que uma linguagem é melhor que outra por conta disso, mas, vale ressaltar que tal mecanismo facilita o desenvolvimento e o entendimento sobre como aplicar alguns conceitos.

Abstração

É a capacidade de representar o mundo real em código, seja em classes e/ou interfaces e de criar um conjunto de rotinas capazes de serem reutilizadas para complementar outras, com seus detalhes de implementação ocultos de quem vai usar.

Envolve a implementação da lógica necessária para execução do código. Só que de forma "oculta" de quem usa. Pois quem usa, só precisa saber o que as classes/interfaces fazem, não como fazem. Este pilar é também considerado uma extensão do Encapsulamento.

Exemplo Prático: Representando um Desenvolvedor do mundo real em código.

Note que no exemplo a seguir a classe abstrata "Desenvolvedor" é uma representação do desenvolvedor no mundo real. Devido a isso ela possui nome, linguagem de programação preferida e anos de experiência como características (atributos) e codar, escrever testes unitários, escrever código, verificar tempo de experiência, beber café e etc… como comportamentos (métodos) que ele possui.

Ao usar a classe Desenvolvedor como um objeto precisamos apenas saber o que ela faz ou pode fazer e não precisamos nos preocupar com o como ela faz.

Somente ela conhece e lida com a complexidade de seus métodos. 
E no mundo real, só o desenvolvedor sabe lidar com a complexidade de seus comportamentos também.

Além disso, é possível que outra abstração seja criada utilizando essa como "base" e complemento.

Resumindo, se essa classe fosse instanciada (caso não fosse abstrata, é claro) só precisariamos nos preocupar com os métodos que ela oferece para usar. E não como o processo de execução do método é feito.

Aviso Importante: Abstração não consiste necessariamente em criar uma classe abstrata. A classe "Desenvolvedor" foi criada como abstrata somente para fins didáticos e para ser reaproveitada na explicação dos outros pilares.

using System;

abstract class Desenvolvedor
{
    public string Nome { get; set; }
    public LinguagemDeProgramacao LinguagemPreferida { get; set; }
    public int AnosDeExperiencia { get; set; }

    protected Desenvolvedor(string nome, int anosDeExperiencia, LinguagemDeProgramacao linguagemPreferida)
    {
        Nome = nome;
        LinguagemPreferida = linguagemPreferida;
        AnosDeExperiencia = anosDeExperiencia;
    }

    public abstract void Codar(string codigo);

    public abstract void EscreverTestesUnitarios(string codigo);

    public bool EhRaiz()
    {
        return AnosDeExperiencia >= 10;
    }

    public void BeberCafe()
    {
        PegarCaneca();
        EscolherCafe();
        EncherCaneca();
        EntornarNaBoca();
    }

    protected void EscreverCodigo(string codigo)
    {
        Console.WriteLine(codigo + " - Desenvolvidor por " + Nome);
    }

    protected void MapearCenariosDeTestes()
    {
        // Implementação...
    }
  
    private void PegarCaneca()
    {
        // Implementação...
    }

    private void EscolherCafe()
    {
        // Implementação...
    }

    private void EncherCaneca()
    {
        // Implementação...
    }

    private void EntornarNaBoca(); 
    {
        // Implementação...
    }
}

Encapsulamento

Mecanismo usado para esconder atributos e detalhes de implementação dos dados passados para a instância da classe. Garantindo que o acesso a dados ocorra apenas através de métodos públicos, impedindo que eles sejam alterados em tempo de execução de fora da classe.

Sabe o que é interessante no encapsulamento? É que ele é uma extensão da abstração! Por que?

Porque quando aplicado ele garante que só a própria classe conheça os detalhes de implementação e disponibilize apenas o que é possível fazer com os dados da classe como dito na definição anteriormente.

E como que aplica o encapsulamento na prática mesmo? Simples: Tornando os atributos privados ou protegidos através dos modificadores de acesso ou visibilidade "private" e "protected" e criando métodos que retornem estes atributos apenas.

A seguir, Note que no exemplo da abstração os atributos eram públicos e tinha um setters. Ou seja, podiam ser modificados na instância por qualquer pessoa que utilizasse a classe "Desenvolvedor" e a qualquer momento.

Aviso Importante: O encapsulamento da classe do exemplo a seguir ainda poderá ser "violado" somente pelas classes derivadas (filhas) através de herança, pois a visibilidade está definida como protegida (protected), não como privada (private).

using System;

abstract class Desenvolvedor
{
    /**
    * Note que no exemplo da abstração os atributos eram públicos e tinha um setters.
    * Ou seja, podiam ser modificados na instância por qualquer pessoa que utilize a classe.
    */
    protected string Nome { get; }
    protected LinguagemDeProgramacao LinguagemPreferida { get; }
    protected int AnosDeExperiencia { get; }

    protected Desenvolvedor(string nome, int anosDeExperiencia, LinguagemDeProgramacao linguagemPreferida)
    {
        Nome = nome;
        LinguagemPreferida = linguagemPreferida;
        AnosDeExperiencia = anosDeExperiencia;
    }

    public abstract void Codar(string codigo);

    public abstract void EscreverTestesUnitarios(string codigo);

    /**
    * Sem encapsulamento a mesma instância dessa classe poderia ter seu valor modificado
    * diversas vezes. E isso é uma brecha de segurança para os dados e a execução da implementação.
    * 
    * Pois perde-se a garantia da integridade do objeto e de seus dados. 
    *
    * No caso deste método os anos de experiência do desenvolvedor não seriam consistentes
    * e nem a verificação de que ele é "raiz", pois essa informação estaria mudando a gosto do freguês.
    *
    * Quando mudar dessa forma, não é o objetivo deste exemplo.
    *
    * Com o encapsulamento só é possível consultar o valor e ele será único conforme a instância
    * é gerada. E só poderá ser modificado pela própria classe.
    */
    public bool EhRaiz()
    {
        return AnosDeExperiencia >= 10;
    }

    public void BeberCafe()
    {
        PegarCaneca();
        EscolherCafe();
        EncherCaneca();
        EntornarNaBoca();
    }

    protected void EscreverCodigo(string codigo)
    {
        Console.WriteLine(codigo + " - Desenvolvidor por " + Nome);
    }

    protected void MapearCenariosDeTestes()
    {
        // Implementação...
    }
  
    private void PegarCaneca()
    {
        // Implementação...
    }

    private void EscolherCafe()
    {
        // Implementação...
    }

    private void EncherCaneca()
    {
        // Implementação...
    }

    private void EntornarNaBoca(); 
    {
        // Implementação...
    }
}
using System;

class Program
{
    public static void Main()
    {
        /**
        * Exemplo Violando Encapsulamento
        * 
        * Obs: Esse código só seria executado se a classe não fosse abstrata e se o atributo "AnosDeExperiencia" ainda fosse público
        * Mas para fins de exemplo, essa demonstração é válida
        */        
        Desenvolvedor desenvolvedorJava = new Desenvolvedor("Javeiro", 10, LinguagemDeProgramacao.Java);
        desenvolvedorJava.AnosDeExperiencia = 5;
        
        if (desenvolvedorJava.EhRaiz()) 
        {
            desenvolvedorJava.BeberCafe();
        }
        
        desenvolvedorJava.AnosDeExperiencia = 12;
        
        /**
        * Exemplo Aplicando Encapsulamento
        * 
        * Não é possível alterar os anos de experiência agora, pois ele não está acessível. A instância criada
        * não o expõe. O mundo externo ou quem está usando a classe não conhece os atributos. Pois eles estão protegidos.
        *
        */        
        Desenvolvedor desenvolvedorCSharp = new Desenvolvedor("Desenvolvedor CSharp", 10, LinguagemDeProgramacao.CSharp);
        
        // Um erro ocorre aqui ao tentar acessar um atributo desconhecido externamente
        desenvolvedorCSharp.AnosDeExperiencia = 5;
        
        // Temos segurança de que esse método funcionará corretamente com os dados iniciais passados para instância
        if (desenvolvedorCSharp.EhRaiz()) 
        {
            desenvolvedorCSharp.BeberCafe();
        }        
  }
}

Herança

É uma das formas de relacionar classes/objetos e/ou compartilhar lógica de implementação.

Comumente utilizada para permitir o reuso das características (atributos) e comportamentos (métodos ) que são comuns para coisas que tem algum tipo de parentesco.

Esse tipo relação é comumente expressada como "classe pai e classe filha", "Super Classe e Subclasse", "Super Classe e Classe Derivada" e também como "é um(a) alguma coisa".

A seguir, o exemplo mostra 2 classes derivadas da classe "Desenvolvedor", são elas "DesenvolvedorCSharp" e "DesenvolvedorJava". Observe que no mundo real ambos os desenvolvedores tem características e desenvolvem comportamentos similares. Só que cada um de sua forma.

Veja que as características (atributos) e comportamentos (métodos) comuns entre ambos não foram específicados em sua totalidade novamente. Pois, foi tirado proveito do benefício da herança! (Veja que a classe contém métodos abstratos)

Sobre os métodos definidos como abstratos na classe Desenvolvedor, estamos dizendo para as classes que serão derivadas (filhas) dela, que obrigatoriamente elas devem ter aqueles métodos, mas a forma como a lógica deles é executada pode ser diferente uma da outra. Ou seja, eles fazem a mesma coisa de formas diferente.

Aviso Importante: A herança não é a única forma de criar relacionamento entre classes. Existem outras formas como Associação, Composição e Agregação. Mas aqui, apenas a herança será abordada. Pois ela é considerada um dos pilares da orientação a objetos, as outras são consideradas técnicas.

using System;

public class DesenvolvedorCSharp : Desenvolvedor
{
    public DesenvolvedorCSharp(string nome, int anosDeExperiencia) : base(nome, anosDeExperiencia, LinguagemDeProgramacao.CSharp)
    {
        Nome = nome;
        AnosDeExperiencia = anosDeExperiencia;
    }

    /**
    * O Desenvolvedor C# tem sua forma de codar.
    * Portanto, o método codar foi definido como abstrato na classe base (Desenvolvedor)
    * Assim, é possível implementar de forma que se enquadre ao contexto da classe derivada (filha)
    */
    public void Codar(string codigo)
    {
        AbrirVisualStudioIDE();
        ConfigurarAppSettings();
        CriarSolucao();
        CriarProjetos();
        ReferenciarProjetos();
        EscreverCodigo(codigo);
    }

    public void EscreverTestesUnitarios(string codigo)
    {
        MapearCenariosDeTestes();
        EscreverCodigo(codigo);
    }

    private void AbrirVisualStudioIDE()
    {
        // Implementação...
    }

    private void ConfigurarAppSettings()
    {
        // Implementação...
    }

    private void CriarSolucao()
    {
        // Implementação...
    }

    private void CriarProjetos()
    {
        // Implementação...
    }

    private void ReferenciarProjetos()
    {
        // Implementação...
    }
}
using System;

public class DesenvolvedorJava : Desenvolvedor
{
    public DesenvolvedorJava(string nome, int anosDeExperiencia) : base(nome, anosDeExperiencia, LinguagemDeProgramacao.Java)
    {
        Nome = nome;
        AnosDeExperiencia = anosDeExperiencia;
    }

    /**
    * O Desenvolvedor Java tem sua forma de codar.
    * Portanto, o método codar foi definido como abstrato na classe base (Desenvolvedor)
    * Assim, é possível implementar de forma que se enquadre ao contexto da classe derivada (filha)
    * Note a diferença entre a implementação deste método e a do método da classe DesenvolvedorCSharp
    */
    public void Codar(string codigo)
    {
        AbrirEclipseIDE();
        ConfigurarPomXml();
        ResolverProblemasDaIDE();
        ResolverProblemasDoMaven();
        EsperarBoaVontadeDaIDEAbrir();
        EscreverCodigo(codigo);
    }
    
    /**
    * O mesmo ocorre com este método.
    * Observe que também não é necessário criar uma implementação parecida em algum aspecto com
    * o de outras classes derivadas. Na classe DesenvolvedorCSharp a quantidade de métodos auxiliares
    * na implementação é menor, aqui alguns passos a mais são executados. 
    */
    public void EscreverTestesUnitarios(string codigo)
    {
        MapearCenariosDeTestes();
        ConfigurarPomXml();
        ConfigurarJUnit();
        EscreverCodigo(codigo);
    }

    private void AbrirEclipseIDE()
    {
        // Implementação...
    }

    private void ConfigurarPomXml()
    {
        // Implementação...
    }

    private void ResolverProblemasDaIDE()
    {
        // Implementação...
    }

    private void ResolverProblemasDoMaven()
    {
        // Implementação...
    }

    private void EsperarBoaVontadeDaIDEAbrir()
    {
        // Implementação...
    }
}
using System;

class Program
{
    public static void Main()
    {
        // Os parâmetros definidos no construtor da classe base (Desenvolvedor) são passados normalmente para as classes derivadas (filhas)
        // Ou seja, as classes DesenvolvedorCSharp e DesenvolvedorJava também possuem os atributos Nome, AnosDeExperiencia e LinguagemDeProgramacao
        // Pois herdaram estes foram herdados
        DesenvolvedorCSharp desenvolvedorCSharp = new DesenvolvedorCSharp("Desenvolvedor CSharp", 10);
        DesenvolvedorJava desenvolvedorJava = new DesenvolvedorJava("Javeiro", 12);

        // Note que, assim como os atributos, os métodos também foram herdados
        desenvolvedorCSharp.Codar("Olá Mundo");
        desenvolvedorCSharp.EscreverTestesUnitarios("Teste Com CSharp - Olá Mundo");

        desenvolvedorJava.Codar("Olá Mundo");
        desenvolvedorJava.EscreverTestesUnitarios("Teste com Java - Olá Mundo");     
  }
}

Polimorfismo

É o mecanismo que permite que duas ou mais classes que herdam comportamentos (métodos) através de herança, se comportem de forma diferente.

Ou seja, métodos com o mesmo nome podem executar códigos e lógicas opostas, parecidas, complementares ou completamente diferentes mesmo!

E isso pode ser feito de duas formas: Estática ou Dinâmica.

Polimorfismo na forma estática (Sobrecarga - Overload)

O polimorfismo na forma estática é também muito conhecido como "sobrecarga" ou para os mais íntimos como "overload".

Isso permite a existência de vários métodos com o mesmo nome mas com quantidade, tipos e/ou ordem de parâmetros levemente diferentes.

Para aplicá-lo é necessário apenas criar outro método com o mesmo nome e uma quantidade de parâmetros diferentes, ordens diferentes ou tipos diferentes (ou não).

Polimorfismo na forma dinâmica(Sobreposição, Reescrita - Override)

Já o polimorfismo na forma forma dinâmica é também muito conhecido como "sobreposição", "reescrita" ou para os mais íntimos "override".

Diferente da sobrecarga (forma estática), na sobreposição é necessário que os métodos tenham exatamente o mesmo nome, tipo de retorno e quantidade de parâmetros.

Para aplicá-lo, basta criar outro método com o mesmo nome, tipo e retorno, não precisa necessariamente ter parâmetros. A implementação de sua lógica que deve ser diferente. Inclusive, ela pode ser complementada com o método da classe base (pai/herdada) caso ele já tenha implementação (Olha só como abstração pode ser complementada, lembra?)

Note que no exemplo a seguir as duas formas de polimorfismo são aplicadas. tanto a sobrecarga como a sobreposição.

Aviso Importante: Por se tratar de um exemplo com objetivos didáticos, a implementação foi feita de forma bem sútil.
Em um projeto real as diferenças podem ser gritantes. Além disso, no exemplo da sobrecarga apenas a ordem dos parâmetros foi alterada.
E no exemplo da sobrescrita apenas os valores de comparação foram alterados e foi feita a inclusão de uma verificação adicional.
Note a presença das palavras-chave virtuale overrideque no C# tratam-se de um recurso que nos permite sinalizar e identificar métodos que sofrem aplicação do polimorfismo na forma dinâmica. Onde "virtual" indica que o método da classe base pode ser sobrescrito por um método da classe deriva e essa sobrescrita é feita através do uso da palavra-chave "override" .

using System;

abstract class Desenvolvedor
{
    protected string Nome { get; }
    protected LinguagemDeProgramacao LinguagemPreferida { get; }
    protected int AnosDeExperiencia { get; }

    protected Desenvolvedor(string nome, int anosDeExperiencia, LinguagemDeProgramacao linguagemPreferida)
    {
        Nome = nome;
        LinguagemPreferida = linguagemPreferida;
        AnosDeExperiencia = anosDeExperiencia;
    }

    public abstract void Codar(string codigo);

    public abstract void EscreverTestesUnitarios(string codigo);

    public virtual bool EhRaiz()
    {
        return AnosDeExperiencia >= 10;
    }

    public void BeberCafe()
    {
        PegarCaneca();
        EscolherCafe();
        EncherCaneca();
        EntornarNaBoca();
    }

    protected void EscreverCodigo(string codigo)
    {
        Console.WriteLine(codigo + " - Desenvolvidor por " + Nome);
    }

    protected void MapearCenariosDeTestes()
    {
        // Implementação...
    }
  
    private void PegarCaneca()
    {
        // Implementação...
    }

    private void EscolherCafe()
    {
        // Implementação...
    }

    private void EncherCaneca()
    {
        // Implementação...
    }

    private void EntornarNaBoca(); 
    {
        // Implementação...
    }
}
using System;

public class DesenvolvedorCSharp : Desenvolvedor
{
    public DesenvolvedorCSharp(string nome, int anosDeExperiencia) : base(nome, anosDeExperiencia, LinguagemDeProgramacao.CSharp)
    {
        Nome = nome;
        AnosDeExperiencia = anosDeExperiencia;
    }

    // Olha a sobrecarga!!! Neste caso os parâmetros só estão sendo passados em ordens diferentes
    public DesenvolvedorCSharp(int anosDeExperiencia, string nome) : base(nome, anosDeExperiencia, LinguagemDeProgramacao.CSharp)
    {
        AnosDeExperiencia = anosDeExperiencia;
        Nome = nome;
    }

    /**
    * Olha a sobrescrita (override).
    * Aqui apenas o valor usado para comparação é diferente, mas é uma variação na implementação.
    */
    public override EhRaiz()
    {
        return AnosDeExperiencia >= 30;
    }

    public void Codar(string codigo)
    {
        AbrirVisualStudioIDE();
        ConfigurarAppSettings();
        CriarSolucao();
        CriarProjetos();
        ReferenciarProjetos();
        EscreverCodigo(codigo);
    }

    public void EscreverTestesUnitarios(string codigo)
    {
        MapearCenariosDeTestes();
        EscreverCodigo(codigo);
    }

    private void AbrirVisualStudioIDE()
    {
        // Implementação...
    }

    private void ConfigurarAppSettings()
    {
        // Implementação...
    }

    private void CriarSolucao()
    {
        // Implementação...
    }

    private void CriarProjetos()
    {
        // Implementação...
    }

    private void ReferenciarProjetos()
    {
        // Implementação...
    }
}
using System;

public class DesenvolvedorJava : Desenvolvedor
{
    public DesenvolvedorJava(string nome, int anosDeExperiencia) : base(nome, anosDeExperiencia, LinguagemDeProgramacao.Java)
    {
        Nome = nome;
        AnosDeExperiencia = anosDeExperiencia;
    }
    
    // Olha a sobrecarga!!! Neste caso os parâmetros só estão sendo passados em ordens diferentes
    public DesenvolvedorJava(int anosDeExperiencia, string nome) : base(nome, anosDeExperiencia, LinguagemDeProgramacao.Java)
    {
        AnosDeExperiencia = anosDeExperiencia;
        Nome = nome;
    }

    /**
    * Olha a sobrescrita (override).
    * Diferente da classe DesenvolvedorCSharp, aqui além do valor usado para comparação ser diferente, 
    * uma verificação nom atributo nome é feita. 
    *
    * Ou seja, este método, nessa classe verifica se o desenvolvedor é raiz de forma diferente!
    */
    public override EhRaiz()
    {
        return Nome.Contains("Dinossauro") && AnosDeExperiencia >= 60;
    }

    public void Codar(string codigo)
    {
        AbrirEclipseIDE();
        ConfigurarPomXml();
        ResolverProblemasDaIDE();
        ResolverProblemasDoMaven();
        EsperarBoaVontadeDaIDEAbrir();
        EscreverCodigo(codigo);
    }

    public void EscreverTestesUnitarios(string codigo)
    {
        MapearCenariosDeTestes();
        ConfigurarPomXml();
        ConfigurarJUnit();
        EscreverCodigo(codigo);
    }

    private void AbrirEclipseIDE()
    {
        // Implementação...
    }

    private void ConfigurarPomXml()
    {
        // Implementação...
    }

    private void ResolverProblemasDaIDE()
    {
        // Implementação...
    }

    private void ResolverProblemasDoMaven()
    {
        // Implementação...
    }

    private void EsperarBoaVontadeDaIDEAbrir()
    {
        // Implementação...
    }
}
using System;

class Program
{
    public static void Main()
    {
        /**
        * Note que os parâmetros do construtor de cada classe são passados em ordens diferentes
        * É nesse cenário que ocorre a sobrecarga do método 
        */
        Desenvolvedor desenvolvedorCSharp = new DesenvolvedorCSharp("Desenvolvedor CSharp", 31);
        Desenvolvedor outroDesenvolvedorCSharp = new DesenvolvedorCSharp(8, "Outro Desenvolvedor CSharp");
        
        Desenvolvedor desenvolvedorJava = new DesenvolvedorJava(65, "Javeiro Dinossaro");
        Desenvolvedor outroDesenvolvedorJava = new DesenvolvedorJava("Javeiro", 6);

        desenvolvedorCSharp.Codar("Olá Mundo");
        outroDesenvolvedorCSharp.Codar("Outro Olá Mundo");        
        desenvolvedorCSharp.EscreverTestesUnitarios("Teste Com CSharp - Olá Mundo");
        outroDesenvolvedorCSharp.EscreverTestesUnitarios("Outro Teste Com CSharp - Olá Mundo");

        desenvolvedorJava.Codar("Olá Mundo");
        outroDesenvolvedorJava.Codar("Outro Olá Mundo");
        desenvolvedorJava.EscreverTestesUnitarios("Teste com Java - Olá Mundo");
        outroDesenvolvedorJava.EscreverTestesUnitarios("Outro Teste com Java - Olá Mundo");
        
        /**
        * Note que só beberá o café os desenvolvedores que atenderem as condições especificadas na implementação de seus métodos
        * que verificam se eles são "raiz" ou não. Ou seja, o resultado varia conforme a lógica, mas, no final faz a mesma coisa.
        */
        if (desenvolvedorCSharp.EhRaiz()) 
        {
            desenvolvedorCSharp.BeberCafe();
        }
        
        if (outroDesenvolvedorCSharp.EhRaiz()) 
        {
            outroDesenvolvedorCSharp.BeberCafe();
        }
        
        if (desenvolvedorJava.EhRaiz()) 
        {
            desenvolvedorJava.BeberCafe();
        }
        
        if (outroDesenvolvedorJava.EhRaiz()) 
        {
            outroDesenvolvedorJava.BeberCafe();
        }
  }
}

Conclusão

Acredito que agora você tem uma ideia melhor sobre os 4 pilares básicos da orientação a objetos e poderá explicá-lo para os outros de forma simples. Seja no dia a dia de trabalho, na faculdade ou até mesmo na entrevista técnica!!! Além disso saberá também como aplicar na prática!!! Olha que top?!!

Mas lembre-se sempre de que estes pilares sempre estarão na vanguarda de qualquer linguagem de programação orientada a objetos.

Eles podem até parecer complicados no começo, mas quando você entende de fato qual o papel de cada um deles você só colhe benefícios, especialmente quando sabe aplicar bem na prática.

E eles existem neste paradigma para facilitar nossa compreensão sobre concepção, segurança, relações e comportamentos de classes e objetos em qualquer linguagem de programação que ofereça suporte nativamente ou não.

Se você gostou desse artigo, deixa seu "gostei" e compartilha com algum amigo ou colega de trabalho. E se tiver alguma crítica, complemento ou sugestão de correção de algum "BUG" detectado no artigo, deixa nos comentários ou entra em contato comigo através do linkedin.

Será um prazer ouvi-lo(a)!!!!

Referências

Sobre o Autor

Kaique Prazeres é Arquiteto de Soluções na Operação Nacional da Programmers Beyond IT e é um ferrenho defensor de boas práticas e qualidade de software com foco em redução de custos e aumento de longevidade de sistemas.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.