Jak styl powinien mieć Czysty Kod?

Czysty Kod nie zawsze jest obiektowy. Czasem będzie napisany w stylu proceduralnym. A jaki styl jest lepszy : proceduralny czy obiektowy? Taki który w danych warunkach ułatwia nam jego rozwój i czytelność – zgodnie z zasadami Czystego Kodu.

Poniżej przykład kodu proceduralnego który posłuży mi do rozważań o czystości kodu jak i jego refaktoryzacji do kodu obiektowego.

public class Rectangle {
    double width;
    double height;
}
...
public class Geometry {
    double area(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.radius * circle.radius
        } else if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.width * rectangle.height;
        } else if (shape instanceof Square) {
            Square square = (Square) shape;
            return square.size * square.size;
        }

        throw new IllegalArgumentException("Unknown shape");
    }
}

Wyboru stylu w jakim będzie dalej pisany kod dokonuję na podstawie obserwacji kierunku zmian, które wynikają z pojawiających się nowych wymagań biznesowych.

Jakie zmiany umożliwia kod proceduralny?

Jeżeli głównie będę dodawał nowe funkcje operujące na już istniejących strukturach danych, to kod proceduralny (nowe procedury) pozostanie prawdopodobnie nadal czytelny. Przykładem jest nowa funkcja zwracająca najmniejszy prostokąt zawierający figurę.

public class Geometry {
    Rectange containingRectange(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            Rectangle rectangle = new Rectangle();
            rectangle.width = 2 * circle.radius;
            rectangle.height= 2 * circle.radius;
            return rectangle;
        } else if (shape instanceof Rectangle) {
            return (Rectangle) shape;
        } else if (shape instanceof Square) {
            ...
        }

        throw new IllegalArgumentException("Unknown shape");
    }
}

Kiedy kod proceduralny stanie się nieczytelny?

Ale jeżeli planuje dodać lub zmodyfikować istniejące struktury danych to wymusi to zmiany we wszystkich istniejących procedurach. Co się stanie kiedy zdecyduję się na zmianę komponentów w strukturze danych Rectangle na punkty opisujące 2 przeciwległe rogi kwadratu?

public class Point {
    double x,y; 
} 

public class Rectangle {
     Point topLeft;
     Point bottomRight; 
}

Nie trudno zauważyć, że taka zmiana wymusi wiele zmian w istniejących procedurach. Sposobem na uniknięcie wielu zmian (lub ich minimalizacje) jest umieszczenie z strukturze Rectangle metod getX() oraz getY() które dokonają niezbędnych wyliczeń.

public class Rectangle {
    private Point topLeft;
    private Point bottomRight;

    double getX(){
        return Math.abs(topLeft.x = bottomRight.x);
    }

    double getY(){
        return Math.abs(topLeft.y = bottomRight.y);
    }
}

Ale zauważ, że od tego momentu zaczynam ukrywać szczegóły struktury danych. Szczegóły w klasie Rectangle zostały ukryte a nowe metody wyliczają potrzebne dane. W ten sposób zaczynam zamieniać styl kodu z proceduralnego na obiektowy.

Jak refaktoryzować kod proceduralny na obiektowy?

Dokonaj samo-enkapsulacji danych w strukturach

Na początku dodaję konstuktory i dokonuję enkapsulacji pół we wszystkich strukturach danych. W moim przypadku dane w strukturach nie są zmieniane, więc pola mogą być typu final.

public class Circle {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

Zdefiniuj wspólny interfejs / klasę bazową dla obecnych struktur

Następnie definiuję pustą klasę bazową Shape którą będą rozszerzać wszystkie struktury danych. Procedura „area” zamiast obiektu „Object” akceptuje od tej pory jako parameter tylko rozszerzenia klasy abstrakcyjnej „Shape”. Może to być także alternatywnie wspólny interfejs.

public abstract class Shape{
}

public class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

...

Przenieś logikę z procedury do klasy bazowej

W celu przeniesienia logiki do klasy bazowej wykonam niewielką modyfikację, aby następnie móc skorzystać z przeniesienia metody w narzędziu IntelliJ.

public class Geometry {
    static double area(Shape shape) {
        return new Geometry().calculateArea(shape);
    }

    private double calculateArea(Shape shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.getRadius() * circle.getRadius();
        } else if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.getWidth() * rectangle.getHeight();
        } else if (shape instanceof Square) {
            Square square = (Square) shape;
            return square.getSize() * square.getSize();
        }

        throw new IllegalArgumentException("Unknown shape :" + shape.getClass());
    }
}

Powyższy kod uzyskałem poprzez ekstrakcję nowej metody, następnie usunięcie słowa static i dodanie wywołania kontruktora.

Następnie przenoszę metodę zawierającą logikę „calculateArea” z „Geometry” do klasy bazowej „Shape”.

public class Geometry {
    static double area(Shape shape) {
        return shape.calculateArea();
    }
}

public abstract class Shape {
    double calculateArea() {
        if (this instanceof Circle) {
            Circle circle = (Circle) this;
            return Math.PI * circle.getRadius() * circle.getRadius();
        } else if (this instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) this;
            return rectangle.getWidth() * rectangle.getHeight();
        } else if (this instanceof Square) {
            Square square = (Square) this;
            return square.getSize() * square.getSize();
        }

        throw new IllegalArgumentException("Unknown shape :" + getClass());
    }
}

Po tym przekszałceniu pojawił się zapach kodu : „klasy bazowe zależne od swoich klas pochodnych”. Rozwiązanie problemu poprowadzi nas do kolejnego przekształcenia.

Wykonaj przesunięcie metody w dół do klas pochodnych

Przekształcenie jest w pełni zautomatyzowane w wielu środowiskach jak IntelliJ, Eclipse, NetBeans.

Usuń niepotrzebną logikę w klasach pochodnych

Ostatecznie kończymy przekaształceniem „zastąp wyrażenia warunkowe polimorfizmem”. W każdej z podklas (czyli naszych dawnych struktur anych) tyko jeden warunek będzie prawdziwy.

Poniżej wynik końcowy naszej refaktoryzacji

public class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double calculateArea() {
        return Math.PI * circle.radius * circle.radius;
    }
}

public class Geometry {
    static double area(Shape shape) {
        return shape.calculateArea();
    }
}

Dodatkowo możemy jeszcze wykonać „wchłonięcie funkcji „Geometry.area” a następnie zmienić nazwę metody „calculateArea” na „area” aby powrócić od dawnego nazewnictwa.

Polecam także artykuł o refaktoryzacji do wzorca Interpreter. Poniżej podziel się wrażeniami z artykułu.

Spread the word. Share this post!

Clean Code & Refactoring Newsletter

Webinars, Articles & Online Courses / Onsite Workshops

Send me your newsletter (you can unsubscribe at any time).

I do accept terms and privacy policy