Princípios SOLID que todos desenvolvedores deveriam saber

11 maio

Fala Galera,

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

2 Replies to “Princípios SOLID que todos desenvolvedores deveriam saber

  1. Gostei, apesar de já ter estudado o SOLID algumas vezes, é sempre bom revisar para validar se não estamos quebrando os princípios no dia a dia, e seus exemplos foram bem simples e objetivo.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *