Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Java Holiday Calendar 2016 (Day 23): Use Mappable Types Instead of Bloated Ones

DZone's Guide to

Java Holiday Calendar 2016 (Day 23): Use Mappable Types Instead of Bloated Ones

Using mappable types in your geometric classes means less inter-class coupling and opens up a more functional path -- where you can apply functions on objects.

· Java Zone
Free Resource

The single app analytics solutions to take your web and mobile apps to the next level.  Try today!  Brought to you in partnership with CA Technologies

Image title

Today's tip is about mappable types. Traditionally, we Java developers have relied on inheritance and types with a number of methods to support the convenient use of our classes. A traditional Point class would contain x and y coordinates and perhaps also a number of functions that allows us to easily work with our Point.

Imagine now that we add new shapes like Circle, and that Circle inherits from Point and adds a radius. Further imagine that there are a bunch of other geometric shapes like Line, Square, and the likes that also appear in the code. After a while, all our classes become entangled in each other and we end up in a messy hairball.

However, there is another way of structuring our geometric classes such that there is a minimum of inter-class coupling. Instead of letting each geometric class provide specific methods for translations like add() and flipAroundXAxis(), we could add just one or two generic methods that operate on the geometric figure and returns a value of any type. We could then break out the old methods from the geometric classes, convert them to functions, and just apply them on the objects, rather than letting the objects handle that responsibility.

Let us take the concept for a spin!

Traits

We start with some basic Traits of the geometrical shapes that are shared amongst most shapes. Here are the basic traits:

interface HasX { int x(); } // x is the x-coordinate
interface HasY { int y(); } // y is the y-coordinate
interface HasR { int r(); } // r is the radius


What's the Point?

Now it is time to create our Point interface like this:

interface Point extends HasX, HasY {
    static Point point(int x, int y) {
        return new Point() {
            @Override public int x() { return x; }
            @Override public int y() { return y; }
        };
    }

    static Point origo() { return point(0, 0); }
    default <R> R map(Function<? super Point, ? extends R> mapper) {
        return Objects.requireNonNull(mapper).apply(this);
    }
    default <R, U> R map(BiFunction<? super Point, ? super U, ? extends R> mapper, U other) {
        return Objects.requireNonNull(mapper).apply(this, other);
    }
}


I have purposely refrained from creating an implementation class of the Point interface. Instead, each time the static point() method is called, an instance from an anonymous class is created. This illustrates that the implementation class is "pure" and does not inherit or override anything. In a real solution, there could, of course, be an implementation of an immutable class PointImpl.

In the middle, there is a conveniency method that returns a point in origo (e.g. point(0, 0)).

The two methods at the end of the class are where the interesting stuff starts. They allow us to apply almost any function to a Point.

The first one takes a mapping function that, in turn, takes a point and maps it so something else (i.e. a Function)

The last one takes a mapping function that takes two points and maps it to a Point (e.g. a Binary Function). The mapping function applies the current Point (i.e. "this") as the first parameter and then it also applies another point. That other point is given as the second argument to the map() function. Complicated? Not really. Read more and it will be apparent what is going on.

The Functions

Now we can define a number of useful functions that we could apply to the Point class.

interface PointFunctions {
    static UnaryOperator<Point> NEGATE = (f) -> point(-f.x(), -f.y());
    static BinaryOperator<Point> ADD = (f, s) -> point(f.x() + s.x(), f.y() + s.y());
    static BinaryOperator<Point> SUBTRACT = (f, s) -> point(f.x() - s.x(), f.y() - s.y());
    static UnaryOperator<Point> SWAP_OVER_X_AXIS = (f) -> point(f.x(), -f.y());
    static UnaryOperator<Point> SWAP_OVER_Y_AXIS = (f) -> point(-f.x(), f.y());
    static BinaryOperator<Point> BETWEEN = (f, s) -> point((f.x() + s.x()) / 2, (f.y() + f.y()) / 2);
    static Function<Point, String> TO_STRING = (p) -> String.format("(%d, %d)", p.x(), p.y());
    static BiFunction<Point, Point, Boolean> EQUALS = (f, s) -> (f.x() == s.x()) && (f.y() == s.y());

    //
    static BiFunction<Point, Point, Double> DISTANCE = (f, s) -> Math.sqrt((f.x()-s.x())^2+(f.y()-s.y())^2);

    //
    static Function<Point, UnaryOperator<Point>> ADD2 = s -> (f) -> point(f.x() + s.x(), f.y() + s.y());
}


These methods (or any other similar methods or lambdas) can now be applied to points without polluting the classes.

Example of Usage

This code snippet will create a point at origo, apply a number of translations to it, and then convert it to a string using the TO_STRING mapper. Note how easy it would be to use another string mapper because now the "to string" functionality is separate from the class itself. It would also be easy to use a custom lambda instead of one of the pre-defined functions.

System.out.println(
     origo()
         .map(ADD, point(1, 1))       // 1
         .map(SWAP_OVER_X_AXIS)       // 2
         .map(NEGATE)                 // 3
         .map(BETWEEN, point(-1, -1)) // 4
         .map(TO_STRING)
    );


This will produce the following output:

(-1, 0)

As can be seen in the picture below, this seems to be correct:

Image title

Expanding the Concept 

If we later introduce a Circle interface like this...

interface Circle extends HasX, HasY, HasR {
  // Stuff similar to Point...
}


...and we change our methods so that they can work with the traits directly (and after some additional refactoring not shown here), we can get a decoupled environment where the functions can be made to operate on any relevant shape. For example, the following method can return a "toString" mapper that can work on anything having an x and a y value (e.g. both Point and Circle)

static <T extends HasX & HasY> Function<T, String> toStringXY() {
    return (T t) -> String.format("(%d, %d)", t.x(), t.y());
}


After further modification, we could take a Circle and ADD a Point to it and the circle will be translated using the point's coordinates.

Alternate Solutions

It is possible to wrap any class in an Optional and then use the Optional's map() method to map values. In that case, we do not need the map() functions in the shapes. On the other hand, we would have to create an Optional and then also get() the end value once all mappings have been applied.

Another way would be just having a bunch of static methods that takes a Point and other shapes as parameters like this:

static <T extends HasX & HasY> Point add(Point p, T other) {
    return point(p.x() + other.x(), p.y() + other.y());
}


But that... is another story...

Follow the Java Holiday Calendar 2016 with small tips and tricks all the way through the winter holiday season.

CA App Experience Analytics, a whole new level of visibility. Learn more. Brought to you in partnership with CA Technologies.

Topics:
java ,lambdas ,functions ,mappable types ,tutorial

Published at DZone with permission of Per-Åke Minborg, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}