Wednesday, 15 April 2015

Lucee 5 beta: abstract (redux)

G'day:
Right, after a bit of a false start with the abstract support in Lucee (ie: they forgot to release it in the 5.0.0.42 version, when I first tried it - "Lucee 5 beta: abstract components (abject fail now fixed)"), it's now all working (as of 5.0.0.43). So I'll revisit with an example to demonstrate its behaviour.

A while back I looked at how PHP dealt with abstraction: "Looking at PHP's OOP from a CFMLer's perspective: inheritance, static & abstract, interfaces", and I have taken that code and converted it to CFML and will use that as an example. It's interesting how close the code is between languages. CFML is a bit cleaner though.



So the example is going to use the cliche of an abstract Shape, and a concrete implementation Circle. Along the way we have another coupla classes too. I'll get to those.

First the Shape:


// Shape.cfc
abstract component {

    private this.dimensions;

    function getDimensions(){
        return this.dimensions
    }

}

A Shape can have dimensions (eg: a circle has two dimensions; a sphere three), and that's about it. Note I'm using Lucee's new private access modifier on my dimensions property here. I'm also declaring it here, but not setting it. We'll set it in a subclass once we know what sort of shape we're dealing with.

Note that despite this being an abstract class, it doesn't actually have abstract methods. The one method it has is concrete. This is completely OK. Because one cannot really have an actual "Shape" object, it's abstract. This means one cannot create instances of it; instead one uses it for the basis of concrete implementations. However it is also still a super class (indeed it is intrinsically a super class) in the normal OO sense of the notion, it can still describe generic behaviour. In this case, all Shapes have dimensions, so all Shapes will need a getDimensions() method, so this is the right place to have the concrete implementation of this method.

Now... what happens if I try to create a Shape object:


// shape.cfm
shape = new Shape()    

I get an error:

Lucee 5.0.0.43 Error (application)
Message you cannot instantiate the abstract component [Shape.cfc], this component can only be extended by other components


All good. Because it's abstract, I cannot create objects from it.

OK, so shapes can have dimensions, and today we're only dealing with two-dimensional shapes, so we'll have another abstract class, TwoDimensionalShape:


// TwoDimensionalShape.cfc
abstract component extends=Shape {

    private this.dimensions = 2;

    abstract function getPerimeter();
    abstract function getArea();

}

This is all a bit contrived, I know. So a shape has the notion of dimensions, and intrinsically a two-dimensional shape has two dimensions, we can set this property now.

Also, here we have a couple of abstract methods. We know all two dimensional shapes will have a notion of a perimeter, and they will all have area. What we don't know is how those values will be derived. IE: the equation for the perimeter of a circle is different from that of a square. So we can identify the requirement, but we cannot implement them. They're an abstract requirement.

Finally I stop pissing around and have a concrete class. A Circle is a TwoDimensionalShape:


// Circle.cfc
component extends=TwoDimensionalShape {

    private this.radius;

    function init(radius){
        this.radius = arguments.radius
    }

    function getCircumference(){
        return 2 * pi() * this.radius
    }

    function getPerimeter(){
        return getCircumference()
    }

    function getArea(){
        return pi() * (this.radius ^ 2)
    }

}

Circle implements all the abstract methods from its super classes. Here the implementation of the getPerimeter() method is simply to call the Circle's own getCircumference() method.

And we test this:


// circle.cfm
circle = new Circle(7)

echo("Dimensions: #circle.getDimensions()#<br>")
echo("Perimeter (circumference): #circle.getPerimeter()# (#circle.getCircumference()#)<br>")
echo("Area: #circle.getArea()#<br>")

This outputs:

Dimensions: 2
Perimeter (circumference): 43.982297150257 (43.982297150257)
Area: 153.9380400259


Just as we'd expect.

OK, so what if we have a concrete class which does not implement all the super class(es) abstract methods. Here's a Square... but it doesn't implement getArea():

// Square.cfc
component extends=TwoDimensionalShape {

    private this.sideLength;

    function init(sideLength){
        this.sideLength = arguments.sideLength
    }

    function getPerimeter(){
        return sideLength * 4
    }

}


// square.cfm
square = new Square(7)

echo("Dimensions: #square.getDimensions()#<br>")
echo("Perimeter (circumference): #circle.getPerimeter()# (#circle.getCircumference()#)<br>")
echo("Area: #circle.getArea()#<br>")

This yields:


And this - other than the slight glitch that it's talking about interfaces not abstract classes (LDEV-284) - is spot on. If a class extends an abstract class, one needs to implement all the abstract methods too. Even if you don't intend to use 'em.

Here all the concrete methods are in the lowest level class, but they can be implemented anywhere in the inheritance hierarchy. I should have contrived some abstract method in Shape (perhaps getLocation()?), and then implement it in TwoDimensionalShape, just to demonstrate this. Oh well: take my word for it.

Right so why do we want any of this? Abstracting a class simply identifies to the calling code that a given class just defines partial behaviour in an inheritance hierarchy, but in and of itself it's not useful. It's just to help the people writing the calling code know this. Abstracting a method is much the same as specifying a method requirement in an interface. However it allows one to do this within an actual class rather than just in an interface.

I dunno how much use it'll get outside of API writers, but... well... it's one of these features that perhaps should have been in CFML from the outset...

--
Adam