This is continuation from Observer: Part I - How to mess up your code with it.
In the previous part, I demonstrated some problems caused by a poor implementation of the Observer Pattern. In this and the next parts I am going to use the same Koala Zoo example to demonstrate how to implement it better.
Today we will be lazy and try to avoid implementing the pattern altogether.
Do it better - Degenerate the pattern
Often what we see is not a need for the full-fledged Observer Pattern, but a possible future need for it. In those cases, just save yourself some time and trouble and do not implement the pattern yet. Prepare for the need, but leave the implementation for the future.
Let's see how the
Koala
would be implemented using that mindset:public class Koala { private KoalaObserver keeper; public void setHungry(final boolean isHungry) { if (keeper != null) { keeper.koalaUpdate(this, isHungry); } } public void setObserver(final KoalaObserver keeper) { this.keeper = keeper; } @Override public String toString() { return "koala"; } }
Instead of inheriting from the
java.util.Observable
, we now implement a very degenerated version of Observable ourselves.As we need only one Observer we implement method
setObserver
, instead of addObserver
. We do not provide method for deleting the Observer, unless we really need that. There is no change detection either. Just implement the bare minimum.But we do need the interface for the
KoalaObserver:
public interface KoalaObserver { void koalaUpdate(Koala koala, boolean isHungry); }
Note that we are now able to pass on the information with correct types. Instead of two
Object
s, we are passing the Koala
and a boolean
telling whether the Koala
is hungry. If you would need to pass more information, a good rule of thumb is to have maximum of four parameters in a method. So you might add two more parameters, like isAngry
and isSick
, but after that you should really refactor your code and implement a KoalaChangeEvent
that would contain the parameters that need to be passed.Now we are ready to implement the actual Observer:
public class ZooKeeper implements KoalaObserver, WaterPipeObserver { @Override public void koalaUpdate(final Koala koala, final boolean isHungry) { if (isHungry) { LOGGER.info("Oh, the " + koala + " is hungry. I'll go into the cage and give" + " some eucalyptus to the little fella!"); } } @Override public void pipeFixingNeeded() { LOGGER.info("Gonna fix the pipe now!"); } }Now the class declaration nicely tells us what entities the
ZooKeeper
is observing. I added the WaterPipe
from the previous posting to demonstrate how all the different updates no longer go to a common update
method. We are free to invent method names that really describe what is happening. Last, here is how you would use the classes:
public void testKoalaGetsFood() { final LogMsgCaptor logMsgCaptor = new LogMsgCaptor(LOGGER); final Koala koala = new Koala(); final ZooKeeper zooKeeper = new ZooKeeper(); koala.setObserver(zooKeeper); koala.setHungry(true); assertEquals( "The koala should get food", "Oh, the koala is hungry. I'll go into the cage and give" + "some eucalyptus to the little fella!", logMsgCaptor.getMsg()); }
If you now tried to do similar mistakes that we did in part I you would get compilation errors.
As you can see, methods are more descriptive and it is easier to see what each of the classes do just by looking at the classes themselves.
Overall, this degenerated pattern offers several advantages compared to using
java.util.Observable
and java.util.Observer
. We have less functionality to maintain. Code is easier to read and debug. We get compilation errors if we break the pattern.Pitfalls
There are two pitfalls here.
1. Skipping the interface
If you are a beginner, you might be tempted to skip the interface and just implement
setKeeper(ZooKeeper keeper)
method in the Koala
. Do not do that - do not trust the Koala
The
Koala
or any Observable is not supposed to have full access to its Observer. Eventually someone is going to add some methods to the ZooKeeper
that are too tempting for the Koala
, like tieYourShoeLacesTogether
. No Koala
would miss that one. Even if you can keep yourself from calling those methods, the future maintenance person will not hesitate. If he can do something, he will.2. Forgetting to synchronize when refactoring the code
Sometimes you do need to refactor the code later and add a list of Observers. You might be tempted to just add a
List
to hold the Observers and be done with it. Do not do that! The full-fledged Observer Pattern implementation needs synchronization.Can you really do that? You'll have to implement the Observer Pattern sooner or later!
Well, it depends on the case. I have been using this deprecated pattern for over 5 years. I can still count with my one hand fingers the times that I needed to refactor the code to use a real Observer Pattern. Usually people have great plans for the future, but they never get implemented. You should prepare for the future, but never implement things that are not really needed now.
In most cases the Observable does just fine with one Observer. Sometimes there are cases where more Observers are needed, but this degenerated pattern is easily updated by storing the observers in a list and adding
addObserver
method. That will work as long as the Observers are added during initialization and synchronization is not needed. And with the Collections.synchronizedList(List list)
you will add the synchronization in a minute, if needed.Sometimes you will have a case where the Observers are added and removed on the fly from different threads and you need to make conditional notifications and/or store different types of Observers. Those cases are rare, but sometimes do happen. For those ones, you need a more complete implementations of the pattern. Let's take a look at those in the next parts.
No comments:
Post a Comment