Java Tutorial

Lesson 2: Decorator Usage

Object decorators, Java I/O decorators, functional wrappers, wrapper order, side effects, blocklists, truncation, counting, and composition.

Run

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

Function Pages

51 pages generated for Java functions in this lesson.

View related functions

Lesson README

Lesson 2: Decorator Usage

This lesson focuses on adding behavior by wrapping objects rather than modifying their concrete classes.

120-Minute Plan

MinutesActivity
0-10Compare decorators with middleware, higher-order functions, and mixins from other languages.
10-30Run scripts/run.sh lesson_2 demo and inspect output order.
30-50Trace the custom text decorators and the Java I/O decorator example.
50-70Discuss why wrapper order changes behavior.
70-100Implement notification decorators in NotificationDecorators.
100-115Apply answers and compare object decorators with lambda decorators.
115-120Identify when a decorator is too implicit and should become an explicit pipeline step.

Exercise

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:

The 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.

Learner Workbook

Learner Workbook: Lesson 2 Decorator Usage

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.

What You Should Understand By The End

0-15 Minutes: Run And Predict

Run:

scripts/reset_exercises.sh lesson_2
scripts/run.sh lesson_2 demo
scripts/run.sh lesson_2 exercise

Before reading the code, predict:

15-35 Minutes: Trace The Demo Stack

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.

35-50 Minutes: Read The Exercise API

Open src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java.

Group functions by behavior:

For each function, answer: does it call base.send every time, sometimes, or never?

50-95 Minutes: Implement In Rounds

Round 1:

Round 2:

Round 3:

Round 4:

Round 5:

Round 6:

Round 7:

Round 8:

95-110 Minutes: Verify Answers

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.

110-120 Minutes: Edge-Case Drill

Try these variations:

Reset when done:

scripts/reset_exercises.sh lesson_2

Instructor Notes

Instructor Notes: Lesson 2 Decorator Usage

Session Goal

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.

Learning Outcomes

By the end of 120 minutes, learners should be able to:

Before Class

Run:

scripts/reset_exercises.sh lesson_2
scripts/run.sh lesson_2 demo
scripts/run.sh lesson_2 exercise

Know the key files:

Minute-By-Minute Plan

TimeSegmentInstructor Moves
0-10OrientationAsk where learners have seen middleware, interceptors, wrappers, or higher-order functions. Map those to decorators.
10-22Run the demoRun scripts/run.sh lesson_2 demo. Ask learners which part of the output proves wrappers are stacked.
22-38Interface and base objectOpen TextProcessor.java and PlainTextProcessor.java. Explain the common contract.
38-55Decorator classesOpen TextProcessorDecorator, TrimDecorator, LowercaseDecorator, ProfanityMaskDecorator, and MetricsDecorator. Trace one input.
55-68Standard library exampleRevisit the BufferedReader -> InputStreamReader -> ByteArrayInputStream chain in DecoratorDemo. Discuss resource ownership.
68-78Wrapper orderingAsk learners to predict output if ProfanityMaskDecorator runs before LowercaseDecorator.
78-100Exercise workLearners implement the staged decorator lab from workbook.md, including transform, audit, blocklist, counting, truncation, and composition.
100-112Solution compareApply answers and compare side-effect order.
112-118Design discussionWhen should decorators become an explicit pipeline list? When are nested constructors too hard to read?
118-120RecapSummarize contract preservation, wrapper order, and side effects.

Teaching Script

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:

This gives learners a standard-library anchor for the same pattern used in the custom demo.

Exercise Facilitation

Learners edit NotificationDecorators.java.

Recommended order:

Hints to give:

Expected Answer Behavior

After applying answers:

scripts/apply_answers.sh lesson_2
scripts/run.sh lesson_2 verify

Expected important lines:

lesson_2 answers verified

Common Mistakes

Discussion Prompts

Extension Challenge

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.

Functions In This Lesson

NotificationDecorators.withPrefix

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:13 | method | answer

