Interpretation of an interface driven implementation of the observer pattern in Java

134 views Asked by At

I am studying the observer pattern implemented in Java and I have some doubts about how the tutorial implementation exactly works.

I know that this pattern is used in all the situation that expect the user interaction with a GUI (for example the user click on a button) or all the situation in which an event happen at an unpredictable time (like a timer, when the timer snap an event have to be handled).

I know that in the observer pattern are involved 2 different type of objects:

  1. The subject object: that is the object on which something happen at an unpredictable time (for example a button that could be clicked in any time by the user)

  2. The observer object (or listener): it seems to me that it contains the code that is performed when something change into the subject object (for example it handle the click of my button). Is it true or am I missing something? I am absolutely not true about this statement...I have the doubt that maybe the observer object have only to listen the change on the subject and perform an operation on the subject when this change happens. For example: the button (the subject) is clicked, the observer listen to it and discover this change and it performs a specific operation on the subject (perform a specific method on the subject object).

What is the correct interpretation? The observer listen the subject change and perform an operation defined into the observer or perform an operation defined into the subject?

I know that to do it I can pass a reference of the subject to the observer (I think that the operation performed is defined into the subject.) but this is a bad practice because it involve a form of tight coupling between subject and observer. I know that an interface driven solution exists but I have some difficulties figuring it out.

The tutorial make this example that represent a view that use Swing to render 2 buttons:

package com.caveofprogramming.designpatterns.demo1.view;

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;

import com.caveofprogramming.designpatterns.demo1.model.Model;

public class View extends JFrame implements ActionListener {

    private Model model;

    private JButton helloButton;        // SUBJECT OBJECT: l'oggetto su cui avviene l'evento
    private JButton goodbyeButton;      // SUBJECT OBJECT: l'oggetto su cui avviene l'evento

    public View(Model model) {
        super("MVC Demo");

        this.model = model;

        // Crea i 2 bottoni definiti sopra:
        helloButton = new JButton("Hello!");
        goodbyeButton = new JButton("Goodbye!");

        // Setta il layout manager da usare che stabilisce come sono posizionati gli elementi:
        setLayout(new GridBagLayout());

        GridBagConstraints gc = new GridBagConstraints();
        gc.anchor = GridBagConstraints.CENTER;
        gc.gridx=1;
        gc.gridy=1;
        gc.weightx=1;
        gc.weighty=1;
        gc.fill=GridBagConstraints.NONE;

        add(helloButton, gc);   // Aggiunge helloButton dentro il layout

        gc.anchor = GridBagConstraints.CENTER;
        gc.gridx=1;
        gc.gridy=2;
        gc.weightx=1;
        gc.weighty=1;
        gc.fill=GridBagConstraints.NONE;

        add(goodbyeButton, gc); // Aggiunge goodbyeButton dentro il layout

        helloButton.addActionListener(this);
        goodbyeButton.addActionListener(this);

        goodbyeButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Sorry to see you go.");
            }

        });

        setSize(600, 500);                          // Setta le dimensioni della finestra
        setDefaultCloseOperation(EXIT_ON_CLOSE);    // Setta il comportamento di cosa succede premendo il tasto X
        setVisible(true);                           // Setta la finestra come visibile
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        JButton source = (JButton)e.getSource();

        if(source == helloButton) {
            System.out.println("Hello there!");
        }
        else {
            System.out.println("Some other button.");
        }

    }   

}

In this example I have 2 buttons that represent my subject objects, these:

private JButton helloButton;        
private JButton goodbyeButton;

The View class implements an ActionListener interface (That it is the interface mentioned earlier?)

Looking inside the ActionListener.class I found:

// Method descriptor #8 (Ljava/awt/event/ActionEvent;)V
public abstract void actionPerformed(java.awt.event.ActionEvent arg0);

This is a method that has to be implemented and this is the method that is invoked when an action occurs and I have to implement it, this:

@Override
public void actionPerformed(ActionEvent e) {

    JButton source = (JButton)e.getSource();

    if(source == helloButton) {
        System.out.println("Hello there!");
    }
    else {
        System.out.println("Some other button.");
    }

}

The actionPerformed() method is implemented into the View class because it extend the ActionListener interface and it handle the click of the buttons.

Is the ActionListener interface implementation my observer object? And it means that the View* class contains the **subject objects (my buttons) but it is also a observer/listener implementation?

To add the listerner (the observer object) to a button I do:

helloButton.addActionListener(this);

and I pass this because the class that contains the observer is the same of the one where the subject objects are defined. Is it correct?

It is a best practice because I bundle together the subject with the observer into a single class and I have not tight coupling derived by a reference of the subject inside the observer?

Is it my reasoning correct or am I missing something?

1

There are 1 answers

1
GhostCat On BEST ANSWER

Let's start with the "theoretical" background. First of all, one has to understand that the observer pattern; and also how "Java" is typically providing it "to you" has been around for "quite some time". There were subtle changes later on, but I think it is fair to say that we are talking about concepts from the late nineties.

My suggestion there: do not only read about the initial form of this pattern (as outlined in the famous Gang-of-four "design principles" book); but also check out what Reactive Programming has to say about it.

So, what I am saying is: in the 2015 you can still sit down and do your Java Swing applications like one did 10 or 15 years ago. But try to not get too focused on this way of doing things; there are alternatives by now; and depending on the requirements of your application, the "old school Java GUI thing" might not be the right answer.

Then: the kind of "bundling" that you suggest in the final paragraphs is more of an "anti-pattern" than a best practice. Sure, it works nice for "very small write-once never-update"-like components.

So, yes, when you start learning about the observer pattern; it is the natural thing to start like that. But it is not an answer to avoid "close coupling" - using your approach, coupling can get so tight (aren't we talking about things that are in the same class?!) that it is almost impossible to "de-couple" them later on.

As soon as you are talking about "realistic" applications, you should avoid such bundling. It starts with the fact ... that you might want the same "action" to happen for a button click ... or when the user turns to some menu. Or other parts of your UI turn more complex; and all of a sudden, "different" actions need to happen for the same button; or the state of "this thingy here" becomes now dependent on the state of "some other thingy over there".

In other words: if your application is complex, changes are very high that the suggested approach leads towards huge, monolithic, un-maintainable spaghetti code bowls.

In other words: anything beyond "simple exercise code that doesn't need maintenance in the future" should not be built following the pattern you outlined here.

Yes, I admit that this doesn't answer the implicit question: "how can it be done better" ... as that is a tough one. One potential tip could be the JavaFx "best practices" from Oracle