Search code examples
javadesign-patternsobserver-pattern

Implementing the Observer Pattern to get notified about every new Account


I am developing an application where I would like to use the observer pattern in the following way: I have 2 classes:

public abstract class Storage<V>{
    private Set<V> values;
    private String filename;

    protected Storage(String filename) throws ClassNotFoundException, IOException {
        values = new HashSet<>();
        this.filename = filename;
        load();
    }

    ...

    public boolean add(V v) throws IllegalArgumentException {
        if (values.contains(v))
            throw new IllegalArgumentException("L'elemento è già presente");
        return values.add(v);
    }

    ...
}

Repository which is a class for saving a collection of Objects. below is a subclass that implements the singleton pattern (the others are practically the same, only the specified generic type changes)

public class AccountStorage extends Storage<Account>{
    
    private static AccountStorage instance = null;

    private AccountStorage(String filename) throws ClassNotFoundException, IOException {
        super(filename);
    }
    
    public static synchronized AccountStorage getInstance() throws ClassNotFoundException, IOException {
        if (instance == null) {
            String savefile = "accounts.ob";
            instance = new AccountStorage(savefile);
        }

        return instance;
    }

after which I have a controller class (Controller for Spring MVC) which through a post request receives an Account in JSON format, deserializes it and adds it to the collection (Tremite the AccountStorage class) like this:

    @PostMapping(value = "new/user", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<String> newAccount(@RequestBody Account a) {

        synchronized (accounts) {
            try {
                accounts.add(a);
                // accounts.save()
            } catch (IllegalArgumentException e) {
                return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
            } catch (IOException e) {
                return new ResponseEntity<String>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
            }   

        }
    }

where accounts is: AccountStorage accounts = AccountStorage.getInstance();

I would like to make sure that, after each addition (or other methods that modify the collection) it is saved to file without calling the function affixed each time after the modification.

My idea is to use the Observer pattern. But I don't know which class must be an Observer and which Observable (assuming this approach is the correct solution).


Solution

  • The common practice for implementing the Observer pattern is to define an Observer interface (Listener) which will declare a general contact and each observer-implementation should provide an action which would be triggered whenever an event occurs.

    A subject maintains a collection of observers (listeners), and exposes methods which allow to add and remove (subscribe/unsubscribe) an observer. Event-related behavior resides in the subject, and when a new event happens, every subscribed observer (i.e. each observer that is currently present in the collection) will be notified.


    enter image description here


    An event to which we are going to listen to is a case when a new Account gets added into an AccountStorage. And AccountStorage would be a subject. That implies that AccountStorage should hold a reference to a collection of observers, provide a functionality to subscribe/unsubscribe and override method add() of the Storage class in order to trigger all the observers when a new account will be added.

    Why can't we add a collection of observers and all related functionality into the Storage class so that every implementation will inherit it? It's a valid question, the answer is that in such a scenario we can't be specific in regard to the nature of the event because we even don't know its type - method add(V) expects a mysterious V. Hence, the observer interface and its method would be faceless. It was the downside of the standard interfaces Observer and Observable that are deprecated since JDK version 9. Their names as well as the method-name update() tell nothing about an event that would be observed. It's only slightly better than define an interface MyInterface with a method myMethod() - no clue where you can use it and what actions should follow when myMethod() is fired.

    It's a good practice when names of observers are descriptive, so that it's clear without looking at the code what they are meant to do. And it's not only related to the Observer pattern, it is a general practice which is called a self-documenting code.

    Let's start by defining an observer interface, I'll call it listener just because AccountAddedListener sounds a bit smoothly, and it's quite common to use the terms listener and observer interchangeably.

    public interface AccountAddedListener {
        void onAccountAdded(Account account);
    }
    

    Now let's proceed with an implementation of the observer, let's say we need a notification manager:

    public class NotificationManager implements AccountAddedListener {
        
        @Override
        public void onAccountAdded(Account account) {
            // send a notification message
        }
    }
    

    Now it's time to turn the AccountStorage into a subject. It should maintain a reference collection of observers, Set is a good choice because it'll not allow to add the same observer twice (which would be pointless) and is able to add and remove elements in a constant time.

    Whenever a new account gets added, subject iterates over the collection of observers and invokes onAccountAdded() method on each of them.

    We need to define a method to add a new observer, and it's also good practice to add another one to be able to unregister the observer when it's no longer needed.

    public class AccountStorage extends Storage<Account> {
    
        private Set<AccountAddedListener> listeners = new HashSet<>(); // collection of observers
        
        @Override
        public boolean add(Account account) throws IllegalArgumentException {
            listeners.forEach(listener -> listener.onAccountAdded(account)); // notifying observers
            
            return super.add(account);
        }
        
        public boolean registerAccountAddedListener(AccountAddedListener listener) {
            return listeners.add(listener);
        }
    
        public boolean unregisterAccountAddedListener(AccountAddedListener listener) {
            return listeners.remove(listener);
        }
    
        // all other functionality of the AccountStorage 
    }