NotificationDecorators.NotificationDecorators
private NotificationDecorators() {
Object decorators, Java I/O decorators, functional wrappers, wrapper order, side effects, blocklists, truncation, counting, and composition.
scripts/run.sh lesson_2 demo
scripts/run.sh lesson_2 exercise
scripts/apply_answers.sh lesson_2
scripts/run.sh lesson_2 verify
scripts/reset_exercises.sh lesson_2
51 pages generated for Java functions in this lesson.
This lesson focuses on adding behavior by wrapping objects rather than modifying their concrete classes.
| Minutes | Activity |
|---|---|
| 0-10 | Compare decorators with middleware, higher-order functions, and mixins from other languages. |
| 10-30 | Run scripts/run.sh lesson_2 demo and inspect output order. |
| 30-50 | Trace the custom text decorators and the Java I/O decorator example. |
| 50-70 | Discuss why wrapper order changes behavior. |
| 70-100 | Implement notification decorators in NotificationDecorators. |
| 100-115 | Apply answers and compare object decorators with lambda decorators. |
| 115-120 | Identify when a decorator is too implicit and should become an explicit pipeline step. |
Use workbook.md for the full two-hour learner path. It includes call-stack tracing, wrapper-order experiments, and side-effect edge cases.
Edit src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java.
Functions to implement:
withPrefixwithSuffixwithMessageTransformwithRateLimitNotewithAuditTrailwithBlocklistwithTruncationwithCountingcomposeThe stubs return a usable channel, so the lesson compiles and runs before completion.
After applying answers, run scripts/run.sh lesson_2 verify to check prefix/suffix behavior, audit ordering, blocklist short-circuiting, truncation, counting, and decorator composition.
This workbook is designed to take about two hours. The goal is to understand wrapper behavior deeply enough that you can predict output before running code.
Run:
scripts/reset_exercises.sh lesson_2
scripts/run.sh lesson_2 demo
scripts/run.sh lesson_2 exercise
Before reading the code, predict:
Open src/demo/java/tutorial/lesson2/demo/DecoratorDemo.java.
Draw this stack on paper:
MetricsDecorator
ProfanityMaskDecorator
LowercaseDecorator
TrimDecorator
PlainTextProcessor
For the input " BUG reports become design signals ", write the value after each wrapper. Then open each decorator class and verify your trace.
Now inspect the Java I/O example:
BufferedReader
InputStreamReader
ByteArrayInputStream
Identify what behavior each layer adds.
Open src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java.
Group functions by behavior:
withPrefix, withSuffix, withMessageTransform, withTruncationwithRateLimitNotewithAuditTrail, withCountingwithBlocklistcomposeFor each function, answer: does it call base.send every time, sometimes, or never?
Round 1:
withPrefix and withSuffix.scripts/run.sh lesson_2 exercise.Round 2:
withMessageTransform.String::trim or message -> message.toUpperCase(Locale.ROOT).Round 3:
withRateLimitNote.Round 4:
withAuditTrail.Round 5:
withBlocklist.Round 6:
withTruncation.0, exact length, shorter messages, longer messages, and -1.Round 7:
withCounting.Round 8:
compose.Run:
scripts/apply_answers.sh lesson_2
scripts/run.sh lesson_2 verify
scripts/reset_exercises.sh lesson_2
Read any failed assertion as a precise edge-case description. If your code behaves differently, trace wrapper order rather than guessing.
Try these variations:
compose with an empty decorator list.Reset when done:
scripts/reset_exercises.sh lesson_2
Teach decorators as object composition: behavior is added by wrapping an object that satisfies the same contract. Learners should be able to trace wrapper order, implement custom decorators, recognize Java I/O decorators, and compare object decorators with functional decorators.
By the end of 120 minutes, learners should be able to:
NotificationChannel interface.Run:
scripts/reset_exercises.sh lesson_2
scripts/run.sh lesson_2 demo
scripts/run.sh lesson_2 exercise
Know the key files:
src/demo/java/tutorial/lesson2/demo/DecoratorDemo.javasrc/demo/java/tutorial/lesson2/demo/TextProcessorDecorator.javasrc/demo/java/tutorial/lesson2/demo/MetricsDecorator.javasrc/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java| Time | Segment | Instructor Moves |
|---|---|---|
| 0-10 | Orientation | Ask where learners have seen middleware, interceptors, wrappers, or higher-order functions. Map those to decorators. |
| 10-22 | Run the demo | Run scripts/run.sh lesson_2 demo. Ask learners which part of the output proves wrappers are stacked. |
| 22-38 | Interface and base object | Open TextProcessor.java and PlainTextProcessor.java. Explain the common contract. |
| 38-55 | Decorator classes | Open TextProcessorDecorator, TrimDecorator, LowercaseDecorator, ProfanityMaskDecorator, and MetricsDecorator. Trace one input. |
| 55-68 | Standard library example | Revisit the BufferedReader -> InputStreamReader -> ByteArrayInputStream chain in DecoratorDemo. Discuss resource ownership. |
| 68-78 | Wrapper ordering | Ask learners to predict output if ProfanityMaskDecorator runs before LowercaseDecorator. |
| 78-100 | Exercise work | Learners implement the staged decorator lab from workbook.md, including transform, audit, blocklist, counting, truncation, and composition. |
| 100-112 | Solution compare | Apply answers and compare side-effect order. |
| 112-118 | Design discussion | When should decorators become an explicit pipeline list? When are nested constructors too hard to read? |
| 118-120 | Recap | Summarize contract preservation, wrapper order, and side effects. |
Use this framing:
"A decorator implements the same interface as the object it wraps. That lets callers keep using the same type while behavior accumulates around the base object."
Draw this call stack:
caller
-> MetricsDecorator
-> ProfanityMaskDecorator
-> LowercaseDecorator
-> TrimDecorator
-> PlainTextProcessor
Then explain the subtlety:
"The call enters from the outside, but each decorator chooses whether to transform before delegation, after delegation, or both. That is why order matters."
For the Java I/O example:
ByteArrayInputStream supplies bytes.InputStreamReader decodes bytes into characters.BufferedReader adds efficient line reading.This gives learners a standard-library anchor for the same pattern used in the custom demo.
Learners edit NotificationDecorators.java.
Recommended order:
withPrefix because it is pure transformation before delegation.withSuffix and withMessageTransform.withRateLimitNote because it transforms after delegation.withAuditTrail because it adds side effects before delegation.withBlocklist, withTruncation, withCounting, and compose.Hints to give:
NotificationChannel."userId unless the TODO says otherwise."After applying answers:
scripts/apply_answers.sh lesson_2
scripts/run.sh lesson_2 verify
Expected important lines:
lesson_2 answers verified
base.send(...) immediately instead of returning a new channel.userId when only the message should change.prefix or storing unnecessary state.Ask learners to write a withRedaction decorator that replaces email addresses with [redacted]. Have them decide whether redaction should happen before audit, after audit, or both. The answer depends on whether audit logs are allowed to contain sensitive data.
private NotificationDecorators() {
public static NotificationChannel withPrefix(NotificationChannel base, String prefix) {
public static NotificationChannel withSuffix(NotificationChannel base, String suffix) {
public static NotificationChannel withMessageTransform(NotificationChannel base, UnaryOperator<String> transform) {
public static NotificationChannel withRateLimitNote(NotificationChannel base) {
public static NotificationChannel withAuditTrail(NotificationChannel base, List<String> auditLog) {
public static NotificationChannel withBlocklist(NotificationChannel base, Set<String> blockedUserIds) {
public static NotificationChannel withTruncation(NotificationChannel base, int maxChars) {
public static NotificationChannel withCounting(NotificationChannel base, AtomicInteger attempts) {
public static NotificationChannel compose( NotificationChannel base, List<UnaryOperator<NotificationChannel>> decorators) {
private DecoratorDemo() {
public static void main(String[] args) throws IOException {
public LowercaseDecorator(TextProcessor next) {
public String process(String input) {
public MetricsDecorator(TextProcessor next) {
public String process(String input) {
public String process(String input) {
public ProfanityMaskDecorator(TextProcessor next) {
public String process(String input) {
String process(String input);
protected TextProcessorDecorator(TextProcessor next) {
public TrimDecorator(TextProcessor next) {
public String process(String input) {
private AnswerVerifier() {
public static void main(String[] args) {
private static void expectEquals(Object expected, Object actual, String label) {
private static void expectThrows(Runnable action, String label) {
public String send(String userId, String message) {
private ExerciseRunner() {
public static void main(String[] args) {
String send(String userId, String message);
private NotificationDecorators() {
public static NotificationChannel withPrefix(NotificationChannel base, String prefix) {
public static NotificationChannel withSuffix(NotificationChannel base, String suffix) {
public static NotificationChannel withMessageTransform(NotificationChannel base, UnaryOperator<String> transform) {
public static NotificationChannel withRateLimitNote(NotificationChannel base) {
public static NotificationChannel withAuditTrail(NotificationChannel base, List<String> auditLog) {
public static NotificationChannel withBlocklist(NotificationChannel base, Set<String> blockedUserIds) {
public static NotificationChannel withTruncation(NotificationChannel base, int maxChars) {
public static NotificationChannel withCounting(NotificationChannel base, AtomicInteger attempts) {
public static NotificationChannel compose( NotificationChannel base, List<UnaryOperator<NotificationChannel>> decorators) {
private NotificationDecorators() {
public static NotificationChannel withPrefix(NotificationChannel base, String prefix) {
public static NotificationChannel withSuffix(NotificationChannel base, String suffix) {
public static NotificationChannel withMessageTransform(NotificationChannel base, UnaryOperator<String> transform) {
public static NotificationChannel withRateLimitNote(NotificationChannel base) {
public static NotificationChannel withAuditTrail(NotificationChannel base, List<String> auditLog) {
public static NotificationChannel withBlocklist(NotificationChannel base, Set<String> blockedUserIds) {
public static NotificationChannel withTruncation(NotificationChannel base, int maxChars) {
public static NotificationChannel withCounting(NotificationChannel base, AtomicInteger attempts) {
public static NotificationChannel compose( NotificationChannel base, List<UnaryOperator<NotificationChannel>> decorators) {