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. Adherir a esses princípios ajudará você a desenvolver software robusto. Eles tornarão 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 de 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 isso 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, componentes devem ser abertos para extensão, mas fechados para modificação. Para entender esse princípio, vamos pegar um exemplo de uma classe que calcula a área de um formato.
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 houver um novo exemplo do tipo Formato
para o qual você precisar calcular a área no futuro, você terá que modificar a classe acima adicionando outro bloco condicional else-if
. Você acabará fazendo isso para cada novo objeto do tipo Formato
.
Para contornar isso, você pode criar um interface e ter cada Formato
implementá-lo. Em seguida, cada classe pode fornecer sua própria implementação para calcular a área. Isso fará com que o seu programa seja 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 da Substituição de Liskov
O princípio da substituição de Liskov afirma que você deve ser capaz de substituir um objeto de superclasse por um objeto de subclasse sem afetar a correção 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 falso }
}
Neste exemplo acima, as classes Eagle
e Ostrich
herdam ambas da classe Bird
e sobrescrevem o método fly()
. No entanto, a classe Ostrich
é forçada a fornecer uma implementação de fantoche porque ela não pode voar, e portanto, ela 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 }
}
Princípio da Segmentação de Interface
De acordo com o princípio da segmentação de interface, você deve construir interfaces pequenas e focadas que não obrigam o cliente a implementar comportamentos que eles não precisam.
Um exemplo direto seria ter uma interface que calcula tanto o area e 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 de fantoche }
}
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.
No outro lado, um Cube
pode implementar ambos. Para superar isso, podemos segregar a interface e ter duas interfaces separadas: uma para calcular a área e outra para calcular o volume. Isso permitirá que os formatos 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 de Inversão de Dependência
No princípio de inversão de dependência, 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 a falta de dependência
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. Isto viola o princípio de 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 email
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
No exemplo acima, nós garantimos a falta de dependência. Employee
não depende de nenhuma implementação concreta, em vez disso, depende apenas da abstração (interface de notificação).
Se precisarmos mudar o modo de notificação, podemos criar uma nova implementação e passá-la para o Employee
.
Conclusão
Em conclusão, nós abordamos a essência dos princípios SOLID através de exemplos simples neste artigo.
Esses princípios formam os blocos de construção para desenvolver aplicações que são altamente extensíveis e reutilizáveis.
Vamos nos conectar no LinkedIn
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/