Entendendo SOLID
SOLID é o acrônimo para 5 princípios, criado por Uncle Bob e útil para tornar sistemas mais fáceis de manter e estender.
Abaixo estão os 5 princípios que formam o acrônimo em inglês.
Contéudo
Single Responsibility - Responsabilidade única
Open/Closed - Aberto/Fechado
Liskov Substitution - Substituição Liskov
Interface Segregation - Segregação de Interface
Dependency Inversion - Inversão de Dependência
Single Responsibility
Este princípio declara que uma classe deve ter apenas uma responsabilidade. Sendo mais claro e levando para a prática, a classe deve ter apenas um motivo para mudar.
O princípio da responsabilidade única é um dos mais importantes, porque é a base para outros princípios e padrões abordando temas como acoplamento e coesão. Menos responsabilidades numa classe geram menos dependências.
Por exemplo, na classe abaixo. Vê-se que a classe Livro tem responsabilidades ligadas as propriedades do livro, mas também um método para imprimir a página.
class Livro {
function getTitulo() {
return "Mundo Sem Fim";
}
function getAutor() {
return "John Doe";
}
function virarPagina() {
// pointer to next page
}
function imprimirPaginaAtual() {
println("Contéudo da Página atual");
}
}
Para aplicar o princípio nesse exemplo deveria ficar assim:
class Livro {
function getTitulo() {
return "Mundo Sem Fim";
}
function getAutor() {
return "John Doe";
}
function virarPagina() {
// pointer to next page
}
function getPaginaAtual() {
return "contéudo pagina atual";
}
}
interface Impressao {
function imprimirPagina(pagina);
}
class ImpressaoConsole implements Impressao {
function imprimirPagina(pagina){
console.log(pagina);
}
}
class ImpressaoHtml implements Printer {
function imprimirPagina(pagina){
<div style="single-page"> {pagina} </div>;
}
}
Desse modo, a responsabilidade de impressão fica exclusiva em uma interface e pode-se estender o sistema criando novas implementações dessa interface.
Open/Closed
Esse princípio declara que classes devem ser fechadas para alteração e abertas para extensão, ou seja, não deve-se alterar classes para adicionar novas implementações. É claro que, para correção de bugs, a alteração de classes é permitida, mas para adicionar novas propriedades ou novos métodos não.
Como exemplo veja essa classe:
class Livro {
function abrirLivro(){
// abrindo livro
}
function realizarAnotacoes(){
// realizando anotações
}
}
Para um livro em papel seria normal, mas imagine se o sistema deve começar a considerar também livro digital:
class Livro {
function abrirLivro(){
if(livroEmPapel)
// abrindo livro em papel
else if(livroDigital)
// abrindo livro digital
}
function realizarAnotacoes(){
if(livroEmPapel)
// realizando anotações em livro em papel
else if(livroDigital)
// realizando anotações em livro digital
}
}
A chance de esse if quebrar a aplicação é grande.
Para isso, pode-se utilizar uma interface e extende-la para as próximas demandas:
interface Livro {
function abrirLivro(){
// abrindo livro
}
function realizarAnotacoes(){
// realizando anotações
}
}
class LivroEmPapel extends Livro {
function abrirLivro(){
// abrindo livro
}
function realizarAnotacoes(){
// realizando anotações
}
}
class LivroDigital extends Livro {
function abrirLivro(){
// abrindo livro
}
function realizarAnotacoes(){
// realizando anotações
}
}
Desse modo, a cada nova implementação não há o risco de quebrar a aplicação que já estava funcionando.
Liskov Substitution
Esse princípio foi introduzido por Barbara Liskov e diz que havendo uma classe filha B derivada de uma outra classe pai A, deveria ser possível trocar a classe B pela classe A sem prejuízos a aplicação.
Um exemplo de um problema que quebra esse princípio seria o seguinte:
interface Funcionario() {
private string Nome;
private string Cargo;
function remunera() {
}
}
class ContratoClt extends Funcionario {
function remunera() {
//remunera como CLT
}
}
class ContratoPJ extends Funcionario {
function remunera() {
//remunera como PJ
}
}
class ContratoEstagio extends Funcionario {
function remunera() {
//remunera como Estagiario
}
}
class Voluntario extends Funcionario {
function remunera() {
//não remunera???
}
}
No exemplo anterior, a classe Voluntário não implementa a função remunera, o que pode ser um problema de compatibilidade com sua interface. Para resolver esse problema pode-se utilizar o próximo princípio, a Segregação de Interfaces, tornando interfaces menores e mais específicas para serem estendidas.
Interface Segregation
O princípio de segregação de Interface serve para definirmos várias interfaces menores para que as classes que implementam uma das interfaces só tenham que se preocupar com métodos que realmente as interessam.
Voltando ao exemplo anterior:
interface FuncionarioPago() {
private string Nome;
private string Cargo;
function remunera() {
}
}
interface FuncionarioNaoPago() {
private string Nome;
private string Cargo;
}
class ContratoClt extends Funcionario {
function remunera() {
//remunera como CLT
}
}
class ContratoPJ extends Funcionario {
function remunera() {
//remunera como PJ
}
}
class ContratoEstagio extends Funcionario {
function remunera() {
//remunera como Estagiario
}
}
class Voluntario extends FuncionarioNaoPago {
}
Dependency Inversion
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. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
Na prática, o exemplo abaixo mostra o problema:
public class WindowsMachine {
private final Keyboard keyboard;
private final Monitor monitor;
public WindowsMachine() {
monitor = new Monitor();
keyboard = new Keyboard();
}
}
Com a implementação, criando um novo Monitor e um novo teclado estamos deixando o acoplamento entre essas classes alto. Uma mudança em uma delas pode quebrar o sistema.
Para resolver isso, podemos fazer da seguinte maneira, utilizando Injeção de Dependência:
public class WindowsMachine {
private final Keyboard keyboard;
private final Monitor monitor;
public WindowsMachine(Monitor monitor, Keyboard keyboard)
{
this.monitor = monitor;
this.keyboard = keyboard;
}
}
Dessa forma desacoplamos o Keyboard e o Monitor do WindowsMachine, criando uma abstração onde qualquer monitor ou teclado passado por parâmetro possa ser acionado. Assim, não é necessário se preocupar em como criar esses dispositivos nessa classe. Preocupa-se apenas em utilizar o que foi recebido.