Codebox Software

Microtypes in Java

Published:

Microtypes are very simple classes that wrap general-purpose values like integers, strings or booleans. Here's an example of a microtype that represents a player's score in a game:

class GameScore {
    private int score;

    public GameScore(int score) {
        this.score = score;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }
}

So why not just use an int variable to represent the score, rather than creating a whole new class?

Benefit 1 - Type Safety

This is probably the greatest benefit of using microtypes - they eliminate a whole class of bugs that can arise when different kinds of value are represented using the same type.

For example, the code below uses int variables to represent both game scores and days of the month, making it possible to accidentally substitute one for the other:

public void updateGameScore(int score) {
    ...
}

int value = getDayOfMonth();
...
updateGameScore(value);

this code will compile and run just fine - it is perfectly legal, but contains a nasty bug (which is further concealed by the poor choice of variable name). If we had used microtypes to represent these 2 quantities instead, the bug would have been immediately found by the compiler:

public void updateGameScore(GameScore score) {
    ...
}

DayOfMonth dayOfMonth = getDayOfMonth();
...
updateGameScore(dayOfMonth); <-- compiler error

Benefit 2 - Additional Validation

General-purpose types such as integers typically have a very broad range of possible values, usually far broader than the quantities which they are being used to represent. For example, if we use an integer to represent the day of the month, the valid values for this quantity range from 1 to 31 - only a tiny subset of all possible integer values.

By including validation code inside the constructor and mutator methods of the microtype, we can ensure that values never fall outside the valid range, for example:

class DayOfMonth {
    public DayOfMonth(int dayValue) {
        if (dayValue < 1 || dayValue > 31) {
            throw new IllegalArgumentException("Invalid dayValue: " + dayValue);
        }
    }
}

Benefit 3 - Future Proofing

If we need to make changes to the type of information held in a variable, our life will be much easier if we have used a microtype.

For example, maybe we realise part way through our game development project that we need to indicate whether a score is a high-score or not. If we have used a microtype to represent score values then the change is simple - we just add a second property to the GameScore class called isHighScore and very little other code will need to change. However, if we started off just using an int to represent the score then we have a much bigger change to make.

Benefit 4 - Convenience Methods

A microtype class provides a natural place to add convenience methods relating to that type. For example, we can add a handy toString method to the DayOfMonth microtype:

public String toString() {
    if (day == 1 || day == 21 || day == 31) {
        return day + "st";
    } else if (day == 2 || day == 22) {
        return day + "nd";
    } else if (day == 3 || day == 23) {
        return day + "rd";
    }
    return day + "th";
}

Since this code will live inside the class that is used to represent these values, it will be easy to find for other people working on the code. If we were using an int to represent these values then the code above would have to be kept somewhere else, perhaps hidden away in a DateUtils class where it would be easy to overlook, possibly resulting in multiple implementations written by different people.

Benefit 5 - Documentation

A microtype class provides the perfect place to add documentation about the values that are being represented. This documentation can be explicit, in the form of descriptive comments, or implicit in the form of code (for example the validation code inside the DayOfMonth constructor shown above clearly documents its range of possible values).

The name of a microtype class itself provides a strong hint to someone reading the code what a given value might represent - imagine trying to work out what is happening here:

int s = value;

now how about this:

GameScore s = new GameScore(value);

despite the terrible variable names it is now clear what the variable s represents.

Although microtypes provide a number of compelling benefits, there are also some costs to consider before deciding whether to use them:

Cost 1 - Class Proliferation

The main disadvantage of using microtypes is that they will increase the number of classes in your project. This can be managed to some extent through judicious use of packages, grouping related types together, and also through scope minimisation - if a microtype is only used in one part of your application then use access modifiers to hide it from other code that doesn't need to see it.

Cost 2 - Use of Operators

The built-in language operators become less useful when values are held within microtype classes. For example, if we want to compare or manipulate two values stored in int variables we can do this kind of thing:

int a, b;

if (a < b) {
    a++;
    b -= 1;
}

If we use microtypes rather than ints we can't use these built-in operators, because a and b are now object references rather than primitive values. Since Java does not allow operator overloading, we have two options:

Option 1 - Access the wrapped values

We can access the contained primitive values and compare those:

GameScore a, b;

if (a.getScore() < b.getScore()) {
    a.setScore(a.getScore() + 1);
    b.setScore(b.getScore() - 1);
}

however this code is much less readable than the version that uses ints, so a better approach is probably to implement some custom methods on the microtype class:

Option 2 - Custom methods

GameScore a, b;

if (a.isLessThan(b)) {
    a.incrementScore();
    b.decrementScore();
}

A suite of these methods could be implemented in an abstract class that is subclassed by each microtype, for example:

public abstract class IntMicrotype {
    int value;

    public void increment() {
        value++;
    }

    public boolean isLessThan(IntMicrotype other) {
        return value < other.value;
    }
    ...
}

public class GameScore extends IntMicrotype {

}

This will work quite nicely, but this is code (and therefore a maintenance overhead, and potential source of bugs) that we wouldn't have if primitive values were used.

Cost 3 - Performance

For many systems the difference in performance between using classes and primitive values will be unnoticeable, however in performance-critical applications it may cause a problem. In most cases it is best to wait until we have actually observed a performance issue before attempting to fix it, so don't let this put you off trying microtypes.

Cost 4 - Null Pointer errors

Since microtypes are objects, it is possible to cause null-pointer errors by attempting to use an uninitialised variable - something that would not happen if primitive values had been used. Although I have listed this as a 'Cost' rather than a 'Benefit', in many situations I think I would actually prefer the behaviour we get from a microtype.

Consider these two scenarios, involving an uninitialised variable a:

Scenario 1 - int variable

The int variable a automatically gets a default value of 0, so no exception is thrown and b is assigned a value of 1:

public class MyClass {
    private int a;

    public void myMethod() {
        int b = a + 1; // b gets the value '1'
    }
}

Although this sounds convenient, this is often the worst thing that can happen. We may have unintentionally failed to initialise a, but the code runs anyway without alerting us to the bug. The problems that this causes may only become apparent later, when other code attempts to use the value held in b, and it will take time to trace the problem back here, to where it happened.

Scenario 2 - Microtype variable

If we use microtypes instead of ints, we will get a runtime error:

public class MyClass {
    private GameScore a;

    public void myMethod() {
        GameScore b = new Gamescore(a.getValue()); // NullPointerException at runtime
    }
}

No-one likes exceptions, but the stack-trace for this one will be very useful, pointing directly to the bug and making it much easier to resolve.

Should you use them?

In summary, microtypes offer a number of advantages - they will probably result in fewer bugs in your code, and will make your code easier to read and maintain. To me, the costs associated with their adoption do not seem serious enough to preclude using them. Also, bear in mind that trying microtypes is relatively low-risk because they can be adopted gradually, one type at a time, and can be introduced quite painlessly into large legacy codebases.