Monday, July 20, 2020

How to change procedural code into object-oriented one?

What style should Clean Code be written in?


Clean Code is not always object-oriented. Sometimes it will be written in procedural style. And what style is better: procedural or object-oriented? We should perform the choice under given conditions which facilitates its development and readability – in accordance with the principles of Clean Code.

Below is an example of the procedural code that will help me consider the purity of the code and its refactoring to the object oriented code.

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");
    }
}

I choose the style in which the code will be written on the basis of observing the direction of changes that result from emerging new business requirements.

What changes does the procedural code allow?


If I mainly add new functions operating on already existing data structures, then the procedural code (new procedures) will probably remain legible. An example is the new function that returns the smallest rectangle containing given figure.

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");
    }
}

When will the procedural code become illegible?


But if you plan to add or modify existing data structures, it will force changes to all existing procedures. What happens when I decide to change the components in the Rectangle data structure to points describing 2 opposite corners of the square?

public class Point {
    double x,y; 
 
public class Rectangle {
     Point topLeft;
     Point bottomRight; 
}

It is not difficult to notice that such a change will force many changes to existing procedures. A way to avoid many changes (or minimize them) is to place the getX () and getY () methods in the Rectangle structure that will perform the necessary calculations.

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);
    }
}

But note that from that moment I start to hide details of the data structure. Details in the Rectangle class have been hidden and new methods calculate the necessary output. In this way, I am starting to change the code style from procedural to object oriented.

How to refactor a procedural code into an object-oriented one?


Perform self-encapsulation of data structures

At the beginning I add constructors and encapsulate details within data structures. In my case, the data in the structures are not changing, so the fields can be final.

public class Circle {
    private final double radius;
 
    public Circle(double radius) {
        this.radius = radius;
    }
 
    public double getRadius() {
        return radius;
    }
}

Define a common interface / base class for existing data structures

Next, I define an empty “Shape” base class that will expand all data structures. From now on, the “area” procedure accepts only the “Shape” abstract class extension as a parameter. Alternatively, it can also be a common interface.

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;
    }
}
 
...

Move the logic from the procedure to the base class

In order to transfer the logic to the base class, I will make a small modification to be able to use the method transfer in the IntelliJ tool.

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());
    }
}

I obtained the above code by extracting a new method “calculateArea”, then deleting the word static and adding a call to the constructor.

Then I move the method containing the “calculateArea” logic from “Geometry” to the “Shape” base class.

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());
    }
}

After this distortion, there was a code smell: “base class is dependent on its derived classes”. Solving the problem will lead us to the next transformation.

Push method down

Oracle Java Tutorial and Material, Oracle Java Exam Prep, Oracle Java Learning

The transformation is fully automated in many environments like IntelliJ, Eclipse, NetBeans.

Delete unnecessary logic in derived classes


Finally, we finish with the transformation “replace conditional expressions with polymorphism”. In each of the subclasses (i.e. our old data structures), only one condition will be true.

Oracle Java Tutorial and Material, Oracle Java Exam Prep, Oracle Java Learning

The final result of our refactoring is below

public class Circle extends Shape {
    private final double radius;

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

    public double getRadius() {
        return radius;
    }

    @Override
    double calculateArea() {
        Circle circle = (Circle) this;
        return Math.PI * circle.getRadius() * circle.getRadius();
    }
}

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

In addition, we can inline “Geometry.area” function and then change the name of “calculateArea” to “area”, so we come back to old naming.

Interpreter pattern

Oracle Java Tutorial and Material, Oracle Java Exam Prep, Oracle Java Learning

Related Posts

0 comments:

Post a Comment