DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
  1. DZone
  2. Coding
  3. Java
  4. Java Holiday Calendar 2016 (Day 23): Use Mappable Types Instead of Bloated Ones

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.

Per-Åke Minborg user avatar by
Per-Åke Minborg
·
Dec. 23, 16 · Tutorial
Like (6)
Save
Tweet
Share
8.63K Views

Join the DZone community and get the full member experience.

Join For Free

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.

Java (programming language) Calendar (Apple)

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

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • How Do the Docker Client and Docker Servers Work?
  • The 12 Biggest Android App Development Trends in 2023
  • Taming Cloud Costs With Infracost
  • Remote Debugging Dangers and Pitfalls

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: