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.