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:
- Send out a welcome email to the user.
- 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();
}
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();
}
Instead, let's try to use the Observer design pattern
The data
public record UserCreated(String userId, String email, String firstName, String lastName) {}
The Publisher interface
public interface Publisher<T> {
void subscribe(Subscriber<T> subscriber);
void unsubscribe(Subscriber<T> subscriber);
void publish(T data);
}
The Subscriber interface
public interface Subscriber<T> {
void next(T data);
}
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));
}
}
public class UserCreatedNotifier implements Subscriber<UserCreated>{
@Override
public void next(UserCreated data) {
System.out.println("Sending email to " + data.email());
}
}
public class VerificationTokenGenerator implements Subscriber<UserCreated> {
@Override
public void next(UserCreated data) {
System.out.println("Generating verification token and sending to " + data.email());
}
}
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"));
}
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());
}
}
//same code as before
AvatarGenerator avatarGenerator = new AvatarGenerator();
userPublisher.subscribe(avatarGenerator);
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)