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 int
s 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 int
s, 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.