Observer Design Pattern explained in 2 minutes

Practical guide to explain Observer Design Pattern

ยท

2 min read

Play this article

Problem Statement

Imagine, you have a pool of workers and a set of jobs that need execution. How are you going to model it?

class Job {
    List<Worker> workers = new ArrayList<>();

    public void executeJob() {
        workers.forEach(worker -> worker.execute(this))
    }
}

The simplest strategy is to pass a list of workers to the Job and then invoke them one by one during execution.

The downside of this approach is that it tightly couples two different concepts - job and its execution together in the same class. It's assigning multiple responsibilities to the same class which violates SRP (Single Responsibility Principle).

Observer Design Pattern can simplify this solution!

public record Job(String jobId) {
}

public interface Observer<T> {
    void update(T data);
}

public interface Subject<T> {
    boolean addObserver(Observer<T> observer);

    boolean removeObserver(Observer<T> observer);

    void notifyObservers(T data);
}

public class JobDispatcher implements Subject<Job> {
    Set<Observer<Job>> observers = new HashSet<>();

    @Override
    public boolean addObserver(Observer<Job> observer) {
        return observers.add(observer);
    }

    @Override
    public boolean removeObserver(Observer<Job> observer) {
        return observers.remove(observer);
    }

    @Override
    public void notifyObservers(Job job) {
        observers.forEach(observer -> observer.update(job));
    }
}

public class JobExecutor implements Observer<Job> {
    UUID uuid;

    public JobExecutor() {
        uuid = UUID.randomUUID();
    }

    @Override
    public void update(Job data) {
        System.out.println(uuid + " is executing jobId " + data.jobId());
    }
}

public class Main {
    public static void main(String[] args) {
        testObserver();
    }

    public static void testObserver() {
        JobExecutor executor1 = new JobExecutor();
        JobExecutor executor2 = new JobExecutor();
        JobDispatcher jobDispatcher = new JobDispatcher();
        jobDispatcher.addObserver(executor1);
        jobDispatcher.addObserver(executor2);
        jobDispatcher.notifyObservers(new Job("job1"));

        jobDispatcher.removeObserver(executor1);
        jobDispatcher.notifyObservers(new Job("job2"));
    }
}

// Output
3139088c-07eb-4f92-92da-0fb3af53b6be is executing jobId job1
fea0bf8a-6b2e-413f-9ae9-4a286b142798 is executing jobId job1
fea0bf8a-6b2e-413f-9ae9-4a286b142798 is executing jobId job2

We have created two new interfaces, Observer and the Subject. The sole responsibility of the Observer is to act when a subject is modified. The subject keeps track of the list of observers and decides how/when to invoke them when it changes.

What's the benefit?

  • Adheres to SRP - Each class has one responsibility and is decoupled from another. The logic of execution can change without impacting the subject.

What's the drawback?

  • This design pattern is very similar to the Publisher/Consumer problem and hence it suffers from all its issues as well. In a production ready code, we would have to be agnostic of various conditions like how to deal with slow observers, backpressure propagation and task backlog management.

Did you find this article valuable?

Support Snehasish Roy by becoming a sponsor. Any amount is appreciated!

ย