Sunday, 5 June 2022

Interfaces over Unions


Unions have started coming into PHP with the release of version 8. We have been using them a bit in typescript and have found that opting for an interface is much better. Interfaces clean up the flow of your code and make things much more readable by eliminating the need for many if statements.

You are not defining what functions an object can have if you use a Union. An interface is much better as you can reliably know that methods are on an object. For this example, we will be using a shape example. We have an interface of a Shape and then two shapes, a Circle and a Square. If we have a function that returns the union Circle | Squire, then we try to calculate the area of the shape we will have some logic there because calculating the area is different for the two shapes.

/** @var Circle | Squire */
$shape = getShape();
if ($shape instanceof Circle) {
    // Calculate area of the circle
} else if ($shape instanceof Squire) {
    // Calculate area of the circle
}

This will leave us with if statements all over our codebase. To clean this up, we can use a Shape interface that has a getArea method on it. This will abstract the logic for calculating the area into each implementation of the Shape. After we refactor, our code is much neater and more semantic.

/** @var Shape */
$shape = getShape();
$area = $shape->getArea();

Overall I think we should be opting to abstract our logic into the implementations of interfaces rather than using union to return multiple types. This will lead to a more structured codebase and logic can be reused easier.

Of course there are some exceptions to this, and there are always two ways to skin a cat. One that always comes straight to mind is making a return type of function nullable with ?Square. This is one way I do usually use unions to return when we are unable to create the instance of a Square and return null.

After we refactored our codebase to use interfaces with, common functions we found not only was our code more readable. There was a lot less off it, this became more maintainable and removed a lot of the bugs we had around trying to use the wrong thing in the wrong place.

interface Shape {
    /** Gets the width of the shape */
    public function getWidth(): float;
    /** Get the total aria for the shape */
    public function getArea(): float;
    /** Get the number of sides this shape has */
    public function getNumberOfSides(): int;
}

class Circle implements Shape {
    /** The radius of the circle */
    public float $radius = 1.0;
    /** @inheritdoc */
    public function getWidth(): float { return $this->radius *; }
    /** @inheritdoc */
    public function getArea(): float { return M_PI * pow($this->radius, 2); }
    /** @inheritdoc */
    public function getNumberOfSides(): float { return 1; }
}

class Square implements Shape {
    /** The radius of the circle */
    public float $width = 1.0;
    /** The height of the circle */
    public float $height = 1.0;
    /** @inheritdoc */
    public function getWidth(): float { return $this->width; }
    /** @inheritdoc */
    public function getArea(): float { return $this->width * $this->height }
    /** @inheritdoc */
    public function getNumberOfSides(): float { return 4; }
}