DEV Community

Taqmuraz
Taqmuraz

Posted on

Modern Java - do we need explicit class declarations?

When we develop a public API, we hope it will live long or forever.
Do we? Then why do we still write and use public constructors? They do not present any abstraction. Once you mentioned in your code some constructors, you nailed them to your program for eternity.

In this article I will explain, how to create objects in a better way, without declaring constructors or classes explicitly.

We need to write a program

Now, let us imagine we want to write a program that would paint the following image :
Image description
Let us write code then.
First, we have to declare common abstractions.

import java.awt.Graphics2D;
interface Drawing
{
    void draw(Graphics2D graphics);
}
Enter fullscreen mode Exit fullscreen mode

Good enough.
Now we may write a code that uses our Drawing interface to paint something into image :

static BufferedImage paint(int width, int height, Drawing drawing)
{
    var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    drawing.draw((Graphics2D)image.getGraphics());
    return image;
}
Enter fullscreen mode Exit fullscreen mode

After that, we need an implementation for a circle drawing :

import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Color;
class Circle implements Drawing
{
    Point center;
    int radius;
    Color color;
    Circle(Point center, int radius, Color color)
    {
        this.center = center;
        this.radius = radius;
        this.color = color;
    }
    @Override
    public void draw(Graphics2D graphics)
    {
        graphics.setColor(color);
        int dr = radius << 1;
        graphics.fillOval(center.x - radius, center.y - radius, dr, dr);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now a box drawing :

import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Rectangle;
class Box implements Drawing
{
    Rectangle rect;
    Color color;
    Box(Rectangle rect, Color color)
    {
        this.rect = rect;
        this.color = color;
    }
    @Override
    public void draw(Graphics2D graphics)
    {
        graphics.setColor(color);
        graphics.fillRect(rect.x, rect.y, rect.width, rect.height);
    }
}
Enter fullscreen mode Exit fullscreen mode

As we see, our paint method does accept only one Drawing object, so, we need an implementation that would combine many drawings into one :

class Scene implements Drawing
{
    Drawing[] drawings;

    Scene(Drawing... drawings)
    {
        this.drawings = drawings;
    }

    @Override
    public void draw(Graphics2D graphics)
    {
        for(var drawing : drawings) drawing.draw(graphics);
    }
}
Enter fullscreen mode Exit fullscreen mode

And, all we have left is to write the main function :

public static void main(String[] args) throws Throwable
{
    var file = new File(args[0]);

    int width = 200;
    int height = 200;

    var drawing = new Scene(
        new Box(new Rectangle(0, 0, width, height), Color.WHITE),
        new Circle(new Point(100, 100), 50, Color.RED),
        new Box(new Rectangle(25, 25, 100, 50), Color.GREEN));
    var image = paint(width, height, drawing);
    ImageIO.write(image, "png", file);
}
Enter fullscreen mode Exit fullscreen mode

It took 85 lines of code. This code is flexible, maintainable, readable. But, don't you see its redundant parts? Almost 30% of lines are about fields and constructors declaration.
Also, we have repeated code here : Graphics2D.setColor calls.
How may we improve this code?

Static functions are better than constructors

What constructor is? A function, that we call to create a new object. Wait...a function, that we call to create a new object...do we need constructors at all?

Java has a brilliant syntaxes - anonymous class expression and lambda expression. Using such expressions we may write code without declaring any classes.

What we are going to do

Here is an example :

interface FloatFunction
{
    float apply(float a);
}
class SumFloatFunction implements FloatFunction
{
    float addition;
    SumFloatFunction(float addition)
    {
        this.addition = addition;
    }
    @Override
    public float apply(float a)
    {
        return a + addition;
    }
}
Enter fullscreen mode Exit fullscreen mode

Let us replace class declaration with a static function declaration inside of the FloatFunction interface :

interface FloatFunction
{
    float apply(float a);

    static FloatFunction sum(float addition)
    {
        return a -> a + addition;
    }
}
Enter fullscreen mode Exit fullscreen mode

Do you see, how small our code now is?
More than that, now we may see all FloatFunction interface implementations available - we don't have to read documentation each time to refresh information about existing implementations. All what we need is declared in the one place.

Using our new approach, let us change solution with Drawing interface.

interface Drawing
{
    void draw(Graphics2D graphics);

    static Drawing box(Rectangle rect, Color color)
    {
        return graphics ->
        {
            graphics.setColor(color);
            graphics.fillRect(rect.x, rect.y, rect.width, rect.height);
        };
    }
    static Drawing circle(Point center, int radius, Color color)
    {
        return graphics ->
        {
            graphics.setColor(color);
            int dr = radius << 1;
            graphics.fillOval(center.x - radius, center.y - radius, dr, dr);
        };
    }
    static Drawing scene(Drawing... drawings)
    {
        return graphics ->
        {
            for(var drawing : drawings) drawing.draw(graphics);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

And main function also has changes :

public static void main(String[] args) throws Throwable
{
    var file = new File(args[0]);

    int width = 200;
    int height = 200;

    var drawing = Drawing.scene(
        Drawing.box(new Rectangle(0, 0, width, height), Color.WHITE),
        Drawing.circle(new Point(100, 100), 50, Color.RED),
        Drawing.box(new Rectangle(25, 25, 100, 50), Color.GREEN));
    var image = paint(width, height, drawing);
    ImageIO.write(image, "png", file);
}
Enter fullscreen mode Exit fullscreen mode

Looks much better? Right. And that was only a first step.

Methods with default implementation

As I said earlier, I don't like we have Graphics2D.setColor calls repeated. If we would paint 1000 boxes of the same color, do we need to call setColor method 1000 times? No.

What then we may do?

Let us create a wrapping function, that will change color as many times as we need :

default Drawing ofColor(Color color)
{
    return graphics ->
    {
        graphics.setColor(color);
        draw(graphics);
    };
}
Enter fullscreen mode Exit fullscreen mode

And, we are going to change Drawing static functions as well, because now they have no business with Color.

interface Drawing
{
    void draw(Graphics2D graphics);

    static Drawing box(Rectangle rect)
    {
        return graphics ->
        {
            graphics.fillRect(rect.x, rect.y, rect.width, rect.height);
        };
    }
    static Drawing circle(Point center, int radius)
    {
        return graphics ->
        {
            int dr = radius << 1;
            graphics.fillOval(center.x - radius, center.y - radius, dr, dr);
        };
    }
    static Drawing scene(Drawing... drawings)
    {
        return graphics ->
        {
            for(var drawing : drawings) drawing.draw(graphics);
        };
    }
    default Drawing ofColor(Color color)
    {
        return graphics ->
        {
            graphics.setColor(color);
            draw(graphics);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Then code that creates many boxes of the same color would look like that :

static Drawing manyBoxes(Color color, Rectangle... rects)
{
    var boxes = Stream.of(rects).map(Drawing::box).toArray(Drawing[]::new);
    return Drawing.scene(boxes).ofColor(color);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I don't propose to always avoid use of classes. We may need them to describe data structures or program entries. But, anywhere else, I would like to use lambda or anonymous class expressions only.

After all, functions let us to decouple from a real program structure and to forget about exact implementation. You can't redirect a constructor call, but you can redirect a function call.
When you develop a large product, you can't rename class or change its accessibility, if it had once a public constructor. Give self more comfort and space, use functions instead of constructors.
That's all for today, thanks if you read this article to the end.
Please, share your thoughts in comments.

Top comments (0)