jonm.dev

The Art of Writing Software



Using Interface Hierarchies to Enforce Access Protection

Tags [ abstract data type, design patterns, inheritance hierarchy, interface, Java, refactoring ]

The “implements” relationship in Java between a class and its interface provides for abstract data types where the client of the interface can’t get at the guts of the implementation to muck around with it.

I recently encountered this where we had a user object that needed to behave differently based on different login states. We decided to use the State Design Pattern, where we would have the user object delegate to a state object for its state-specific behavior.

The way we initially set this up was to set up the User interface (one example here was that we needed to use a different user ID in different states):

public interface User {
  String getUserId();
}

public interface UserState {
  String getUserId(User u);
}

public class UserImpl implements User {
  private String emailAddress;
  private String sessionId;
  private UserState userState;
  public String getUserId() {
    return this.userState.getUserId(this);
  } 
}

Ok, great. Now, it turned out that the userId, depending on what state you were in, was either your sessionId, or it was a hash of your email address. This meant that the user states had to actually get at the emailAddress and sessionId of the UserImpl. Unfortunately, the argument of the getUserId(u) method in the UserState interface takes a User as an argument, and there’s way to get the emailAddress of a User.

So we initially went down the road of adding getEmailAddress() and getSessionId() to the User interface, but that started clogging it up with a bunch of methods that the clients of Users would never need to use.

Eventually, we settled on a revised hiearchy:

public interface User {
  String getUserId();
}

public interface InternalUser extends User {
  String getEmailAddress();
  String getSessionId();
}

public interface UserState {
  String getUserId(InternalUser iu);
}

The implementation of UserImpl didn’t actually change, but now we could still have the user object and its state interact via interfaces (side note: maybe in this case it would have been ok to pass a UserImpl as the argument of the UserState’s getUserId(u) method, because this was so simple, but I’ve gotten really used to passing arguments and dependencies as interfaces). Also, the clients of the User interface were separated from having to know how the users were being implemented.

So: lesson for the day: if your interfaces are getting too bloated with methods, one place to look for refactoring is to try to break that interface into an inheritance hierarchy to simplify things.