Um guia sólido para os princípios do SOLID
1. Introdução
Neste tutorial, discutiremosthe SOLID principles of Object-Oriented Design.
Primeiro, começaremos porexploring the reasons they came about and why we should consider them ao projetar o software. Em seguida, descreveremos cada princípio ao lado de algum código de exemplo para enfatizar o ponto.
2. A Razão para Princípios SÓLIDOS
Os princípios do SOLID foram primeiramente conceituados por Robert C. Martin em seu artigo de 2000,Design Principles and Design Patterns. Esses conceitos foram posteriormente construídos por Michael Feathers, que nos apresentou a sigla SOLID. E nos últimos 20 anos, esses 5 princípios revolucionaram o mundo da programação orientada a objetos, mudando a maneira como escrevemos software.
Então, o que é o SOLID e como ele nos ajuda a escrever um código melhor? Simplificando, Martin's e Feathers 'design principles encourage us to create more maintainable, understandable, and flexible software. Conseqüentemente,as our applications grow in size, we can reduce their complexitye nos poupará de muitas dores de cabeça mais adiante!
Os 5 conceitos a seguir compõem nossos princípios do SOLID:
-
Súnica responsabilidade
-
Open/Closed
-
Substituição deLiskov
-
Iegregação da face
-
DInversão de dependência
Embora algumas dessas palavras possam parecer assustadoras, elas podem ser facilmente entendidas com alguns exemplos simples de código. Nas seções a seguir, vamos mergulhar profundamente no que cada um desses princípios significa, junto com um exemplo rápido de Java para ilustrar cada um.
3. Responsabilidade Única
Vamos começar com o princípio da responsabilidade única. Como podemos esperar, este princípio afirma quea class should only have one responsibility. Furthermore, it should only have one reason to change.
How does this principle help us to build better software? Vamos ver alguns de seus benefícios:
-
Testing - Uma classe com uma responsabilidade terá muito menos casos de teste
-
Lower coupling - menos funcionalidade em uma única classe terá menos dependências
-
Organization - classes menores e bem organizadas são mais fáceis de pesquisar do que as monolíticas
Tome, por exemplo, uma classe para representar um livro simples:
public class Book {
private String name;
private String author;
private String text;
//constructor, getters and setters
}
Nesse código, armazenamos o nome, o autor e o texto associados a uma instância deBook.
Agora vamos adicionar alguns métodos para consultar o texto:
public class Book {
private String name;
private String author;
private String text;
//constructor, getters and setters
// methods that directly relate to the book properties
public String replaceWordInText(String word){
return text.replaceAll(word, text);
}
public boolean isWordInText(String word){
return text.contains(word);
}
}
Agora, nossa classeBook funciona bem e podemos armazenar quantos livros quisermos em nosso aplicativo. Mas, de que adianta armazenar as informações se não podemos enviar o texto para nosso console e lê-lo?
Vamos jogar a cautela ao vento e adicionar um método de impressão:
public class Book {
//...
void printTextToConsole(){
// our code for formatting and printing the text
}
}
Esse código, no entanto, viola o princípio de responsabilidade única descrito anteriormente. Para consertar nossa bagunça, devemos implementar uma classe separada que se preocupe apenas com a impressão de nossos textos:
public class BookPrinter {
// methods for outputting text
void printTextToConsole(String text){
//our code for formatting and printing the text
}
void printTextToAnotherMedium(String text){
// code for writing to any other location..
}
}
Impressionante. Não só desenvolvemos uma classe que alivia oBook de suas tarefas de impressão, mas também podemos aproveitar nossoBookPrinter class para enviar nosso texto para outra mídia.
Seja por e-mail, registro ou qualquer outra coisa, temos uma classe separada dedicada a este assunto.
4. Aberto para extensão, fechado para modificação
Agora, é hora do 'O' - mais formalmente conhecido comoopen-closed principle. Simplificando,classes should be open for extension, but closed for modification.In doing so, westop ourselves from modifying existing code and causing potential new bugs em um aplicativo que de outra forma seria feliz.
Claro, oone exception to the rule is when fixing bugs in existing code.
Vamos explorar mais o conceito com um exemplo de código rápido. Como parte de um novo projeto, imagine que implementamos um sclassGuitar .
É totalmente desenvolvido e ainda tem um botão de volume:
public class Guitar {
private String make;
private String model;
private int volume;
//Constructors, getters & setters
}
Iniciamos o aplicativo e todo mundo adora. No entanto, após alguns meses, decidimos queGuitar é um pouco chato e poderia ser usado com um padrão de chama incrível para torná-lo um pouco mais 'rock and roll'.
Neste ponto, pode ser tentador apenas abrir o sclassGuitar e adicionar um padrão de chama - mas quem sabe quais erros podem ocorrer em nosso aplicativo.
Em vez disso, vamosstick to the open-closed principle and simply extend our Guitar class:
public class SuperCoolGuitarWithFlames extends Guitar {
private String flameColor;
//constructor, getters + setters
}
Ao estender o sclassGuitar , podemos ter certeza de que nosso aplicativo existente não será afetado.
5. Substituição Liskov
O próximo na lista é a substituição de Liskov, que é sem dúvida o mais complexo dos 5 princípios. Simplificando,if class A is a subtype of class B, then we should be able to replace B with A without disrupting the behavior of our program.
Vamos direto ao código para nos ajudar a entender esse conceito:
public interface Car {
void turnOnEngine();
void accelerate();
}
Acima, definimos uma sinterfaceCar imples com alguns métodos que todos os carros devem ser capazes de cumprir - ligar o motor e acelerar para frente.
Vamos implementar nossa interface e fornecer alguns códigos para os métodos:
public class MotorCar implements Car {
private Engine engine;
//Constructors, getters + setters
public void turnOnEngine() {
//turn on the engine!
engine.on();
}
public void accelerate() {
//move forward!
engine.powerOn(1000);
}
}
Como nosso código descreve, temos um mecanismo que podemos ligar e podemos aumentar a potência. Mas espere, é 2019, e Elon Musk tem sido um homem ocupado.
Agora estamos vivendo na era dos carros elétricos:
public class ElectricCar implements Car {
public void turnOnEngine() {
throw new AssertionError("I don't have an engine!");
}
public void accelerate() {
//this acceleration is crazy!
}
}
Ao jogar um carro sem motor na mistura, estamos inerentemente mudando o comportamento do nosso programa. Isso éa blatant violation of Liskov substitution and is a bit harder to fix than our previous 2 principles.
Uma solução possível seria retrabalhar nosso modelo em interfaces que levem em consideração o estado sem mecanismo de nossoCar.
6. Segregação de Interface
O ‘I’ em SOLID significa separação de interface e significa simplesmente quelarger interfaces should be split into smaller ones. By doing so, we can ensure that implementing classes only need to be concerned about the methods that are of interest to them.
Para este exemplo, vamos tentar nossas mãos como zookeepers. E, mais especificamente, trabalharemos no recinto do urso.
Vamos começar com uma interface que descreve nossas funções como tratadores de ursos:
public interface BearKeeper {
void washTheBear();
void feedTheBear();
void petTheBear();
}
Como zeladores ávidos, estamos mais do que felizes em lavar e alimentar nossos amados ursos. No entanto, estamos todos muito cientes dos perigos de acariciá-los. Infelizmente, nossa interface é bastante grande e não temos escolha a não ser implementar o código para acariciar o urso.
Vamosfix this by splitting our large interface into 3 separate ones:
public interface BearCleaner {
void washTheBear();
}
public interface BearFeeder {
void feedTheBear();
}
public interface BearPetter {
void petTheBear();
}
Agora, graças à segregação de interface, estamos livres para implementar apenas os métodos que importam para nós:
public class BearCarer implements BearCleaner, BearFeeder {
public void washTheBear() {
//I think we missed a spot...
}
public void feedTheBear() {
//Tuna Tuesdays...
}
}
E, finalmente, podemos deixar as coisas perigosas para as pessoas loucas:
public class CrazyPerson implements BearPetter {
public void petTheBear() {
//Good luck with that!
}
}
Indo além, poderíamos até mesmo dividir nossoBookPrinter class de nosso exemplo anterior para usar a segregação de interface da mesma maneira. Implementando uma interfacePrinter com um único métodoprint , poderíamos instanciarConsoleBookPrinter andOtherMediaBookPrinter classes separados.
7. Inversão de Dependência
O princípio de Inversão de Dependências refere-se ao desacoplamento de módulos de software. Dessa forma, em vez de módulos de alto nível dependendo de módulos de baixo nível, ambos dependerão de abstrações.
Para demonstrar isso, vamos voltar à velha escola e dar vida a um computador Windows 98 com código:
public class Windows98Machine {}
Mas de que serve um computador sem monitor e teclado? Vamos adicionar um de cada ao nosso construtor para que cada instanciação deWindows98Computer we venha pré-embalada com umaMonitor areia aStandardKeyboard:
public class Windows98Machine {
private final StandardKeyboard keyboard;
private final Monitor monitor;
public Windows98Machine() {
monitor = new Monitor();
keyboard = new StandardKeyboard();
}
}
Este código funcionará, e poderemos usarStandardKeyboardeMonitor livremente em nossa classeWindows98Computer . Problema resolvido? Nem tanto. By declaring the StandardKeyboard and Monitor with the new keyword, we’ve tightly coupled these 3 classes together.
Isso não apenas torna nosso fragmentoWindows98Computer para testar, mas também perdemos a capacidade de trocar nossoStandardKeyboard class por um diferente, caso seja necessário. E estamos presos com nossa classeMonitor também.
Vamos separar nossa máquina deStandardKeyboard adicionando uma interfaceKeyboard mais geral e usando isso em nossa aula:
public interface Keyboard { }
public class Windows98Machine{
private final Keyboard keyboard;
private final Monitor monitor;
public Windows98Machine(Keyboard keyboard, Monitor monitor) {
this.keyboard = keyboard;
this.monitor = monitor;
}
}
Aqui, estamos usando o padrão de injeção de dependência para facilitar a adição da dependênciaKeyboard na classeWindows98Machine.
Vamos também modificar nossa classeStandardKeyboard para implementar a interfaceKeyboard para que seja adequada para injetar na classeWindows98Machine:
public class StandardKeyboard implements Keyboard { }
Agora nossas classes estão desacopladas e se comunicam por meio da abstraçãoKeyboard. Se quisermos, podemos mudar facilmente o tipo de teclado em nossa máquina com uma implementação diferente da interface. Podemos seguir o mesmo princípio para a classeMonitor.
Excelente! Nós separamos as dependências e somos livres para testar nossoWindows98Machine com qualquer estrutura de teste que escolhermos.
8. Conclusão
Neste tutorial, pegamos umdeep dive into the SOLID principles of object-oriented design.
Nósstarted with a quick bit of SOLID history and the reasons these principles exist.
Letra por letra, temosbroken down the meaning of each principle with a quick code example that violates it. We then saw how to fix our code e fazemos com que ele adira aos princípios SOLID.
Como sempre, o código está disponível emGitHub.