Princípios SOLID: responsabilidade única e aberto/fechado.

Por dti digital|
Atualizado: Jul 2023 |
Publicado: Jan 2018

SOLID é um acrônimo mnemônico criado por Michael Feathers e popularizados pelo Uncle Bob, Robert C. Martin, que relaciona a cada letra um tópico de boas práticas de programação. A prática destes princípios SOLID visa deixar o projeto mais coeso, reaproveitável e torna a sua manutenção mais fácil.

Este artigo abordará as duas primeiras letras – ou melhor, as duas primeiras práticas –, o Single responsibility principle (princípio da responsabilidade única) e o Open/closed principle (principio aberto/fechado).

solid
Figura 1 – Princípios de um código orientado a objetos sustentável

O princípio da responsabilidade única tem como regra que uma classe deve possuir uma, e apenas uma, responsabilidade. Pode-se traduzir isso em “uma classe deve ter apenas um motivo para mudar”.

Por que uma classe deve ter apenas uma responsabilidade?

A reposta é simples: quanto maior o número de responsabilidades, maiores as chances de modificação dessa classe no futuro e maiores as chances de inserção de bugs que atrapalharão a classe por inteiro.

Imaginemos o seguinte exemplo:

#region Código 1
public class Cozinheiro
{
	public void fazerPrato(object prato) 
	{
        // código para a realização de pratos
    }
 
    public double valorDoPrato(object prato) 
	{
        // código para o calculo do valor do prato
    }
	
    public void recolherPratos() 
	{
        // código para recolher os pratos
    }
}
#endregion

Esta classe é responsável por preparar o prato e calcular o valor do prato e recolher os pratos. Esses ‘es’ indicam que a nossa classe possui mais de uma responsabilidade, e assim, mais de um motivo para ser modificada. Caso desejássemos modificar a maneira com que os pratos são recolhidos, teríamos que modificar a nossa classe Cozinheiro. Caso esta modificação gerasse algum bug, não apenas a retirada dos pratos, mas sim todas as funcionalidades da classe poderiam estar comprometidas. A classe Cozinheiro deixaria de preparar a comida devido um problema com a retirada de pratos.

Quer ver mais conteúdos como esse?

Como deveria ser?

A pergunta aqui é: qual a responsabilidade da classe Cozinheiro? Preparar o prato! Perceba que não importam a quantidade de métodos necessários para tal. A ‘responsabilidade única’ está ligada à função da classe, e não à maneira que esta função é implementada. Veja como ficaria a refatoração do código:

 

#region Código 2
public class Cozinheiro
{
	public void fazerPrato(object prato) 
	{
        // código para a realização de pratos
    }
}

public class Caixa
{	
    public double valorDoPrato(object prato) 
	{
        // código para o calculo do valor do prato
    }
}
	
public class Garcon
{	
    public void recolherPratos() 
	{
        // código para recolher os pratos
    }
}
#endregion

A classe Cozinheiro agora possui apenas uma única responsabilidade: preparar o prato. O único motivo para alterar essa classe é caso a forma de preparar o prato seja modificada, reduzindo assim a complexidade, facilitando a legibilidade e reduzindo o acoplamento.

Figura 2 - "Só porque você pode, não significa que você deva"
Figura 2 – “Só porque você pode, não significa que você deva”

O princípio aberto/fechado diz que as entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação, ou seja, deve ser possível estender o comportamento de uma classe, mas não a modificar.

Qual a importância deste princípio?

A extensibilidade! É comum a manutenção de códigos. Você deve, então, lidar com as classes já criadas e, possivelmente, adicionar ou remover funcionalidades. Para uma classe ‘não-extensível’, a adição ou remoção de funcionalidades implica na modificação da classe por si só. Ao fazer tal mudança, você estará propenso a fazer com que outras partes do seu código não funcionem mais.

Utilizando a nossa classe Cozinheiro, poderíamos implementar a função de preparar pratos da seguinte forma:

#region Código 3
public class Cozinheiro
{
	public void fazerPrato(object prato) 
	{
		if (prato is Omelete)
		{
		// código para a realização de um omelete
		}
		
		if (prato is Macarrao)
		{
		// código para a realização de um macarrao
		}
		
		if (prato is FrangoAssado)
		{
		// código para a realização de um frango assado
		}
    }
}

public class Omelete
{
// propriedades do omelete
}
public class Macarrao
{
// propriedades do macarrão
}
public class FrangoAssado
{
// propriedades do frango assado
}

#endregion

Aparentemente, nenhum problema. Mas e se quiséssemos implementar um prato novo? Teríamos que modificar a classe Cozinheiro.

E qual é o problema de um IF a mais na classe?

Ao adicionar outro IF de validação, além de termos que substituir a classe na publicação da nova versão, estaremos correndo o risco de introduzir bugs em uma classe que já estava funcionando.

Assim, para cada prato novo, seriam necessárias novas alterações na classe Cozinheiro, e para cada uma dessas alterações, teríamos que nos assegurar que as funcionalidades anteriores continuassem funcionando e que as novas funcionalidades não interferissem nas antigas.

Então qual é a solução?

A utilização da abstração. A classe pai deve possuir o método exigido (que no nosso caso é o fazerPrato), enquanto os filhos dessa classe devem apenas implementar a forma com que este método será executado. Veja como este princípio seria implementado no exemplo anterior:

#region Código 4
public class Cozinheiro
{
	public void fazerPrato(Prato prato) 
	{
		prato.Fazer();
    }
}

public abstract class Prato
{
	public abstract void Fazer();
}

public class Omelete : Prato
{
// propriedades do omelete

	public override void Fazer()
	{
	// código para a realização de um omelete
	}
}

public class Macarrao : Prato
{
// propriedades do macarrao

	public override void Fazer()
	{
	// código para a realização de um macarrao
	}
}

public class FrangoAssado : Prato
{
// propriedades do frango assado

	public override void Fazer()
	{
	// código para a realização de um frango assado
	}
}
#endregion

Possuímos agora uma abstração bem definida, onde todas as extensões implementam suas próprias regras de negócio sem necessidade de modificar uma funcionalidade devido a remoção, mudança ou inclusão de outra.

Dessa forma a nossa nova classe Cozinheiro está aberta para extensão pois podemos incluir tantos pratos quanto desejarmos, mas está fechada para modificação pois podemos fazer isso sem ter que alterar a classe. Assim, além da independência entre as extensões, o código fica mais claro e a aplicação de testes de unidades fica mais fácil e coesa.

Observe assim que aplicando apenas dois princípios do SOLID em conjunto, aumenta-se bastante as chances de que o sistema criado seja mais claro e simples de estender.

Tem alguma dúvida sobre os princípios SOLID? Entre em contato com a gente.

Por: Victor Castro.
Revisão: Jéssica Saliba.

Quer saber mais?

Desenvolvimento de Software

Confira outros artigos

Veja outros artigos de Desenvolvimento de Software