This is continuation from
Observer: Part III - Java EventListenerList.
Sorry for the delay. I ditched the original ending for this series and did not have time to write a new one until now. I was planning to finish by presenting you generic versions of the original Java Observer/Observable and teaching how to use them. But I changed my mind, since the interesting part about generic observer is not how to use it, but how to write it. And that is the main issue today.
Warning for the beginners!
If you have been reading through the previous parts and are here to learn how to use the observer pattern, the generic observer is not really the best way. You are better off by sticking to the stuff that you can find in the previous parts. While making the pattern implementation generic does solve some of it's problems, it does not change the fact that the
Observer
is a concrete class that you have to inherit. Also, you would need to take these classes into your own code base, to keep and take care for the rest of your application's life.
Experience is about knowing when it's best to keep things simple, and when it's worth to drive for a perfect solution that is often more complex. We are dealing with a simple pattern here, why not keep the solution simple too?
You have been warned. The perverts who want The Perfect Observer Pattern may continue...
If you really, really want to bring in some complex stuff to your code-base, there is even MORE complicated solution you can use for the observer pattern. In his blog,
Steven Jeuris's has solved the same problem by using dynamic proxies. The solution is beautiful and uses both generics and reflection. Just a type of thing your average maintenance person is going to loooove! Just don't use it in performance-critical places as doing things through reflection is quite slow.
Steven also promotes
PerfectJPattern that has generic implementations for several design patterns. Each pattern is nicely explained with code examples, see
the Observer, for example. I just stumbled into that, but it might be interesting to check how it does things and if it's good, maybe run it through the legality checks to see if it's published under a licence that the company I work with would allow. Most big companies cannot just allow you to bring in new third-party libraries without legality-check. The reason is that some open-source licences make their user applications open-source too.
Prerequisites
While the previous parts of this series have been beginner-level, this one is not. It's not that this would be much harder to understand, just that you need some background information. So if you are a beginner, I suggest that you go through this
Java lesson on Generics before continuing.
If you are an expert in generics, you are going to have some good laughs on my behalf.
If you just stumbled in without reading the previous parts, you might want to check the Java implementation of the
Observer
interface and an
Observable
class to go with the interface. Those are the classes that we are rewriting here.
Are you ready? Let's start.
Defining the signature
When you start designing generic classes, it's always best to begin by thinking about their signatures. In this case the question is - how many generic arguments do we need here?
Fortunately, the answer can be found in the book
Java Generics and Collections, page 137. It gives us the following signatures:
public class Observable<S extends Observable<S, O, A>, O extends Observer<S, O, A>, A>
public interface Observer<S extends Observable<S, O, A>, O extends Observer<S, O, A>, A>
That is nice.... huh? S stands for Subject, O for Observer and A as Argument.
It does come with a warning: "
So you might wonder: could the declarations be simplified by dropping the type parameter S from Observable and the type parameter O from Observer? But this won't work, since you need S to be in scope in Observable so that it can be passed as a parameter to Observer, and you need O to be in scope in Observer so that it can be passed as a parameter to Observable".
I guess I should have read the first 137 pages also, but I was just googling around and I did not understand the warning. There was no further explanation and I did not see any obvious reason why the extended classes should be in scope. So my little caveman brain told me: "Me more smart, me does this better, more simple".
What IF we try to do this with one generic argument only? That would give us the following signatures:
public class Observable<A>
public interface Observer<S extends Observable<A>, A>
Yeah, yeah, I know it won't work, but let's just play we don't know it yet.
Trying out with one generic argument
Here is the signature for
Observer
's update method:
void update(S observable, A arg);
At the same time
Observable
would manage its
Observers
like this:
public void addObserver(final Observer<? extends Observable<A>, A> observer)
As we implement our
Observable
we will stumble into a problem in
notifyObservers(A arg)
method. We need to call the
update(S observable, A arg)
method of the
Observer
, but it does not accept our
Observable
as an argument. Why not, the definition for S was
<S extends Observable<A>>
? Typing is very strict in generics and the
Observer
wants something that
extends the
Observable
.
Observable
extends
Object
, so based on the generics rules it is not valid argument for update method. I have not yet found syntax in generics for saying:
"<? extends Something, but Something will do too>"
, and that problem actually does not go away by adding more generic arguments. So I just ended up using plain, non-typed
Observer
in
notifyObservers(A arg)
method in order to be able to pass
Observable.this
as an argument.
But this little incident draws our attention to typing. Our
Observer
accepts items that are of type
<? extends Observable<A>>
. So is it possible that we end up passing incompatible argument to update method? Actually, in this one generic argument solution, it is. Take a look at this:
public class Koala extends Observable<Boolean> {
public void setHungry(final boolean isHungry) {
setChanged();
notifyObservers(isHungry);
}
@Override
public String toString() {
return "koala";
}
}
public class Tiger extends Observable<Boolean> {
public void setAngry(final boolean isAngry) {
setChanged();
notifyObservers(isAngry);
}
@Override
public String toString() {
return "tiger";
}
}
public interface KoalaObserver extends Observer<Koala, Boolean> {
}
And here is our test case that shows the problem:
public void testMixedObservers() {
final Tiger tiger = new Tiger();
final KoalaObserver koalaObserver = new ZooKeeper();
tiger.addObserver(koalaObserver);
try {
tiger.setAngry(true);
}
catch (final ClassCastException ex) {
assertEquals(
"org.beyondhc.lessons.observer.genericwithonearg.GenericObserverTest$Tiger"
+ " cannot be cast to "
+ "org.beyondhc.lessons.observer.genericwithonearg.GenericObserverTest$Koala",
ex.getLocalizedMessage());
}
}
As you can see, we can add
KoalaObserver
to
Tiger
.
Tiger
will happily accept any
Observer
that extends
Observable<Boolean>
. As we cannot check for the correct type, we get an ugly
ClassCastException
. This is definitely to the worse from the original solution. It is clear now what the warning in the book meant - unless the
Observer
knows the
Observable
and vice versa, we cannot be sure that the user does not make similar mistakes than with the non-typed variants. Although I was not able to repeat this particular problem with a version that uses two generic arguments, I am pretty sure someone smarter than me can get it broken too. Thus, let's forget trying this with less arguments and return to the three-argument version.
Observable and Observer with 3 generic arguments
When the signatures are set, the rest of the implementation is not that hard. But the signature does look complex, so you might be wondering about the usage. And doesn't it make this thingy really non-flexible if
Observable
is so tightly coupled with its
Observer
?
It turns out that this tight coupling actually forces you to better design. First, it makes it more tempting to implement a separate interface for the
Observer:
public interface TigerObserver extends
Observer<Tiger, TigerObserver, Boolean> {
}
And secondly, as the
Observer
can implement only one interface, you are forced to composition if you want to observe more than one type of items:
public class ImprovedZooKeeper {
private final TigerObserver tigerObserver;
public ImprovedZooKeeper() {
this.tigerObserver = new TigerObserver() {
@Override
public void update(final Tiger observable, final Boolean arg) {
// Do your actions here
}
};
}
public void observerTiger(final Tiger tiger) {
tiger.addObserver(tigerObserver);
}
}
And the composition effectively hides the
update
method so that only
Tiger
may call it.
The
Observer
and
Observable
discussed in this article can be found in the
BeyondHc util package.
The broken ones with 1 and 2 generic arguments are available too. They can be found on the test side of the BeyondHc, in packages
observer.genericwithonearg
and
observer.genericwithtwoargs.