13

I'm big a fan of static type checking. It prevents you from making stupid mistakes like this:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

But it doesn't prevent you from making stupid mistakes like this:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

The issue comes when you are using the same type to represent to obviously different kinds of information. I was thinking a good solution to this would be extending the Integer class, just to prevent business logic errors, but not add functionality. For example:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

Is this considered good practice? Or are there unforeseen engineering problems down the road with such a restrictive design?

  • 5
    Won't a.SetAge( new Age(150) ) still compile? – John Wu Jul 3 '18 at 0:04
  • 1
    Java's int type has a fixed range. Clearly you'd like to have integers with custom ranges, e.g. Integer<18, 110>. You're looking for refinement types or dependent types, which some (non-mainstream) languages do offer. – Theodoros Chatzigiannakis Jul 3 '18 at 3:38
  • 3
    What you are talking about is great way to do design! Sadly, Java has so primitive type system that it is difficult do right. And even if you try to do it, it might result in side-effects like lower performance or lower developer productivity. – Euphoric Jul 3 '18 at 6:56
  • @JohnWu yes your example will still compile, but that is the single point of failure for that logic. Once you declare your new Age(...) object you can't incorrectly assign it to a variable of type Weight in any other places. It reduces the number of places where mistakes can happen. – J-bob Jul 6 '18 at 20:16
12

You are essentially asking for a unit system (no, not unit tests, "unit" as in "physical unit", like meters, volts etc.).

In your code Age represents time and Pounds represents mass. This leads to things like unit conversion, base units, precision, etc.


There were/are attempts to get such a thing into Java, for example:

The later two seem to be living in this github thing: https://github.com/unitsofmeasurement


C++ has units via Boost


LabView comes with a bunch of units.


There are other examples in other languages. (edits are welcome)


Is this considered good practice?

As you can see above, the more likely a language is used to handle values with units, the more natively it supports units. LabView is often used to interact with measurement devices. As such it makes sense to have such a feature in the language and using it would certainly be considered a good practice.

But in any general purpose high level language, where demand is low for such amount of rigor, it is probably unexpected.

Or are there unforeseen engineering problems down the road with such a restrictive design?

My guess would be: performance/memory. If you deal with a lot of values, the overhead of an object per value might become a problem. But as always: Premature optimisation is the root of all evil.

I think the bigger "problem" is getting people used to it, as the unit is usually implicitly defined like so:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

People will be confused when they have to pass an object as a value for something which could seemingly be desribed with a simple int, when they are not familiar with unit systems.

  • "People will be confused when they have to pass an object as a value for something which could seemingly be desribed with a simple int..." --- for which we have the Principal of Least Surprise as guidance here. Good catch. – Greg Burghardt Jul 3 '18 at 12:52
  • 1
    I think custom ranges moves this out of the realm of a units library and into the dependant types – jk. Jul 3 '18 at 13:50
2

What you seem to be looking for is known as tagged types. They are a way of saying "this is an integer which represent age" while "this is also an integer but it represents weight" and "you can't assign one to the other". Note, that this goes further than physical units such as meters or kilograms: I may have in my program "people's heights" and "distances between points on a map", both measured in meters, but not compatible with each other since assigning one to the other doesn't make sense from the business logic's perspective.

Some languages, like Scala support tagged types quite easily (see the link above). In others, you can create your own wrapper classes, but this is less convenient.

Validation, e.g. checking that a person's height is "reasonable" is another issue. You can put such code in your Adult class (constructor or setters), or inside your tagged type/wrapper classes. In a way, built-in classes such as URL or UUID fulfil such a role (among others, e.g. providing utility methods).

Whether using tagged types or wrapper classes will actually help make your code better, will depend on several factors. If your objects are simple and have few fields, the risk of assigning them wrong is low and the additional code needed to use tagged types may not be worth the effort. In complex systems with complex structures and lots of fields (especially if many of them share the same primitive type), it may actually be helpful.

In the code I write, I often create wrapper classes if I pass around maps. Types such as Map<String, String> are very opaque by themselves, so wrapping them in classes with meaningful names such as NameToAddress helps a lot. Of course, with tagged types, you could write Map<Name, Address> and not need the wrapper for the whole map.

However, for simple types such as Strings or Integers, I have found wrapper classes (in Java) to be too much of a nuisance. Regular business logic wasn't so bad, but there arised a number of problems with serializing these types to JSON, mapping them to DB objects, etc. You can write mappers and hooks for all the big frameworks (e.g. Jackson and Spring Data), but the extra work and maintenance related to this code will offset whatever you gain from using these wrappers. Of course, YMMV and in another system, the balance may be different.

6

In contrast to null's answer, defining a type for a "unit" can be beneficial if an integer isn't enough to describe the measurement. For instance, weight is often measured in multiple units within the same measurement system. Think "pounds" and "ounces" or "kilograms" and "grams".

If you need a more granular level of measurement defining a type for the unit is beneficial:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

For something like "age" I recommend calculating that at run time based on the person's birth date:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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