Neste artigo, você vai aprender sobre os princípios SOLID. Você vai ganhar entendimento de cada princípio juntamente com exemplos de código em Java.
Os princípios SOLID são um conjunto de cinco princípios de projeto usados em programação orientada a objetos. Conforme você adere a esses princípios, isso ajudará a desenvolver software robusto. Isso tornará seu código mais eficiente, legível e manutenvel.
SOLID é um acrônimo que representa:
- Princípio da Responsabilidade Única
- Princípio Aberto/Fechado
- Princípio da Substituição de Liskov
- Princípio da Segmentação de Interface
- Princípio de Inversão de Dependência
Princípio da Responsabilidade Única
O princípio da responsabilidade única afirma que todas as classes devem ter uma única responsabilidade, um único motivo para mudança.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
No exemplo acima, a classe Employee
tem algumas behaviors específicas da classe de funcionário, como getDesignation
e updateSalary
.
Adicionalmente, ela também tem outro método chamado sendMail
, que se desvia da responsabilidade da classe Employee
.
Este comportamento não é específico para esta classe e ter ele viola o princípio da responsabilidade única. Para superar isso, você pode mover o método sendMail
para uma classe separada.
Veja como:
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){
// }
}
public class NotificationService {
public void sendMail() {
// }
}
Princípio Aberto/Fechado
De acordo com o princípio aberto/fechado, os componentes devem ser abertos para extensão, mas fechados para modificação. Para entender este princípio, vamos tomar o exemplo de uma classe que calcula a área de uma forma.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// calcular a área do Quadrado
} else if(shape instanceof Circle){
// calcular a área do Círculo
}
return areaOfShape;
}
O problema com o exemplo acima é que, se no futuro houver uma nova instância de tipo Shape
para a qual você precise calcular a área, você terá que modificar a classe acima, adicionando outro bloco condicional else-if
. Você vai acabar fazendo isso para cada novo objeto do tipo Shape
.
Para superar isso, você pode criar uma interface e fazer com que cada Shape
implemente essa interface. Então, cada classe pode fornecer sua própria implementação para o cálculo da área. Isso tornará seu programa facilmente extensível no futuro.
interface IAreaCalculator(){
double area();
}
class Square implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Square");
return 0.0;
}
}
class Circle implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Circle");
return 0.0;
}
}
Princípio de Substituição de Liskov
O princípio de substituição de Liskov afirma que você deve ser capaz de substituir um objeto de classe superior por um objeto de classe derivada sem afetar a corretude do programa.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() {
// alguma implementação }
}
class Ostrich extends Bird {
@Override
public void fly() {
// implementação fictícia }
}
No exemplo acima, as classes Eagle
e Ostrich
herdam ambas da classe Bird
e sobrescrevam o método fly()
. Entretanto, a classe Ostrich
é obrigada a fornecer uma implementação falso porque não consegue voar, e portanto, não se comporta da mesma forma se nós substituirmos o objeto da classe Bird
por ele.
Isso viola o princípio da substituição de Liskov. Para corrigir isso, podemos criar uma classe separada para aves que voam e ter a classe Eagle
a ela herdar, enquanto outras aves podem herdar de outra classe, que não incluirá nenhuma funcionalidade de fly
.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // algumas implementações }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // algumas implementações }
}
Interface Segregation Principle
De acordo com o princípio da segregação de interfaces, você deve construir interfaces pequenas e focadas que não obrigam o cliente a implementar comportamentos que eles não precisam.
Um exemplo simples seria ter uma interface que calcula tanto a área quanto o volume de um objeto geométrico.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // calcular a área }
double calculateVolume(){ // implementação falso }
}
O problema com isso é que se um objeto Square
implementar isto, então ele é forçado a implementar o método calculateVolume()
, o que ele não precisa.
Por outro lado, um Cubo
pode implementar ambos. Para superar isso, podemos separar a interface e ter duas interfaces separadas: uma para calcular a área e outra para calcular o volume. Isso permitirá que as formas individuais decidam o que implementar.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // calcular a área }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // calcular a área }
@Override
public double calculateVolume() {// calcular o volume }
}
Princípio da Inversão de Dependência
No princípio da inversão de dependência, os módulos de alto nível não devem depender de módulos de baixo nível. Em outras palavras, você deve seguir a abstração e garantir o baixo acoplamento
public interface Notification {
void notify();
}
public class EmailNotification implements Notification {
public void notify() {
System.out.println("Sending notification via email");
}
}
public class Employee {
private EmailNotification emailNotification;
public Employee(EmailNotification emailNotification) {
this.emailNotification = emailNotification;
}
public void notifyUser() {
emailNotification.notify();
}
}
No exemplo dado, a classe Employee
depende diretamente da classe EmailNotification
, que é um módulo de baixo nível. Isso viola o princípio da inversão de dependência.
public interface Notification{
public void notify();
}
public class Employee{
private Notification notification;
public Employee(Notification notification){
this.notification = notification;
}
public void notifyUser(){
notification.notify();
}
}
public class EmailNotification implements Notification{
public void notify(){
// implementar notificação via e-mail
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
No exemplo acima, garantimos o baixo acoplamento. Employee
não depende de nenhuma implementação concreta, depende apenas da abstração (interface de notificação).
Se precisarmos alterar o modo de notificação, podemos criar uma nova implementação e passá-la para o Employee
.
Conclusão
Em conclusão, abordamos a essência dos princípios SOLID por meio de exemplos diretos neste artigo.
Esses princípios formam os blocos de construção para o desenvolvimento de aplicativos altamente extensíveis e reutilizáveis.
Vamos conectar-nos em LinkedIn
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/