October 30, 2011

Observer: Part II - Degenerated Observer


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 Objects, 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