I'm familiar with a piece of Java code that fires callbacks in response to receiving certain raw events. The idea is that the raw events themselves don't have enough information to be useful to the ultimate consumers. So this piece of code transforms the data, and then fires a more meaningful callback.
Here's how it goes. The Transformer class implements a pair of interfaces. One allows clients to register themselves as callbacks, so they can listen to meaningful events. The class maintains a collection of callbacks into which it fires the transformed messages. The other interface allows Transformer to listen to the raw events. Here's some pseudo-code.
public interface Callback
{
void onEvent(String s);
}
public interface Publisher
{
void registerCallback(Callback c);
}
public interface RawListener
{
void onRawEvent(String s);
}
class Transformer implements Publisher, RawListener
{
private List<callback> allCallbacks;
public void registerCallback(Callback c)
{
this.allCallbacks.add(c);
}
public void onRawEvent(String s)
{
String transformedString = this.transform(s);
foreach (Callback c : this.allCallbacks)
{
c.onEvent(transformedString);
}
}
private String transform(String s)
// details omitted
So, clients of the Publisher interface that are interested in receiving events implement the Callback interface, then add themselves into the Transformer's collection. Some other class fires events into the RawListener interface. The idea is that the implementors of Callback don't have to know anything about the RawListener or the format of the raw events.
However, there's a difference between "don't have to know" and "shouldn't know". If the raw event format is subject to change, then we really don't want the implementors of Callback to depend on that in any way. The problem is, that once a client has their mitts on a Publisher, then they can cast it into a RawListener. Like a population of frogs expanding to fill a new niche in the ecosystem, living code will exploit that.
void cleverAndRisky(Publisher p)
{
RawListener listener = (RawListener) p;
String rawEvent = // details omitted
listener.onRawEvent(rawEvent);
}
In Don Box's excellent book, Effective COM, he offers some compelling arguments about the dangers of the QueryInterface method, which is basically a cast. (Incidentally, I think the first chapter of his book is among the finest technical writing I've ever read.) Whenever a class implements interfaces used for different purposes, one runs the risk of a client writing brittle code.
I've grown fond of using Inner Classes to attack this problem. Consider an improved Transformer implementation below. With this new approach, the cleverAndRisky method above won't ever work. This doesn't complicate the Transformer code too much, and it sets a good example of paying careful attention to what gets exposed.
class Transformer implements Publisher
{
private List<callback> allCallbacks;
public void registerCallback(Callback c)
{
allCallbacks.add(c);
}
private class RawListenerImpl implements RawListener
{
public void onRawEvent(String s)
{
String transformedString = transform(s);
foreach (Callback c : allCallbacks)
{
c.onEvent(transformedString);
}
}
}
private String transform(String s)
// details omitted
Now, I'm not saying that you have to treat users of your code like two-year-olds, who will poke their fingers into every dangerous socket you leave open. Programmers are a pretty smart bunch. But I am suggesting that the public part of an API should be given careful thought. It's a subtle point, but the public API includes what I can cast interfaces into.
Keeping track of this sort of thing sets a good example. Be mindful of what you do when you are in a leadership position. Take pride when the troops imitate you.
No comments:
Post a Comment