6
\$\begingroup\$

I've been working on a Java-based mathematics library focusing on vectors and matrices. I plan to use it for an important upcoming project, so the classes are analogous to data types available in GLSL (e.g. Vector2 for GLSL's vec2, Matrix4 for GLSL's mat4, and so on).

I'm trying to only expose interface to clients (e.g. Vector2) and keep implementation classes package-private (e.g. Vector2f). I'm trying to use the static factory methods and, since I'm using Java 1.8, I'm trying to take advantage of the new option that allows static methods to be added to interfaces.

However, there's something I currently don't like too much and I'm wondering if there's a way to improve the code design to address it.

Here's what the interface looks like (I've omitted the non-static methods for brevity because they're not relevant to the question):

public interface Vector2 extends Measurable, Bufferable {

    static Vector2 createZeroVector() {
        return Vector2f.createZeroVector();
    }

    static Vector2 createFrom(float x, float y) {
        return Vector2f.createFrom(x, y);
    }

    static Vector2 createFrom(double x, double y) {
        return Vector2f.createFrom(x, y);
    }

    static Vector2 createFrom(final float[] values) {
        return Vector2f.createFrom(values);
    }

    static Vector2 createFrom(final double[] values) {
        return Vector2f.createFrom(values);
    }

    static Vector2 createNormalizedFrom(float x, float y) {
        return Vector2f.createNormalizedFrom(x, y);
    }

    static Vector2 createNormalizedFrom(double x, double y) {
        return Vector2f.createNormalizedFrom(x, y);
    }

    static Vector2 createNormalizedFrom(final float[] values) {
        return Vector2f.createNormalizedFrom(values);
    }

    static Vector2 createNormalizedFrom(final double[] values) {
        return Vector2f.createNormalizedFrom(values);
    }

    // ... non-static methods omitted ...
}

I'll briefly note that:

final class Vector2f implements Vector2 {
    // details omitted
}

Since the interface methods are static, they must be implemented in the interface itself. Classes implementing the interface don't get overrides because this can be resolved at compile time.

However, if a client goes ahead and does something like this:

public class Vector2d implements Vector2 {
    // a double-precision implementation
}

and then tries to use the interface as intended:

// ...
Vector2 vec = Vector2.createFrom(5f, 3f);

Then there seem to be at least 2 problems:

  1. the interface knows about a specific implementation and can only return Vector2f instances, and

  2. a client expecting a Vector2d implementation class gets a Vector2f instead!

Since the methods in the interface are static, there seems to be no way for the client to actually get an instance of the hypothetical Vector2d implementation without changing the interface itself.

How can I improve the code's design in order to resolve this problem for clients?

I've read other articles and I'm not aware of something else that might be helpful here, though I might be wrong.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

To put it simply, you're using the wrong language elements to achieve your goals.

The purpose of interfaces is to serve as a contract. They define a set of actions that implementations must be able to perform. Although Java 8 made it possible to add static and default methods to an interface (and I'll have to read up on why they did that), it's best when an interface contains no implementation at all. Before adding static or default methods, think twice, and question yourself if there's a better way.

In this example, it seems quite clear that all those createFrom* methods are in fact utility methods, that would be best in a utility class. There's no point putting them anywhere else if they cannot be extended or overridden. So it would be better to extract these to a Vector2dUtils class or similar.

And why does a Vector2d extend Bufferable and Measurable? What does that even mean? It seems like this interface is trying to do too much. I strongly suspect violations of the single responsibility principle in your implementation.

  1. the interface class knows about a specific implementation and can only return Vector2f instances, and

Why does an interface know about any implementations at all? It really shouldn't.

  1. a client expecting a Vector2d implementation class gets a Vector2f instead!

Why would a client expect a specific implementation from a method that returns an interface type? It really shouldn't.

You seem deeply confused. I recommend further reading on the subject:

  • In Effective Java by Joshua Bloch (in this order):

    • Item 19: Use interfaces only to define types
    • Item 17: Design and document for inheritance or else prohibit it
    • Item 18: Prefer interfaces to abstract classes
    • Item 16: Favor composition over inheritance
  • Consider a related pattern used by java.util.Collections (source code), in particular the facilities provided by methods like .emptyList, .unmodifiableList, .synchronizedList

  • Consider a related pattern used by java.util.EnumSet (source code), especially how its various factory methods create either RegularEnumSet or a JumboEnumSet depending on what is appropriate given the input params, and the client just doesn't need to know whatever is the actual implementation.

\$\endgroup\$
5
  • \$\begingroup\$ I'm familiar with the book, but I intend to follow up on that again. For example, "Item 1: Consider static factory methods instead of constructors", if applied here would suggest a new class to do something like: Matrix4 m = Matrices.createFrom(...); However, it was my initial impression that one reason for allowing static methods in interfaces was to avoid having to create these kinds of classes. This is why I gave it a go at first. I think you may've slightly misunderstood #1 & #2; we agree that the client shouldn't care about impls., but only noting something unexpected by someone else. \$\endgroup\$
    – code_dredd
    Commented Feb 22, 2015 at 10:48
  • \$\begingroup\$ I forgot to mention, your question on Bufferable and Measurable; they are interfaces that are unrelated to the question. But to answer your question, the Measurable interface only adds a public float length(); because vectors can be measured (i.e. length and magnitude are the synonyms here). The Bufferable interface simply adds public float[] toFloatArray(); and public FloatBuffer toFloatBuffer(); methods as these are helpful for passing data to OpenGL/GLSL, particularly if you're using JOGL. Hope this clarifies. \$\endgroup\$
    – code_dredd
    Commented Feb 22, 2015 at 11:52
  • \$\begingroup\$ Item 1 is about an alternative for constructors, and it applies specifically for implementations, not interfaces. It gives examples of legitimate use cases (descriptive names, instance control, ...), none of which matches your case here. Item 19 is essentially saying that implementation doesn't belong to interfaces, and it's a good principle that stretches beyond Java itself. The new facilities of Java 8 are power tools, like a chainsaw, and you should use them with care, and with good justification, ruling out alternatives as inferior, which I just don't see here. \$\endgroup\$
    – janos
    Commented Feb 22, 2015 at 13:06
  • \$\begingroup\$ In your opinion, what would a brief code snippet taking those points above into account look like? I can't write much here, but I'm planning to keep the interface, after removing all the factory methods, and move them to a utility class (e.g. Matrix4 m = Matrix4Util.createIdentityMatrix();). However, I'm trying to avoid separate utility classes like this, if at all possible. \$\endgroup\$
    – code_dredd
    Commented Feb 24, 2015 at 5:00
  • \$\begingroup\$ I think that makes sense. You can post your updated version in a different question and seek another review. It will be good to include there some code using Vector2d and Vector2f instances. I'm especially concerned about the bullet points #1 and #2 you raised in your post. \$\endgroup\$
    – janos
    Commented Feb 24, 2015 at 22:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.