For some time I’ve wanted to be able to define basic contracts for interface clients in a stricter manner than simply specifying the client constraints in javadoc. The problem is that interfaces can’t provide implementation so the constraints can’t be applied in the interface but instead must be applied in the implementations. This leads to duplication of the constraint logic and make it easy to omit these checks when implementing the service. Good test design and coverage will detect and expose the problems but it would be better if we could prevent the problem in the first place.
Below is a simple service that manages a collection of users. There are a number of simple client constraints, such as not passing null or empty strings to findUser, that are captured in the javadoc. What we want to do is enforce these across all implementations.
/**
* A service to manage user accounts
*/
public interface UserService {
/**
* Locate the user.
*
* @param id the id of the user to locate. Must not be null or empty
* @return the located user or null if a user with the supplied id is not found.
*/
User findUser(String id);
/**
* Remove the user from the system. If the user does not exits the request is ignored.
*
* @param id the id of the user to remove. Must not be null or empty.
*/
void removeUser(String id);
/**
* Store a user instance. If the user already exists the record is updated, otherwise a
* new record is created.
*
* @param user the user to store. Must not be null.
*/
void storeUser(User user);
}
In-line assertions
When it comes time to implement the interface you need to be aware of the client contract constraints and implement them via assertions or run-time exceptions. I like assertions so I’ll use them to add the rules around my implementation TransientUserService. The first version will simply add these constraints directly to the implementation.
public class TransientUserService implements UserService {
private Map _users = new HashMap ();
public User findUser(String id) {
assert null != id && id.length() > 0 : “An id must be supplied”;
return _users.get(id);
}
public void storeUser(User user) {
assert null != user : “A user must be supplied”;
_users.put(user.getId(), user);
}
public void removeUser(String id) {
assert null != id && id.length() > 0 : “An id must be supplied”;
_users.remove(id);
}
}
Nothing surprising here, the assertions are diligently applied in a simple and consistent manner. When a second implementation or subclass is created the assertions are once again added, again in the third and fourth implementations and so on. I find this duplication of code undesirable so what other options exist.
Wrapper Assertions
The next implementation wraps a user service instance and enforces the assertions before delegating to a concrete implementation.
public class UserServiceAssertionWrapper implements UserService {
private final UserService service;
public UserServiceAssertionWrapper(UserService service) {
assert null != service : "the UserService must be supplied";
this.service = service;
}
public User findUser(String id) {
assert null != id && id.length() > 0 : "An id must be supplied";
return service.findUser(id);
}
public void removeUser(String id) {
assert null != id && id.length() > 0 : "An id must be supplied";
service.removeUser(id);
}
public void storeUser(User user) {
assert null != user : "A user must be supplied";
service.storeUser(user);
}
}
This approach reduces the amount of duplication as the rules are captured in a single class that applies the assertions before proceeding. It does however mean that we must remember to wrap all of our instances in the wrapper before providing them to clients. The can be made easier with using a dependency injection framework or factories but at some point it will be forgotten and the contract wont be enforced.
Injection via Aspects
Aspect orientated programming provides us with an additional solution. Until java has support for design by contract (DBC) I believe that inserting contract constraints via aspects is the cleanest and most robust approach. The idea is simple. As part of the interface create an aspected that provides pointcuts that pick out all implementations of the interface methods and advice that injects the contract constraints.
/**
* A service to manage user accounts
*/
public interface UserService {
/**
* Locate the user.
*
* @param id the id of the user to locate. Must not be null or empty
* @return the located user or null if a user with the supplied id is not found.
*/
User findUser(String id);
/**
* Remove the user from the system. If the user does not exits the request is ignored.
*
* @param id the id of the user to remove. Must not be null or empty.
*/
void removeUser(String id);
/**
* Store a user instance. If the user already exists the record is updated, otherwise a
* new record is created.
*
* @param user the user to store. Must not be null.
*/
void storeUser(User user);
static aspect UserServiceConstraints {
pointcut findUser(UserService service, String id): execution(User UserService.findUser(String))
&& target(service)
&& args(id);
pointcut removeUser(UserService service, String id): execution(void UserService.removeUser(String))
&& target(service)
&& args(id);
pointcut storeUser(UserService service, User user): execution(void UserService.storeUser(User))
&& target(service)
&& args(user);
before(UserService service, String id) : findUser(service, id) {
assert null != id && id.length() > 0 : “An id must be supplied”;
}
before(UserService service, String id) : removeUser(service, id) {
assert null != id && id.length() > 0 : “An id must be supplied”;
}
before(UserService service, User user) : storeUser(service, user) {
assert null != user : “A user must be supplied”;
}
}
}
In the above code the pre-conditions for calling UserService methods have been added via an inner static aspect. This keeps the contract and interface nice and close to each other. Implementations have the pre-conditions applied via the before advice consistently without any need to manually wrap the service instances. The service definition and constraints are both documented and coded in the same file making it easier to keep them in sync as the interface evolves. The interface and aspect can be contained in a separate library and you can use aspectj’s binary weaving at either compile or run-time (though I prefer compile time application of advice in most circumstances) to apply the advice. As a bonus if you use eclipse and the
Other Contract Assertions
Although the example above only enforces a few pre conditions, it’s not hard to add simple post conditions and invariants. For example after successfully adding a user we should be able to find that user via the service. The advice below implements the rule.
after(UserService service, User user) returning : storeUser(service, user) {
assert null != service.findUser(user.getId()): "User must be able to found after being added";
}
Final Note
With this new approach it’s easy to get carried away. There is a fine line to be carefully walked between what rules you add via contract advice and the rules checked and asserted for in unit tests. Whenever I create an interface I create an abstract unit test that validates implementations. Contract advice doesn’t remove the need for these tests.
Recent Comments