Course content
Observer: Refactor OrderService Notifications
Medium·Tagsdesign-patternsobserverpub-subrefactoring
Problem Statement
The starter `OrderService` exposes `subscribe(OrderObserver)`, `unsubscribe(OrderObserver)`, and `markShipped(orderId)` but its internal notify loop is wrong. Instead of calling the uniform `observer.onOrderShipped(order)` method on each subscriber, it does an `instanceof` chain to discriminate between subscriber types and reaches into their internals to record the notification:
```java
public Order markShipped(String orderId) {
Order order = new Order(orderId);
for (OrderObserver obs : observers) {
if (obs instanceof EmailObserver) {
((EmailObserver) obs).getNotifiedOrders().add(order);
} else if (obs instanceof SmsObserver) {
((SmsObserver) obs).getNotifiedOrders().add(order);
} else if (obs instanceof AuditLogObserver) {
((AuditLogObserver) obs).getNotifiedOrders().add(order);
}
// any other observer type is silently dropped
}
return order;
}
```
This is the wrong way to use the Observer pattern. The Subject is treating each subscriber's reference as a tag ("which kind of observer is this?") instead of as a delegate ("do your thing on this order"). The `onOrderShipped` method on each observer is stubbed and never called. Adding a new observer type means editing the `instanceof` chain, which is exactly what the pattern is supposed to make unnecessary.
Refactor so that the notification logic lives on each observer and `OrderService.markShipped` becomes a uniform notify loop. After your refactor, the design must expose this exact public API:
```java
class Order {
public Order(String orderId);
public String getOrderId();
}
interface OrderObserver {
void onOrderShipped(Order order);
}
class EmailObserver implements OrderObserver {
public List<Order> getNotifiedOrders();
}
class SmsObserver implements OrderObserver {
public List<Order> getNotifiedOrders();
}
class AuditLogObserver implements OrderObserver {
public List<Order> getNotifiedOrders();
}
class OrderService {
public OrderService();
public void subscribe(OrderObserver observer);
public void unsubscribe(OrderObserver observer);
public Order markShipped(String orderId);
// creates an Order, calls observer.onOrderShipped(order) on every subscriber, returns the order.
// contains no instanceof.
// wraps each notify call so that one observer's exception does not block the others.
}
```
The `onOrderShipped` method on each concrete observer is empty in the starter. Move the recording logic (adding the order to `notifiedOrders`) into each `onOrderShipped`. Then replace the body of `markShipped` with a uniform loop that calls `observer.onOrderShipped(order)` on every subscriber, wrapped in a try/catch so a misbehaving observer does not block the others.
The validator runs ten checks:
1. `new EmailObserver().onOrderShipped(order)` records the order. The recording logic must live in the observer.
2. `new SmsObserver().onOrderShipped(order)` records the order.
3. `new AuditLogObserver().onOrderShipped(order)` records the order.
4. A subscribed observer is notified when `markShipped` is called.
5. Two subscribed observers are both notified.
6. An observer that has been unsubscribed is not notified.
7. The validator subscribes its own brand-new `OrderObserver` (an `AnalyticsObserver` it defines internally) and asserts the service notifies it. Any `instanceof` chain on subscriber types fails this check.
8. If a subscribed observer throws on notification, the other subscribers are still notified. The notify loop must be exception-isolated.
9. `OrderService.markShipped` returns the same `Order` instance the observers were notified with.
10. The observers field on `OrderService` is declared as a collection of the `OrderObserver` interface (not a concrete observer class).
The smelly starter passes only the constructor and field-type checks; it fails every behavioural check until the refactor is real.
Examples
Example 1
Input
service.subscribe(new EmailObserver()); service.markShipped("o1");Output
"EmailObserver.getNotifiedOrders() contains an Order with orderId \"o1\""Why
The Subject calls onOrderShipped on every subscriber. EmailObserver records the order in its notifiedOrders list.
Example 2
Input
service.subscribe(email); service.subscribe(sms); service.unsubscribe(email); service.markShipped("o2");Output
"sms.getNotifiedOrders() has \"o2\"; email.getNotifiedOrders() does not."Why
Unsubscribe removes the observer from the notify list. Only the still-subscribed observer is called.
Example 3
Input
service.subscribe(new SomeBrandNewObserver()); service.markShipped("o3");Output
"the new observer's onOrderShipped is called"Why
Adding a new observer type is a new class implementing OrderObserver. OrderService handles it without any code change because it depends only on the interface.
Constraints
- •Refactor must keep these classes named: Order, OrderObserver, EmailObserver, SmsObserver, AuditLogObserver, OrderService.
- •OrderObserver must remain an interface with method `void onOrderShipped(Order order)`.
- •OrderService must have a no-arg constructor and expose subscribe(OrderObserver), unsubscribe(OrderObserver), and markShipped(String).
- •OrderService.markShipped must not branch on the runtime type of any subscriber.
- •The observers field on OrderService must be declared as a collection of the OrderObserver interface (not a concrete observer class).
- •An exception thrown by one subscriber must not prevent the remaining subscribers from being notified.
Hints
Stuck? Reveal a nudge toward the right pattern, one step at a time.
Hint 1
Each observer's onOrderShipped should record the order in its notifiedOrders list. For all three observers it is a one-liner: notifiedOrders.add(order).
Hint 2
Once each observer's onOrderShipped is implemented, OrderService.markShipped becomes a uniform notify loop. Replace the instanceof chain with: for (OrderObserver obs : observers) { obs.onOrderShipped(order); }.
Hint 3
Wrap the call to obs.onOrderShipped(order) in a try/catch (RuntimeException ignored) {}. The validator subscribes a deliberately failing observer in front of the real ones; if the failure is allowed to propagate, the later observers never get notified.
Hint 4
Do not branch on the runtime type of any subscriber. The whole point of Observer is that the Subject never asks 'which kind of observer is this?'. It just calls onOrderShipped and trusts the observer to do the right thing.
Hint 5
If you keep the instanceof chain and just add new branches, you are not applying the pattern. The validator subscribes its own AnalyticsObserver class, which the chain does not know about, so any chain that lists known observer types will fall through and fail.
Hint 6
Watch out for the observers field's declared type. It must be `List<OrderObserver>` (or another collection of the interface), not `List<EmailObserver>`. The validator inspects the generic type via reflection and rejects concrete-class parameterisations.