In questo articolo, imparerai sugli principi SOLID. Otterrai una comprensione di ciascuno degli principi insieme agli esempi di codice Java.
Gli principi SOLID sono una serie di cinque principi di progettazione utilizzati in programmazione orientata agli oggetti. Adeguarsi a questi principi aiuterà a sviluppare software robusto. renderanno il tuo codice più efficiente, leggibile e facile da manutenere.
SOLID è un acronimo che sta per:
- Principio della Single Responsibility
- Principio Open/Closed
- Principio di Liskov
- Principio dell’Interfaccia Segregata
- Principio dell’Inversione dipendenza
Principio della Single Responsibility
Il principio della Single Responsibility stabilisce che ogni classe deve avere una sola responsabilità, una sola ragione per il cambiamento.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
Nell’esempio soprastante, la classe Employee
ha alcuni comportamenti specifici della classe impiegato come getDesignation
e updateSalary
.
Inoltre, ha anche un altro metodo chiamato sendMail
che si discosta dalla responsabilità della classe Employee
.
Questo comportamento non è specifico di questa classe, e averlo viola il principio della Single Responsibility. Per superare questo, puoi spostare il metodo sendMail
in una classe separata.
Ecco come fare:
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
}
public class NotificationService {
public void sendMail() { // }
}
Principio Aperto/Chiuso
Secondo il principio aperto/chiuso, i componenti devono essere aperti all’estensione, ma chiusi alla modifica. Per capire questo principio, prendiamo l’esempio di una classe che calcola l’area di una forma.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// calcola l'area del Quadrato
} else if(shape instanceof Circle){
// calcola l'area del Cerchio
}
return areaOfShape;
}
Il problema con questo esempio è che se in futuro dovesse esserci una nuova istanza di tipo Shape
per cui è necessario calcolare l’area, dovresti modificare la classe aggiungendo un altro blocco condizionale else-if
. Lo fareste per ogni nuovo oggetto di tipo Shape
.
Per superare questo problema, puoi creare un’interfaccia e far sì che ogni Shape
implementi questa interfaccia. Quindi, ogni classe può fornire la propria implementazione per il calcolo dell’area. Questo renderà il programma estensibile in 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;
}
}
Principio di Sostituzione di Liskov
Il principio di sostituzione di Liskov afferma che devi essere in grado di sostituire un oggetto di una classe superiore con un oggetto di una sottoclasse senza influenzare la correttezza del programma.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() { // qualche implementazione }
}
class Ostrich extends Bird {
@Override
public void fly() { // implementazione fittizia }
}
Nell’esempio precedente, la classe Eagle
e la classe Ostrich
estendono entrambe la classe Bird
e sovrascrivono il metodo fly()
. Tuttavia, la classe Ostrich
è costretta a fornire una realizzazione finta perché non può volare, e quindi non si comporta allo stesso modo se sostituiamo l’oggetto della classe Bird
con essa.
Questo viola il principio di sostituzione di Liskov. Per risolvere questo problema, possiamo creare una classe separata per gli uccelli capaci di volare e far estendere la Eagle
ad essa, mentre gli altri uccelli possono estendere una classe diversa, che non include alcun comportamento di volo.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // alcune implementazioni }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // alcune implementazioni }
}
Principio dell’isolamento delle interfacce
Secondo il principio dell’isolamento delle interfacce, dovreste costruire interfacce piccole e focalizzate che non costringono il client a implementare comportamenti che non ha bisogno.
Un esempio diretto potrebbe essere avere un’interfaccia che calcola sia l’area che il volume di una forma.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // calcola l'area }
double calculateVolume(){ // implementazione finta }
}
Il problema con questo è che se una forma Square
implementa questo, allora è costretta a implementare il metodo calculateVolume()
, che non ha bisogno.
Dall’altro lato, un Cube
può implementare entrambi. Per superare questo, possiamo segregare l’interfaccia e avere due interfacce separate: una per calcolare l’area e un’altra per calcolare il volume. Questo consentirà alle singole forme di decidere cosa implementare.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // calcola l'area }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // calcola l'area }
@Override
public double calculateVolume() {// calcola il volume }
}
Principio dell’inversione delle dipendenze
Il principio dell’inversione delle dipendenze stabilisce che i moduli a alto livello non devono dipendere da quelli a basso livello. In altre parole, dovete seguire l’astrazione e garantire una scarsa dipendenza.
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();
}
}
Nell’esempio fornito, la classe Employee
dipende direttamente dalla classe EmailNotification
, che è un modulo a basso livello. Questo viola il principio dell’inversione delle dipendenze.
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(){
// implementa la notifica via email
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
Nell’esempio precedente, abbiamo garantito una scarsa dipendenza. Employee
non dipende da alcuna implementazione concreta, ma dipende solo dall’astrazione (interfaccia di notifica).
Se dobbiamo cambiare il metodo di notifica, possiamo creare una nuova implementazione e passarla alla classe Employee
.
Conclusione
In conclusione, in questo articolo abbiamo spiegato l’essenza dei principi SOLID attraverso esempi semplici.
Questi principi costituiscono i blocchi di costruzione per lo sviluppo di applicazioni altamente estensibili e riutilizzabili.
Conniamoci su LinkedIn.
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/