DEV Community

Ishan Soni
Ishan Soni

Posted on

The Observer design pattern

The Observer design pattern is also called Publish-Subscribe pattern and is the core principle of the Reactive paradigm (PUSH in favor of PULL).

Design Considerations

There are many observers (subscribers that are interested in some data) that are observing a publisher (which maintains that data). Observers register themselves to the publisher. The publisher can then push data to these observers, since it maintains a list of subscribed observers (PUSH!), whenever there is a change in the publisher’s state.

This pattern defines a one-to-many dependency between objects such that when the state of one object changes, all its dependents are notified and updated automatically.

Example: You are working on the user microservice. When a user is created, the following activities have to be performed:

  1. Send out a welcome email to the user.
  2. Generate and send a verification token to this user using which they can verify their account.

One way of structuring our code would be:

public void createUser(CreateUserCommand command) {
    //omitted - code to create and save a user
    sendWelcomeEmail();
    generateAndSendVerificationToken();
}
Enter fullscreen mode Exit fullscreen mode

This violates both the S and O of the SOLID principles. Your UserService is doing too much work and is not extensible. Let's assume, as part of the user creation process, we now need to create an Avatar for the user as well. Your code would then become:

public void createUser(CreateUserCommand command) {
    //omitted - code to create and save a user
    sendWelcomeEmail();
    generateAndSendVerificationToken();
    generateAvatar();
}
Enter fullscreen mode Exit fullscreen mode

Instead, let's try to use the Observer design pattern

The data

public record UserCreated(String userId, String email, String firstName, String lastName) {}
Enter fullscreen mode Exit fullscreen mode

The Publisher interface

public interface Publisher<T> {
    void subscribe(Subscriber<T> subscriber);
    void unsubscribe(Subscriber<T> subscriber);
    void publish(T data);
}
Enter fullscreen mode Exit fullscreen mode

The Subscriber interface

public interface Subscriber<T> {
    void next(T data);
}
Enter fullscreen mode Exit fullscreen mode

Let's create the concrete Publisher and Subscribers

public class UserPublisher implements Publisher<UserCreated> {

    private final List<Subscriber<UserCreated>> subscribers = new ArrayList<>();

    @Override
    public void subscribe(Subscriber<UserCreated> subscriber) {
        subscribers.add(subscriber);
    }

    @Override
    public void unsubscribe(Subscriber<UserCreated> subscriber) {
        subscribers.remove(subscriber);
    }

    @Override
    public void publish(UserCreated data) {
        subscribers.forEach(s -> s.next(data));
    }

}
Enter fullscreen mode Exit fullscreen mode

public class UserCreatedNotifier implements Subscriber<UserCreated>{

    @Override
    public void next(UserCreated data) {
        System.out.println("Sending email to " + data.email());
    }

}
Enter fullscreen mode Exit fullscreen mode

public class VerificationTokenGenerator implements Subscriber<UserCreated> {

    @Override
    public void next(UserCreated data) {
        System.out.println("Generating verification token and sending to " + data.email());
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's test it out

public void createUser(CreateUserCommand command) {
    //... User creation code

    UserPublisher userPublisher = new UserPublisher();

    UserCreatedNotifier userCreatedNotifier = new UserCreatedNotifier();
    VerificationTokenGenerator verificationTokenGenerator = new VerificationTokenGenerator();

    userPublisher.subscribe(userCreatedNotifier);
    userPublisher.subscribe(verificationTokenGenerator);

    userPublisher.publish(new UserCreated("user-1", "user1@gmail.com", "User", "1"));
}
Enter fullscreen mode Exit fullscreen mode

Output

Now, let's work on the Avatar generation process and see how extensible this design pattern is:

public class AvatarGenerator implements Subscriber<UserCreated> {
    @Override
    public void next(UserCreated data) {
        System.out.println("Generating avatar for user " + data.email());
    }

}
Enter fullscreen mode Exit fullscreen mode

//same code as before    
AvatarGenerator avatarGenerator = new AvatarGenerator();
userPublisher.subscribe(avatarGenerator);
Enter fullscreen mode Exit fullscreen mode

Output

Advantages

Loose Coupling: When two objects are loosely coupled, they can interact but have a very little knowledge of each other. The observer design pattern provides an object design where the publisher and subscribers are loosely coupled:
The only thing a publisher knows about a subscriber is that it implements the subscriber interface. We can add new/remove subscribers at any time since the only thing the publisher depends on is a list of objects that implement the subscriber interface.

Changes to either publisher or subscribers will not affect each other.

Top comments (0)