Os Princípios do SOLID e como criar softwares mais robustos

Node JS

Os Princípios do SOLID e como criar softwares mais robustos

Sempre que vamos iniciar um novo projeto ou começar a codificar alguma nova feature, pensamos em fazer isso com a melhor qualidade possível. Isso demonstra que temos preocupação com o que vai ser entregue e com o produto final. Por isso, conhecer e usar as boas práticas de programação tem como finalidade auxiliar nesse caminho, reduzindo a complexidade do código, o baixo acoplamento entre classes, separação de responsabilidades e uma boa definição de relação entre elas. Nesse contexto, iremos ver sobre o SOLID e os seus 5 princípios da programação orientada a objetos que facilitam no desenvolvimento de softwares, tornando-os mais fáceis de se entender e manter.

O que é S.O.L.I.D

O SOLID é um acrônimo que representa cinco princípios da programação orientada a objetos e design de código teorizados pelo nosso querido Uncle Bob (Robert C. Martin) por volta do ano 2000. O autor Michael Feathers foi responsável pela criação do acrônimo:

**[S]**ingle Responsibility Principle (Princípio da Responsabilidade Única)

**[O]**pen/Closed Principle (Princípio do Aberto/Fechado)

**[L]**iskov Substitution Principle (Princípio da Substituição de Liskov)

**[I]**nterface Segregation Principle (Princípio da Segregação de Interfaces)

**[D]**ependency Inversion Principle (Princípio da Inversão de Dependências)

Mas quais são os benefícios de se utilizar SOLID?

Existem muitos benefícios em se utilizar SOLID em seus projetos, independente da linguagem (desde que seja POO). Abaixo alguns pontos que podem te ajudar a entender os seus benefícios.

  • Seja fácil de se manter, adaptar e se ajustar às alterações de escopo;
  • Seja testável e de fácil entendimento;
  • Seja extensível para alterações com o menor esforço necessário;
  • Que forneça o máximo de reaproveitamento;
  • Que permaneça o máximo de tempo possível em utilização.

SRP — Princípio da Responsabilidade Única

A class should have one, and only one, reason to change.

Esse é o primeiro princípio e diz que “uma classe deve ter somente um motivo para mudar”, em outras palavras, deve ter somente uma única responsabilidade dentro do software, ou seja a classe deve ter apenas uma única tarefa ou ação a ser executada.

public class Student {
	public void GetGrades(){ /***/ }
	public void AddGrades(){ /***/ }

	public void CalculateGrades(){ /***/ }

	public void Update(){ /***/ }
}

Reparem que essa classe faz muitas coisas que não necessariamente são tarefas dela. Quando se viola o princípio da responsabilidade única, você começa ter problemas como:

  • Alto acoplamento – Mais responsabilidades geram um maior nível de dependências, deixando o sistema engessado e difícil para alterações;
  • Falta de coesão – Uma classe não deve assumir responsabilidades que não são suas;
  • Dificuldade para criar testes automatizados – Torna-se difícil a tarefa de criar testes automatizados, pois é difícil criar um “mock” para este tipo de classe;
  • Dificuldade para fazer o reaproveitamento de código;

OCP — Open-Closed Principle

You should be able to extend a classes behavior, without modifying it.

Diz que “as entidades de software devem ser abertas para ampliação, mas fechadas para modificação”. Simplificando o significado, isso se refere ao fato de que podemos estender o comportamento de uma classe, quando for necessário, por meio de herança, interface e composição, mas não podemos permitir a abertura dessa classe para fazer pequenas modificações.

public class CreditCard
    {
        private int _TypeOfCreditCart;
        public int TypeOfCreditCart { get; set; }
        public double GetDiscount(double monthlyCost)
        {
            if (_TypeOfCreditCart == 1)
            {
                return monthlyCost * 0.10;
            }
            else
            {
                return monthlyCost * 0.05;
            }
        }
    }

Da forma que está a classe CreditCard precisa verificar o tipo do cartão para aplicar a regra de negócio correta na hora do desconto. Supondo que a empresa cresceu e resolveu trabalhar com outras bandeiras de cartão de crédito, seria necessário modificar essa classe! Dessa forma, estaríamos quebrando o princípio Open-Closed.

Mas por que não alteramos a classe CreditCard?

Olhando para o código não seria mais fácil apenas acrescentar mais um IF e verificar o novo tipo de bandeira de cartão aplicando as respectivas regras? Sim, e provavelmente essa seria a solução que programadores menos experientes iriam fazer. Mas, esse é exatamente o problema! Alterar uma classe já existente para adicionar um novo comportamento, corremos um sério risco de introduzir bugs em algo que já estava funcionando.

Corrigindo a classe aplicando o OCP

Vamos para o exemplo, podemos aplicar as premissas de se isolar o comportamento extensível atrás de uma classe abstrata ou interface, podemos criar uma class abstract com o nome CreditCart contendo o método GetDiscount(), e fazer com que nossas classes de cartão de crédito implementem essa classe abastract. Além disso, iremos colocar as regras de cálculo de desconto para suas respectivas classes, dentro do método GetDiscount(), fazendo com que no momento da utilização o código dependa somente da classe abstrata CrediCart que iremos criar.

Veja o código abaixo refatorado.

abstract class CreditCart
    {
        public abstract double GetDiscount(double monthlyCost);
    }

class VisaCard: CreditCart
    {
        public override double GetDiscount(double monthlyCost)
        {
            return monthlyCost * 0.10;
        }
    }

class MasterCard: CreditCart
    {
        public override double GetDiscount(double monthlyCost)
        {
            return monthlyCost * 0.05;
        }
    }

