Course content
Decorator: Wrap a Notifier with Logging and Retries
Medium·Tagsdesign-patternsdecoratorstructuralcomposition
Problem Statement
The application uses a `Notifier` interface for sending messages:
```java
interface Notifier {
void send(String to, String message); // may throw RuntimeException on failure
}
```
`EmailNotifier implements Notifier` is the concrete leaf and is already implemented. Two cross-cutting concerns — logging and retries — need to apply to any Notifier (email, future Slack/SMS channels, mocks for testing). Implement them as separate decorator classes so they compose at wiring time:
```java
class LoggingNotifier implements Notifier {
public LoggingNotifier(Notifier wrapped);
public void send(String to, String message);
// 1. Print: "sending to <to>: <message>\n" to stdout (exact format).
// 2. Delegate to the wrapped notifier.
}
class RetryingNotifier implements Notifier {
public RetryingNotifier(Notifier wrapped, int maxAttempts);
public void send(String to, String message);
// Try wrapped.send up to maxAttempts times. If the call succeeds, return
// immediately. If it throws RuntimeException, catch it and try again up
// to (maxAttempts - 1) more times. If all attempts fail, re-throw the
// last exception.
}
```
With both decorators in place, the application can wire any chain at runtime:
```java
Notifier robust = new RetryingNotifier(new LoggingNotifier(new EmailNotifier()), 3);
robust.send("alice@x.com", "hello");
```
The validator runs three checks:
1. logging_decorator — `LoggingNotifier` prints `sending to <to>: <message>` (with a trailing newline) BEFORE delegating, then calls the wrapped notifier exactly once with the same arguments.
2. retry_decorator — `RetryingNotifier` retries on `RuntimeException` and stops as soon as one attempt succeeds; if every attempt fails, it re-throws the last exception. Both branches are tested: a flaky notifier that fails twice then succeeds completes after 3 attempts; a notifier that always fails causes `send` to throw after exactly `maxAttempts` calls.
3. composition_and_stacking — both decorators implement `Notifier`, both accept a `Notifier` (and `RetryingNotifier` an `int`) via their constructor, and stacking `RetryingNotifier(LoggingNotifier(recording))` delivers the message to the inner recording notifier.
The stub decorators throw `UnsupportedOperationException` from every method, so they fail every check until the methods are implemented.
Examples
Example 1
Input
var r = new Recording(); new LoggingNotifier(r).send("alice@x.com", "hi")Output
"stdout: \"sending to alice@x.com: hi\\n\"; r received one send(\"alice@x.com\", \"hi\")"Why
The decorator logs first, then delegates. Both effects happen on every send.
Example 2
Input
var flaky = new Flaky(failsBefore=2); new RetryingNotifier(flaky, 3).send("a", "m")Output
"no exception; flaky was called 3 times"Why
Two failures, third attempt succeeds. The retry decorator stops as soon as one attempt completes without throwing.
Example 3
Input
var always = new AlwaysFail(); new RetryingNotifier(always, 3).send("a", "m")Output
"throws RuntimeException; always was called 3 times"Why
All attempts failed. The decorator re-throws the last exception so the caller knows the operation gave up.
Constraints
- •LoggingNotifier and RetryingNotifier must implement the Notifier interface.
- •LoggingNotifier accepts a Notifier in its constructor and holds it as a field (composition).
- •RetryingNotifier accepts a Notifier and an int maxAttempts in its constructor.
- •LoggingNotifier prints exactly: "sending to <to>: <message>\n" to stdout before delegating.
- •RetryingNotifier catches RuntimeException and retries; on success it returns; on exhaustion it re-throws the last RuntimeException.
- •Neither decorator extends EmailNotifier — they wrap by composition, not inheritance.