Princípios SOLID que todos desenvolvedores deveriam saber
A programação Orientada a Objeto trouxe um novo paradigma para o desenvolvimento de software. A programação Orientada a Objetos permite que os desenvolvedores programem suas Classes e criem objetos com o propósitos e funcionalidades para atender uma demanda de negócio. Mas o paradigma da programação Orientada a Objetos não nos previne de escrever um código confuso ou no pior caso um software com baixa ou nenhuma manutenibilidade.
Com isso, um grupo de princípios de desenvolvimento de software foi agrupada por Robert C Martin. Esses cinco princípios nos guia de como podemos criar softwares legíveis e sustentáveis.
Esses cinco princípios foram chamado de SOLID (acrônimo foi derivado por Michael Feathers)
- S: Single Responsibility Principle
- O: Open-Closed Principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Principle
Nós iremos ver cada um desses princípios abaixo, porém, lembre-se os exemplos que foram utilizados aqui não são aplicados no nosso dia a dia ou mesmo são cenários reais. Eles foram usados para fácil compreensão. O mais importante é você entender o seus conceitos e como aplicá-los no seu dia a dia.
Single Responsibility Principle
A Class should have only one Job
Uma classe deve ser responsável por fazer apenas um trabalho. Se a classe tem mais de uma responsabilidade, ela tende-se a ter um acoplamento. Uma mudança em uma responsabilidade resulta em modificação de outra responsabilidade.
public class Produto
{
public string Nome { get; set; }
public Decimal Preco { get; set; }
public Produto(string nomeProduto, decimal precoProduto)
{
this.Nome = nomeProduto ?? throw new ArgumentNullException();
this.Preco = precoProduto;
}
public void Save()
{
//abreviado
}
public Produto GetProduto()
{
//abreviado
}
}
A classe “Produto” viola o princípio da responsabilidade única. E porque esta classe viola este princípio? O SRP nos guia que uma determinada classe só deva ter uma única responsabilidade. A classe “Produto” tem duas responsabilidades uma de gerenciar as suas propriedades e a segunda de armazenar e obter o “Produto” no banco de dados.
Então para atender ao princípio devemos separar as responsabilidades.
public class Produto
{
public string Nome { get; set; }
public Decimal Preco { get; set; }
public Product(string nomeProduto, decimal precoProduto)
{
this.Nome = nomeProduto ?? throw new ArgumentNullException();
this.Preco = precoProduto;
}
}
public class ProdutoRepository
{
public void Save()
{
//abreviado
}
public Produto GetProduct()
{
//abreviado
}
}
Open-Closed Principle
Software entities(Classes, modules, functions) should be open for extension, not modification.
Continuando com a nossa classe Produto, veja o exemplo abaixo
public class Product
{
public string Nome { get; set; }
public Decimal Preco { get; set; }
public Product(string nomeProduto, decimal precoProduto)
{
this.Nome = nomeProduto ?? throw new ArgumentNullException();
this.Preco = precoProduto;
}
public void AplicarDesconto()
{
if (this.Nome == "Geladeira")
this.Preco = this.Preco * .8m;
if (this.Nome == "Fogao")
this.Preco = this.Preco * .75m;
}
}
O método “AplicarDesconto” fere o principio Open-Closed porque ele não suporta outros tipos de Produto e assim toda vez que tiver um novo produto teremos que modificar o método para atender um novo tipo.
Agora, veja a nova implementação, não precisamos ficar modificando o método “AplicarDesconto”. Caso apareça um novo Produto basta estender o método “AplicarDesconto”
public class Produto
{
public string Nome { get; set; }
public Decimal Preco { get; set; }
private const decimal DESCONTO_PADRAO = .3M;
public virtual void AplicarDesconto()
{
this.Preco = this.Preco * DESCONTO_PADRAO;
}
public Produto(string nomeProduto, decimal precoProduto)
{
this.Nome = nomeProduto ?? throw new ArgumentNullException();
this.Preco = precoProduto;
}
}
public class Geladeira : Produto
{
public Geladeira(string nomeProduto, decimal precoProduto) : base(nomeProduto, precoProduto)
{
}
public override void AplicarDesconto()
{
this.Preco = this.Preco * .8m;
}
}
public class Fogao : Produto
{
public Fogao(string nomeProduto, decimal precoProduto) : base(nomeProduto, precoProduto)
{
}
public override void AplicarDesconto()
{
this.Preco = this.Preco * .75m;
}
}
Liskov Substitution Principle
A sub-class must be substitutable for its super-class
O objetivo deste princípio é que uma subclasse possa assumir sua superclasse sem erros. Se o código estiver verificando qual é o tipo de classe então podemos está violando este princípio.
Veja o exemplo abaixo ainda com a nossa classe produto
public string ObterCaracteristicaProduto(Produto produto)
{
if (produto is Geladeira)
return ObterCaracteristicaGeladeira(produto as Geladeira);
if (produto is Fogao)
return ObterCaracteristicaFogao(produto as Fogao);
return null;
}
O código acima está ferindo tanto o princípio de Liskov Substituion quanto do Open-Closed. Para cada produto precisamos saber qual é o produto e ainda se entrar um novo produto teremos que modificar o código para atender este novo produto.
Veja agora o exemplo abaixo
public abstract class Produto
{
public string Nome { get; set; }
public Decimal Preco { get; set; }
public abstract string ObterCaracteristica();
public Produto(string nomeProduto, decimal precoProduto)
{
this.Nome = nomeProduto ?? throw new ArgumentNullException();
this.Preco = precoProduto;
}
}
public class Geladeira : Produto
{
public Geladeira(string nomeProduto, decimal precoProduto) : base(nomeProduto, precoProduto)
{
}
public override string ObterCaracteristica()
{
return "Geladeira Frost Free";
}
}
public class Fogao : Produto
{
public Fogao(string nomeProduto, decimal precoProduto) : base(nomeProduto, precoProduto)
{
}
public override string ObterCaracteristica()
{
return "Fogao 4 bocas";
}
}
Agora nosso método pode chamar pela subclasse sem problemas e nem precisamos fazer Casting
public string ObterCaracteristicaProduto(Produto produto)
{
return produto.ObterCaracteristica();
}
Interface Segregation Principle
Make fine grained interfaces that are client specific. Clients should not be forced to depend upon interfaces that they do not use.
Esse princípio nos avisa sobre os problemas de utilizar interfaces muito grandes.
Veja a interface IShape no exemplo abaixo
public interface IShape
{
void DrawCircle();
void DrawSquare();
void DrawRectangle();
}
Essa interface desenha algumas formas como Circulo, Quadrado e Retângulo. As classes devem implementar todos os seus métodos. Veja no exemplo abaixo
public class Circle : IShape
{
public void DrawCircle()
{
//...
}
public void DrawSquare()
{
//...
}
public void DrawRectangle()
{
//...
}
}
public class Square : IShape
{
public void DrawCircle()
{
//...
}
public void DrawSquare()
{
//...
}
public void DrawRectangle()
{
//...
}
}
public class Reactangle : IShape
{
public void DrawCircle()
{
//...
}
public void DrawSquare()
{
//...
}
public void DrawRectangle()
{
//...
}
}
Já podemos perceber o problema né. A classe Circle é obrigada a implementar “DrawSquare” e “DrawRectangle”. A classe Quadrado é obrigada a implementar “DrawCircle” e “DrawRectangle” e assim vai. E se tivéssemos que colocar um novo método na interface por exemplo “DrawTriangule”. Todas as classes seriam obrigadas a implementar esse novo método não é mesmo?
Agora veja o exemplo abaixo
public interface IShape
{
void Draw();
}
public class Circle : IShape
{
public void Draw()
{
}
}
public class Square : IShape
{
public void Draw()
{
}
}
public class Rectangle : IShape
{
public void Draw()
{
}
}
public class Triangule : IShape
{
public void Draw()
{
}
}
Dependency Inversion Principle
Dependency should be on abstractions not concretions
A. High-level modules should not depend upon low-level modules. Both should depend upon abstractions.
B. Abstractions should not depend on details. Details should depend upon abstractions.
Esse princípio trata que nosso software é composto por componentes tanto em alto nível quanto em baixo nível. Quando isso ocorre temos que usar algum tipo de Injeção de Dependência para que possamos abstrair dos detalhes de implementação deste componentes, tirando o acoplamento e deixando nossas classes mais coesas.
Um exemplo clássico deste princípio é o padrão “Repository” aonde abstraímos dos detalhes de implementação do banco de dados ou um Storage qualquer.
Veja o exemplo
public class ProdutoRepository
{
private SqlConnection Connection { get; set; }
public ProdutoRepository(SqlConnection connection)
{
Connection = connection;
}
public void Save()
{
//abreviado
}
public Produto GetProduct()
{
//abreviado
}
}
O que acontece com a classe ProdutoRepository? Estamos com um acoplamento com o SqlConnection no qual caso nosso software precise trabalhar com outro banco torna muito complicado a troca.
Veja agora utilizando Interface
public class ProdutoRepository
{
private IDbConnection Connection { get; set; }
public ProdutoRepository(IDbConnection connection)
{
Connection = connection;
}
public void Save()
{
//abreviado
}
public Produto GetProduct()
{
//abreviado
}
}
Na classe acima fizemos uma pequena modificação, como diz o princípio devemos depender de abstrações e assim quando receber o “IDbConnection”, estamos recebendo uma abstração de conexão com o banco de dados. Essa abstração pode ser Oracle, SQL Server, MySQL e etc. Esses detalhes não importa, única coisa que interessa é que ela é responsável por fornecer um acesso ao Banco de Dados.
Essa abstração será fornecida por algum Injetor como SIMPLEINJECT, STRUCTMAP, NINJECT, entre outros e com esses injetores podemos escolher qual banco de dados melhor se encaixa para nossa aplicação.
Conclusão
Nós cobrimos os cinco princípios do SOLID que todos os desenvolvedores deveriam saber. No começo pode ser assustador mas com prática constante esses princípios entra no nosso dia a dia e se torna parte nós. Utilizando esses princípios você verá um grande impacto no desenvolvimento do software tornando os mesmos muito mais legíveis e com grande manutenibilidade. Sem contar que você se tornará um desenvolvedor melhor.
Abs e até a próxima
Java and Pl/SQL Programmer
5 aMuito útil. Seria excelente se todos os programadores tivessem acesso a tais conhecimentos desde cedo. Muito obrigado por compartilhar.
Desenvolvedor de front-end na Inovyo
5 aArtigo muito bom, curti também paulo. Paulo Ricardo G. dos Santos, PSD I, PSM I, CSM, SFC
Gerente de Operações de TI
5 aFabio Jacob 👍🏻
Senior Software Engineer | Cloud Solution Developer | 2x Azure & Github Certified
5 aLucas Terra
Senior Software Engineer (backend-heavy) - NodeJS (JS/TS) & Tech Lead & Ethical Hacker sometimes
5 aFinalmente um jeito fácil pra entender (e depois repassar) parabéns pelo conteúdo!!