We still have types in an API that will be implemented, but now, rather than type interrelations, the focus will be on type hierarchies. Although I could once again turn to the Node types in GameCracker for illustration, I will mix it up a bit and demonstrate my difficulties using my Matrix library.
The Matrix types
The gimmick of this library is that I wanted a type-rich linear algebra package; apart from the general Matrix type, I wanted a subtype for Vectors (1-column matrices), 3x3 matrices, 3-element vectors etc. These specialized types could define methods appropriate for that particular matrix (for example, 3D vectors have a function called cross product), and, the primary reason, common operations like addition and transposition can be defined in subtypes to return a more specific type. Just in case I'm talking nonsense, here is a hopefully helpful example, only focusing on two types and just a couple of methods for the sake of simplicity:public interface Matrix {Technically everything could be done with the single Matrix type, but being able to use and work with subtypes help readability a lot when doing math. Readability is really-really important. Working with 3D points is so much easier when you have a List<Vector3> rather than a List<Matrix>, not to mention that the Vector3 type can have very nice getX(), getY(), getZ() functions.
/** Returns the number of columns. */
public int getColumns();
/** Returns the number of rows. */
public int getRows();
/** Returns the (row,col) element. */
public double get(int row, int col);
/** Adds this matrix and m, and returns the result. */
public Matrix plus(Matrix m);
}
public interface Vector extends Matrix {
/** Returns the number of components. */
public int getDimension();
/** Returns the i-th component. */
public double getCoord(int i);
/** Adds this vector and v, and returns the result. */
public Vector plus(Matrix m);
/** Compute the dot product of this vector and v. */
public double dot(Vector v)}
Implementing the Types
There are a number of possible implementations for the Matrix API. The most straightforward is one that uses an array to store the elements. Note that the implementation must provide concrete classes for every type: it must have 2D vectors, 3D vectors, it must have vectors that are neither, and it must have matrices that are not vectors. The parent types are not just there to define common functionality to the subtypes.A couple of disclaimers before I proceed.
public class ArrayMatrix implements Matrix {
private double[][] data;
@Override
public int getColumns() {
return data[0].length;
}
@Override
public int getRows() {
return data.length;
}
@Override
public double get(int row, int col) {
return data[row][col];
}
@Override
public Matrix plus(Matrix m) {
if (m.getRows()!=getRows() || m.getColumns()!=getColumns())
throw new IllegalArgumentException("size mismatch");
Matrix result=MatrixFactory.create(m.getRows(), m.getColumns());
for (int row=0; row<getRows(); row++)
for (int col=0; col<getColumns(); col++)
result.set(row, col, get(row, col)+m.get(row, col));
}
}
public class ArrayVector extends ArrayMatrix implements Vector {
@Override
public int getDimension() {
return getRows();
}
@Override
public double getCoord(int i) {
return get(i, 0);
}
@Override
public Vector plus(Matrix m) {
return (Vector)super.plus(m);
}
@Override
public double dot(Vector v) {
if (getDimension()!=v.getDimension())
throw new IllegalArgumentException("size mismatch");
double result=0;
for (int i=0; i<getDimension(); i++)
result+=getCoord(i)*v.getCoord(i);
return result;
}
}
- First, this does not exactly reflect what I have in my library in terms of names and such. My goal is not to be faithful to the source material, but to provide a simple example.
- Second, I just omit constructors and functions like Matrix.set, hoping that no-one will seriously miss them during this discussion.
- Third, about the creation of the matrix instances. As part of the gimmick, I want every matrix that is created through operations throughout the library to have the most specific type. For example, if a matrix has only one column, then it needs to be a Vector, so that the user can cast it to one and take advantage of the subtype. Since the API is public, I have no way of enforcing this, but I have a MatrixFactory class that does take care of this constraint, and everywhere in my library I use this factory whenever I need an instance.
- All different kinds of matrix implementations have their own factory because of this, but I wanted general API functions that create new instances (like Matrix.plus) to create one of the default implementation, rather than of the same implementation as the 'creator'. The reason for this is to avoid surprises. Imagine that you get two matrices, add them together, try to modify the result, and you get an exception because the first original matrix was actually an ImmutableMatrix, and its plus method also returned an ImmutableMatrix. That would make using the library rather unpleasant; that's why whenever a matrix needs to be created in the library, the default MatrixFactory is used.
API as Abstract Classes
To share code with subtypes, you stick that code into a common supertype. Usually. To do that here, we need the API types to be abstract classes rather than interfaces:
public abstract class Matrix {That's all nice and good, ArrayMatrix can inherit the matrix operations from Matrix, ArrayVector can inherit the vector operations from Vector.
/** Returns the number of columns. */
public abstract int getColumns();
/** Returns the number of rows. */
public abstract int getRows();
/** Returns the (row,col) element. */
public abstract double get(int row, int col);
/** Adds this matrix and m, and returns the result. */
public Matrix plus(Matrix m) {
if (m.getRows()!=getRows() || m.getColumns()!=getColumns())
throw new IllegalArgumentException("size mismatch");
Matrix result=MatrixFactory.create(m.getRows(), m.getColumns());
for (int row=0; row<getRows(); row++)
for (int col=0; col<getColumns(); col++)
result.set(row, col, get(row, col)+m.get(row, col));
}
// other non-abstract functions
}
public abstract class Vector extends Matrix {
@Override
public int getDimension() {
return getRows();
}
@Override
public double getCoord(int i) {
return get(i, 0);
}
@Override
public Vector plus(Matrix m) {
return (Vector)super.plus(m);
}
public double dot(Vector v) {
if (getDimension()!=v.getDimension())
throw new IllegalArgumentException("size mismatch");
double result=0;
for (int i=0; i<getDimension(); i++)
result+=getCoord(i)*v.getCoord(i);
return result;
}
}
Now ArrayVector can no longer inherit from ArrayMatrix, since it already has a superclass. So rather than duplicating the 'other' functions among different implementations, we need to duplicate the implementation-specific 'core' functions and data structures among the different types of that implementation. And yes, we could use this opportunity to implement ArrayVector using a simple double[] rather than double[][], but we also had this option in the previous version, and now we don't have the option to share the code.
public class ArrayMatrix extends Matrix {
private double[][] data;
@Override
public int getColumns() {
return data[0].length;
}
@Override
public int getRows() {
return data.length;
}
@Override
public double get(int row, int col) {
return data[row][col];
}
// all the other functions we'll just inherit
}
public class ArrayVector extends Vector {
// aww, we no longer extend ArrayMatrix
private double[][] data;
@Override
public int getColumns() {
return data[0].length;
}
@Override
public int getRows() {
return data.length;
}
@Override
public double get(int row, int col) {
return data[row][col];
}
// all the other functions inherited from Vector
}
This looks like a trade, and a quite favourable one too (there are a lot of functions that have default implementations), but I have a very strong counterargument. I want ImmutableVector to be an ImmutableMatrix. The immutable implementation is a bit of a special case in that it comes with the guarantee that their matrix elements will never change. Users of the library utilize this guarantee by using the Immutable* types (in contrast to the other implementations like Array*, which don't even need to be public). To have an ImmutableVector that is not an ImmutableMatrix, it doesn't make any sense.
This counterargument doesn't actually seem that strong as I originally thought, but anyway... We tried to avoid code duplication, but didn't really succeed. In Java, ArrayVector cannot inherit common Vector behaviour from the API while inheriting common ArrayMatrix behaviour as well. No multiple inheritence allowed. I opted to keep the type hierarchy in the implementation (that means the API needs to consist of interfaces), and moved the common functions to static helper methods.
Static Helper Class
Now I have something like this:public class MatrixOp {This moves the implementations of the functions to a single point, but the functions themselves still need to be present in the concrete implementation types to do the delegating call. (And when the implementation is a single line, I don't even bother with delegating.) That's still quite a bit of repeating code.
public static Matrix plus(Matrix m1, Matrix m2) {
// add the matrices, return the result
}
public static double dot(Vector v1, Vector v2) {
// ...
}
// other similar functions implementing the common operations
}
public class ArrayMatrix implements Matrix {
private double[][] data;
@Override
public int getColumns() {
return data[0].length;
}
@Override
public int getRows() {
return data.length;
}
@Override
public double get(int row, int col) {
return data[row][col];
}
@Override
public Matrix plus(Matrix m) {
return MatrixOp.plus(this, m);
}
}
public class ArrayVector extends ArrayMatrix implements Vector {
@Override
public int getDimension() {
return getRows();
}
@Override
public double getCoord(int i) {
return get(i, 0);
}
@Override
public Vector plus(Matrix m) {
return (Vector)super.plus(m);
}
@Override
public double dot(Vector v) {
return MatrixOp.dot(this, v);
}
}
You could propose just removing all the fancy operations from the main types, and making them available to the user as static functions in the helper class. While that makes sense from a design perspective, and it get rids of all code duplication, I still have to say no. Having all the functions on the matrix types themselves is really important to me. In the name of readability.
I am eagerly waiting for default methods, coming in Java 8, that will make the ideal implementation possible.