API Design: Observer pattern in state change or transactional communication December 27, 2009
Posted by metalickl in Java, Software Insights.Tags: api, api design, design, java, observer pattern
trackback
The challenge I’ve personally met can be boiled down to design a simple interfacing method to a web service call, for example authentication. The caveats here include network latency, response handling and possible errors(timeout, 404, IOException’s you name it). In terms of response handling, I remembered in C we had some kind of predetermined response codes (such as those found in HTTP response, 200′s, 300′s indicating success, and the 400′s/500′s indicating failure). With this design, you simply define a list of response codes (with thorough documentation), and then simply return an int that describes the response in the least verbose manner. To visualize, it becomes something like this:
// Don't do this
/** indicating success in authentication */
public static final int AUTHENTICATION_SUCCESS = 200;
...
/**
* returns response code,
* such as {@link AUTHENTICATION_SUCCESS}
*/
public int authenticate();
The advantage of abstraction is also its weakness. The response itself is converted to a number, where much of the information is lost(abstracted away) during the process. The user of this API would first having to read the source/javadoc comments to understand what are each of the response codes, then implement the logic to handle each case respectively. If there’s a bug inside the method, and a number that’s not a valid response is returned, the client code needs to handle that as well. Worst, if there’s declared exception (ie the familiar IOException), and the client needs to call several of these methods, one after the succession of the other, the client code would look something like this.
// Don't do this
// API methods
public int authenticate() throws IOException;
public int fetchBillingInfo() throws IOException;
public int payBill() throws IOException;
...
// Client code
public void run() {
try {
if (authenticate() == AUTHENTICATION_SUCCESS) {
try {
if (fetchBillingInfo() == FETCHBILL_SUCCESS) {
try {
if (payBill() == PAYBILL_SUCCESS) {
// do something
} else { // do somehting }
} catch (IOException e) { // do somehting }
} else { // do something }
} catch (IOException e) { // do something }
} else { // do soemthing }
} catch (IOException e) { // do something }
}
You see where I’m getting? Obviously no.. because the code here is unnecessarily nested, very ugly indeed. In terms of performance, try-catch blocks is also much slower to execute.
The solution to the above concerns I’ve found to be most elegant, which takes advantage of all the goodness of type safety and verboseness is to implement observer pattern. The class that wishes to access the service API first becomes registered as a service state listener. Then the API simply fires appropriate events in place of returning different responses. Look at the example below:
// API
/**
* an enumeration of service states
* <p>much clearer to read and understand by client
*/
public Enum ServiceState {
AUTHENTICATION_SUCCESS,
AUTHENTICATION_BAD_USRNAME_OR_PW,
AUTHENTICATION_PASSWORD_EXPIRED,
AUTHENTICATION_USER_BANNED, BILL_FETCHED,
BILL_UNAVAILABLE, PAYBILL_SUCCESS
}
/**
* client code must implement this interface in order to access
* the service interfacing methods and be notified of the responses.
*/
public interface ServiceStateListener { void serviceStateChanged(ServiceState ss); }
public void Session.authenticate();
public void Session.fetchBillingInfo();
public void Session.payBill();
...
// Client Code
public void run() {
session.addServiceStateListener(this);
session.authenticate();
}
public void serviceStateChanged(ServiceState ss) {
if (ss == AUTHENTICATION_SUCCESS) {
session.fetchBillingInfo();
} else if (ss == BILL_FETCHED) {
session.payBill();
} else if (ss == PAYBILL_SUCCESS) {
session.removeServiceStateListener(this);
} else if (ss == AUTHENTICATION_BAD_USRNAME_OR_PW) {
...
} else {
...
}
}
The API itself defines the different service states or responses, in a case that more service states are needed, they can be easily added to the Enum without breaking the compatibility. The service methods do not declare checked Exceptions, but fires notification to indicate service state changes. To access the API, the client code simply becomes registered as a ServiceStateListener, and then call the first service method, i.e. session.authenticate(). The work is minimum from there on, the state transition and logic is simple and is handled in serviceStateChanged(). This designs takes advantage of the goodness of type safety, the API will never notify of anything other than a Service State. It is also much more readable and organized. This is the solution I have found and implemented in my personal project.
Comments»
No comments yet — be the first.