class Program
    {
        static void Main(string[] args)
        {
            CreditCart card = new VisaCard();
            var discount1 = card.GetDiscount(100);
            Console.WriteLine($"visa : {discount1} %");

            card = new MasterCard();
            var discount2 = card.GetDiscount(100);
            Console.WriteLine($"master card : {discount2} %");
            Console.ReadLine();
        }
    }

LSP— Liskov Substitution Principle

“Derived classes must be substitutable for their base classes.”

O princípio de Liskov diz que “uma classe derivada deve ser substituível por sua classe base“, simplificando diz que as classes/tipos base podem ser substituídas por qualquer uma das suas subclasses. O principio de Liskov foi introduzido por Barbara Liskov em sua conferência “Data abstraction” em 1987. A definição formal de Liskov diz que:

se S é um subtipo de T, então os objetos do tipo T, em um programa, podem ser substituídos pelos objetos de tipo S sem que seja necessário alterar as propriedades deste programa.Wikipedia.

Vamos ver o exemplo abaixo para facilitar o entendimento:

using System;
using System.Collections.Generic;
			
class Vehicle {
	public virtual void StartEngine(){
		Console.WriteLine("Vehicle is started");
	}
}

class Car: Vehicle {
	public override void StartEngine(){
		EngageIgnition();
		Console.WriteLine("Car is started");
	}

	private void EngageIgnition() {
	   // Procedimento de ignição
   }
}

class ElectricCar: Vehicle {
	public override void StartEngine(){
		TurnOnPower();
		Console.WriteLine("Eletric Car is started");
	}

	private void TurnOnPower() {
	   // Procedimento de ligar energia
   }
}

public class Program
{
	public static void Main()
	{
		List<Vehicle> listOfVehicle = new List<Vehicle>();
		listOfVehicle.Add(new Car());
		listOfVehicle.Add(new ElectricCar());
						
		foreach (var vehicle in listOfVehicle) {
			vehicle.StartEngine();
		}
	}
}

Algumas coisas que temos que ficar atentos sobre o princípio de Liskov e sobre as suas violações, abaixo uma lista do que viola o princípio:

  • Sobrescrever/implementar um método que não faz nada;
  • Lançar uma exceção inesperada;
  • Retornar valores de tipos diferentes da classe base;

ISP — Interface Segregation Principle

Make fine grained interfaces that are client specific.

O princípio da segregação de interface diz que “Uma classe não deve ser forçada a implementar interfaces e métodos que não irão utilizar”, esse princípio refere-se especificamente na forma como criamos nossas interfaces e que devemos basicamente criar interfaces mais específicas do que genéricas. Dessa forma evitamos implementar métodos que não são necessários.

Veja abaixo:

public interface Vehicle {
	void StartEngine();
	void Run();
	void OpenDoors();
}

public class Car : Vehicle {
	public void StartEngine(){}
	public void Run(){}
	public void OpenDoors(){}
}

public class Motorcycle: Vehicle {
	public void StartEngine(){}
	public void Run(){}
	public void OpenDoors(){}
}

Perceba que ao criar a interface Vehicle, atribuímos comportamentos genéricos e isso acabou forçando a classe Motorcycle a implementar o método OpenDoors() que ela não deveria ter, pois motos e veículos de duas rodas não tem portas! Com isso, estamos violando o Interface Segregation Principle — E o LSP também!

Para corrigir, devemos criar interfaces específicas. Veja:

public interface Vehicle {
	void StartEngine();
	void Run();
}

public interface VehicleWithDoors : Vehicle {
	void OpenDoors();
}

public class Car : VehicleWithDoors {
	public void StartEngine(){}
	public void Run(){}
	public void OpenDoors(){}
}

public class Motorcycle: Vehicle {
	public void StartEngine(){}
	public void Run(){}
}

DIP — Dependency Inversion Principle

Depend on abstractions, not on concretions.

Este com certeza é um dos princípios mais populares e aposto que se você não ouviu falar já viu ou implementou algo que utiliza esse princípio como base e ele diz “Dependa de abstrações e não de implementações”.

De acordo com Uncle Bob, esse princípio pode ser definido da seguinte forma:

  1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.
  2. As abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Normalmente é fácil de confundir o princípio da inversão de dependência que é um conceito com a injeção de dependência que é um design partner. Apesar disso, ambos têm relação entre si, pois ajudam a deixar o código desacoplado.

Abaixo segue um código que representa a Inversão de Dependência e o uso da Injeção de Dependência.

public class Drive
{
		private readonly IVehicle _vehicle;

		public Drive(IVehicle vehicle)
    {
        _vehicle = vehicle;
    }
 
    public void Add(Car car)
    {
	     _vehicle.Run(car);
    }
}

public class Car
{
    public int CarId { get; set; }
    public string Name{ get; set; }
}
 
public interface IVehicle
{
    void Run(Car car);
}
 
public class Vehicle
{
    public void Run(Car car)
    {
        // procedimento para o carro andar
    }
}

Conclusão

A ideia desse artigo é demonstrar como os princípios do SOLID podem te ajudar a melhor a escrita de código e como o uso dos princípios em projetos de software podem tirar proveito dos benefícios do correto uso da OOP e por fim evitando problemas como duplicação de código, falta de padronização, isolamento de funcionalidades e dificuldade de manutenção.

De início parece ser complicado usar todos os princípios do SOLID, mas com a prática e a repetição você vai conseguir adquirir a experiência necessária para tornar os seus próximos softwares mais robustos, escaláveis e flexíveis.

Espero que este post tenha sido útil para você e se gostou compartilhe com seus amigos e não deixe de acessar outros conteúdos do blog.

Referências

Explore mais