TIL - Principio di apertura-chiusura


Oggi ho letto un paper sul principio di apertura-chiusura chiamato anche OCP1.

OCP fa parte di una serie di principi che insieme formano il S.O.L.I.D.:

  • S -> SRP (Single Responsability Principle)
  • O -> OCP (Open Closed Principle)
  • L -> LSP (Liskov substitution Principle)
  • I -> ISP (Interface Segregation Principle)
  • D -> DIP (Dependency Inversion Principle)

L’OCP ha una sola e semplicissima regola:

Entità di un software (classi, moduli, funzioni, etc.) dovrebbero essere aperti all’estensione ma chiusi alla modifica.

TLDR: un software ben disegnato non dovrebbe mai essere modificato (immutabile) e dovrebbe essere facile da estendere.

Moduli conformi all’OCP rispettano queste due regole:

Aperti all’estensione

Questo significa che la funzionalità del modulo può essere estesa; questo ci consente di adattare il software ai nuovi bisogni che con il tempo emergono.

Chiusi alla modifica

Il codice sorgente del modulo è inviolato. Nessuno ha il permesso di cambiarlo.

Ma in pratica?

Facciamo un esempio pratico, creiamo una classe Rectangle in cui definiamo i soliti getter e setter per l’altezza e la larghezza:

public class Rectangle {

    private double width;
    private double height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }
}

Il nostro cliente ci chiede di creare un’applicazione che ci permetta di calcolare l’area totale di un insieme di rettangoli, detto-fatto:

public class AreaCalculator {

    public double getTotalAreaFromRectangles(Rectangle[] rectangles) {
        double totalArea = 0;
        for (Rectangle rectangle : rectangles) {
            totalArea += rectangle.getHeight() * rectangle.getWidth();
        }
        return totalArea;
    }
}

Perfetto, il nostro compito è fini- ah, il cliente però ha appena riferito di aver esteso il proprio core-business sui cerchi e vorrebbe che l’applicazione in questione riesca a calcolare non solo il totale dell’area di rettangoli ma anche di cerchi.

La prima soluzione che viene in mente è quello di modificare il metodo getTotalAreaFromRectangles per adattarlo alle nuove esigenze creando più o meno qualcosa del genere:

Creiamo la nostra bella classe Circle

public class Circle {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}

e modifichiamo getTotalAreaFromRectangles in getTotalAreaFromShapes:

public class AreaCalculator {

    public double getTotalAreaFromShapes(Object[] shapes) {
        double totalArea = 0;
        for (Object shape : shapes) {
            if(shape instanceof Rectangle) {
                Rectangle rectangle = (Rectangle) shape;
                totalArea += rectangle.getWidth() * rectangle.getHeight();
            } else {
                Circle circle = (Circle) shape;
                totalArea += circle.getRadius() * circle.getRadius() * Math.PI;
            }

        }
        return totalArea;
    }
}

Kronk1

Bene, il tutto funziona e il cliente è felice, no? WRONG!

Una settimana dopo il cliente ci comunica che anche il settore dei triangoli potrebbe essere un gran bell’investimento … e così via. Come si può ben notare questo ci costringe a modificare il metodo getTotalAreaFromShapes per estenderlo a nuovi tipi, infrangendo il principio di apertura-chiusura.

Okay ma come risolvo?

Una possibile soluzione è quella di usare l’astrazione: creando una interfaccia base generica per tutti i tipi su cui dovremmo calcolare l’area:

public interface Shape {
    double getArea();
}

Dopo di ciò ci basterà implementare l’interfaccia Shape ed implementare il metodo getArea:

public class Rectangle implements Shape {

    private double width;
    private double height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    @Override
    public double getArea() {
        return this.getWidth() * this.getHeight();
    }
}

e

public class Circle implements Shape {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return getRadius() * getRadius() * Math.PI;
    }
}

Notiamo subito che spostiamo la responsabilità di calcolare l’area all’interno della classe stessa rendendo il metodo getTotalAreaFromShapes semplice, robusto e resiliente al cambiamento di tipo:

public class AreaCalculator {

    public double getTotalAreaFromShapes(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
           totalArea += shape.getArea();
        }
        return totalArea;
    }
}

Così facendo la nostra applicazionne è aperta all’estensione ma chiusa al cambiamento, come esplicitato dall’OCP.

kronk

Questo è uno dei principi cardine del S.O.L.I.D. ma anche uno dei più difficili da non violare: se avete violato uno dei principi precedentemente trattati avete sicuramente violato anche questo.


  1. OCP