public static NotificationChannel withPrefix(NotificationChannel base, String prefix) {

NotificationDecorators.withSuffix

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:19 | method | answer

public static NotificationChannel withSuffix(NotificationChannel base, String suffix) {

NotificationDecorators.withMessageTransform

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:25 | method | answer

public static NotificationChannel withMessageTransform(NotificationChannel base, UnaryOperator<String> transform) {

NotificationDecorators.withRateLimitNote

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:31 | method | answer

public static NotificationChannel withRateLimitNote(NotificationChannel base) {

NotificationDecorators.withAuditTrail

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:36 | method | answer

public static NotificationChannel withAuditTrail(NotificationChannel base, List<String> auditLog) {

NotificationDecorators.withBlocklist

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:45 | method | answer

public static NotificationChannel withBlocklist(NotificationChannel base, Set<String> blockedUserIds) {

NotificationDecorators.withTruncation

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:56 | method | answer

public static NotificationChannel withTruncation(NotificationChannel base, int maxChars) {

NotificationDecorators.withCounting

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:67 | method | answer

public static NotificationChannel withCounting(NotificationChannel base, AtomicInteger attempts) {

NotificationDecorators.compose

lesson_2/answers/java/tutorial/lesson2/exercise/NotificationDecorators.java:76 | method | answer

public static NotificationChannel compose( NotificationChannel base, List<UnaryOperator<NotificationChannel>> decorators) {

DecoratorDemo.DecoratorDemo

lesson_2/src/demo/java/tutorial/lesson2/demo/DecoratorDemo.java:10 | constructor | demo

private DecoratorDemo() {

DecoratorDemo.main

lesson_2/src/demo/java/tutorial/lesson2/demo/DecoratorDemo.java:13 | method | demo

public static void main(String[] args) throws IOException {

LowercaseDecorator.process

lesson_2/src/demo/java/tutorial/lesson2/demo/LowercaseDecorator.java:11 | method | demo

public String process(String input) {

MetricsDecorator.MetricsDecorator

lesson_2/src/demo/java/tutorial/lesson2/demo/MetricsDecorator.java:8 | constructor | demo

public MetricsDecorator(TextProcessor next) {

MetricsDecorator.process

lesson_2/src/demo/java/tutorial/lesson2/demo/MetricsDecorator.java:13 | method | demo

public String process(String input) {

PlainTextProcessor.process

lesson_2/src/demo/java/tutorial/lesson2/demo/PlainTextProcessor.java:5 | method | demo

public String process(String input) {

ProfanityMaskDecorator.process

lesson_2/src/demo/java/tutorial/lesson2/demo/ProfanityMaskDecorator.java:9 | method | demo

public String process(String input) {

TextProcessor.process

lesson_2/src/demo/java/tutorial/lesson2/demo/TextProcessor.java:5 | interface method | demo

String process(String input);

TrimDecorator.TrimDecorator

lesson_2/src/demo/java/tutorial/lesson2/demo/TrimDecorator.java:4 | constructor | demo

public TrimDecorator(TextProcessor next) {

TrimDecorator.process

lesson_2/src/demo/java/tutorial/lesson2/demo/TrimDecorator.java:9 | method | demo

public String process(String input) {

AnswerVerifier.AnswerVerifier

lesson_2/src/exercise/java/tutorial/lesson2/exercise/AnswerVerifier.java:10 | constructor | exercise

private AnswerVerifier() {

AnswerVerifier.main

lesson_2/src/exercise/java/tutorial/lesson2/exercise/AnswerVerifier.java:13 | method | exercise

public static void main(String[] args) {

AnswerVerifier.expectEquals

lesson_2/src/exercise/java/tutorial/lesson2/exercise/AnswerVerifier.java:68 | method | exercise

private static void expectEquals(Object expected, Object actual, String label) {

AnswerVerifier.expectThrows

lesson_2/src/exercise/java/tutorial/lesson2/exercise/AnswerVerifier.java:74 | method | exercise

private static void expectThrows(Runnable action, String label) {

EmailChannel.send

lesson_2/src/exercise/java/tutorial/lesson2/exercise/EmailChannel.java:5 | method | exercise

public String send(String userId, String message) {

ExerciseRunner.ExerciseRunner

lesson_2/src/exercise/java/tutorial/lesson2/exercise/ExerciseRunner.java:10 | constructor | exercise

private ExerciseRunner() {

ExerciseRunner.main

lesson_2/src/exercise/java/tutorial/lesson2/exercise/ExerciseRunner.java:13 | method | exercise

public static void main(String[] args) {

NotificationChannel.send

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationChannel.java:5 | interface method | exercise

String send(String userId, String message);

NotificationDecorators.withPrefix

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:12 | method | exercise

public static NotificationChannel withPrefix(NotificationChannel base, String prefix) {

NotificationDecorators.withSuffix

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:18 | method | exercise

public static NotificationChannel withSuffix(NotificationChannel base, String suffix) {

NotificationDecorators.withMessageTransform

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:23 | method | exercise

public static NotificationChannel withMessageTransform(NotificationChannel base, UnaryOperator<String> transform) {

NotificationDecorators.withRateLimitNote

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:28 | method | exercise

public static NotificationChannel withRateLimitNote(NotificationChannel base) {

NotificationDecorators.withAuditTrail

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:33 | method | exercise

public static NotificationChannel withAuditTrail(NotificationChannel base, List<String> auditLog) {

NotificationDecorators.withBlocklist

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:38 | method | exercise

public static NotificationChannel withBlocklist(NotificationChannel base, Set<String> blockedUserIds) {

NotificationDecorators.withTruncation

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:43 | method | exercise

public static NotificationChannel withTruncation(NotificationChannel base, int maxChars) {

NotificationDecorators.withCounting

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:48 | method | exercise

public static NotificationChannel withCounting(NotificationChannel base, AtomicInteger attempts) {

NotificationDecorators.compose

lesson_2/src/exercise/java/tutorial/lesson2/exercise/NotificationDecorators.java:53 | method | exercise

public static NotificationChannel compose( NotificationChannel base, List<UnaryOperator<NotificationChannel>> decorators) {

NotificationDecorators.withPrefix

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:12 | method | stub

public static NotificationChannel withPrefix(NotificationChannel base, String prefix) {

NotificationDecorators.withSuffix

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:18 | method | stub

public static NotificationChannel withSuffix(NotificationChannel base, String suffix) {

NotificationDecorators.withMessageTransform

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:23 | method | stub

public static NotificationChannel withMessageTransform(NotificationChannel base, UnaryOperator<String> transform) {

NotificationDecorators.withRateLimitNote

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:28 | method | stub

public static NotificationChannel withRateLimitNote(NotificationChannel base) {

NotificationDecorators.withAuditTrail

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:33 | method | stub

public static NotificationChannel withAuditTrail(NotificationChannel base, List<String> auditLog) {

NotificationDecorators.withBlocklist

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:38 | method | stub

public static NotificationChannel withBlocklist(NotificationChannel base, Set<String> blockedUserIds) {

NotificationDecorators.withTruncation

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:43 | method | stub

public static NotificationChannel withTruncation(NotificationChannel base, int maxChars) {

NotificationDecorators.withCounting

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:48 | method | stub

public static NotificationChannel withCounting(NotificationChannel base, AtomicInteger attempts) {

NotificationDecorators.compose

lesson_2/stubs/java/tutorial/lesson2/exercise/NotificationDecorators.java:53 | method | stub

public static NotificationChannel compose( NotificationChannel base, List<UnaryOperator<NotificationChannel>> decorators) {