Friday, 25 October 2013

Represent your IDs as Value Objects

I'm a big fan of creating Value Object classes for ids. It might seem over-engineering at first, so if you think so, please suspend your disbelief for a short time and let me try to show you some (3) benefits.

Let's have the following example. In your RPG game a Player can choose an Avatar for herself from a bunch. Let's assume first that you haven't followed my advice and used simply primitives (or their autoboxed versions) for ids. It's easy to imagine that the code for this use case includes an application service (or facade if you like) which does everything application services are supposed to do, and delegates to a domain service.

In the Domain all kind of checks and fiddling could ensue with passing on and on the playerId and the avatarId to various objects and methods.

//application service - orchestrates beetween domain services and objects,
//authenticates, handles transactions, ....
class PlayerAccessoriesService {
 
        public void assignAvatarToPlayer(Integer playerId, Integer avatarId) {
                authenticationCheck();
                logging();
                transactionStart();
                playerService.assignAvatarToPlayer(playerId, avatarId);
                transactionEnd();
        }      
}
//domain service
class PlayerService {
        
        public void assignAvatarToPlayer(Integer playerId, Integer avatarId) {
                avatarRepository.checkAvatarIdIsValid(avatarId);
                Player player = playerRepository.find(playerId);
                player.setAvatar(avatarId);
                playerRepository.update(player);
        }             
}
 
interface PlayerRpository {
      Player find(Integer playerId);  
}
interface AvatarRepository {
      void checkAvatarIdIsValid(Integer avatarId);  
}
So let's see the various problems with this approach!

Problem 1

Now, since both ids are Integers imagine how easy to make a small mistake and pass playerId instead of avatarId somewhere. I assure you, in a fairly complex code with many moving parts, hours of debugging can follow before you detect this mistake. Introducing id classes like


class PlayerId {
        private final Integer value;
        public PlayerId(Integer value) { this.value = value; }
        public Integer getValue() { return this.value; }
}
eliminates this problem. The new method signatures would look like

class PlayerService {
        public void assignAvatarToPlayer(PlayerId playerId, AvatarId avatarId) { ... }             
}
The difference is somewhat similar to the one between static and dynamic typing. The transformation from primitives to Value Objects happens in the Anti-corruption layer, wherever you may draw it (between application and domain layer, or outside of the application layer).

Problem 2

At some point it turns out that Integer is too limited to hold the playerIds and the decision to convert to Long is made. Without a wrapper class every single method which has it as an Integer argument must change (see the code above). It can mean a pretty large number of changes. If it's wrapped around with a Value Object, the only places we need to change are the ones when it's actually evaluated (at the boundaries of the anti-corruption layers, where it is transformed to/from primitive).

Problem 3

Let's assume the client was sloppy and sent us a Null as playerId. The code passes it around for a while before something tries to actually use it (like the database), then most probably blows up with some strange exception. If it happened deep enough (long way from the entry point, the app service), it might prove tricky to locate the source of the problem. If in our Value Object class constructor we add some validation and wrap the primitive value in it at once when it steps inside our domain, like this
class PlayerId {
        private final Integer value;
        public PlayerId(Integer value) { 
                Validate.notNull("Player Id must be non-null", value)
                this.value = value; 
        }
        public Integer getValue() { return this.value; }
}
it will blow up immediately, making the problem very easy to locate. What we actually did here is applying the Fail-fast principle.

No comments :

Post a Comment