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 :
Let us write code then.
First, we have to declare common abstractions.
import java.awt.Graphics2D;
interface Drawing
{
void draw(Graphics2D graphics);
}
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;
}
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);
}
}
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);
}
}
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);
}
}
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);
}
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;
}
}
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;
}
}
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);
};
}
}
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);
}
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);
};
}
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);
};
}
}
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);
}
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)