UNC COMP 301 - F26/Lec 12 - Adapter Design Pattern; Composition over Inheritance; Decorator DP
Watch on YouTube →
Overview
Muhammad Sayeed Ghani explains how the Adapter pattern makes incompatible APIs work together and how the Decorator pattern adds features through composition rather than inheritance. Media-player adapters, dog and window feature combinations, and discounted price tags show how interface-based delegation avoids changing legacy code and limits subclass explosions; Java I/O streams provide a practical example of chained decorators.
Key takeaways
- An Adapter implements the interface a client expects, holds a reference to an incompatible adaptee, and delegates or translates calls without modifying legacy or third-party code.
- Inheritance scales poorly when independent features must be combined: three binary window features already require up to 2^3 = 8 subclasses.
- A Decorator implements the same interface as the object it wraps, enabling runtime feature combinations while requiring it to reimplement every interface method.
- Decorator methods can delegate unchanged operations and modify selected results; a border decorator adjusts window width, while a price decorator subtracts discounts from getAmount.
- Nested decorators form a chain of object references, so two $20 discounts applied to a $100 price tag yield $60 without copying the underlying object.
- Decorator ordering and encapsulation create real constraints: features may depend on a particular wrapper order, and wrapped private or protected state must be exposed through methods.
Chapters
- Structural patterns describe how classes and objects are composed into larger structures, unlike creational patterns or behavioral patterns such as Iterator.
- The Adapter pattern converts a class interface into one expected by a client, allowing otherwise incompatible classes to work together.
- A physical travel adapter illustrates the idea: it can change plug shape and, in some cases, voltage.
- A client expects a MediaPlayer target interface with a play(fileName) method, while a legacy player exposes a format-specific method such as playVLCFile.
- The three possible fixes are changing the legacy code, changing the client, or inserting an adapter between them.
- Editing working legacy code risks introducing bugs into a large codebase and can disrupt other software that already depends on it.
- Changing the client for each format creates hard-coded conditionals and tight coupling as support expands to formats such as VLC and MP4.
- The client is the software making the request, and the target is the interface the client expects to use.
- The adaptee is the existing class with the incompatible interface; the adapter sits between it and the client.
- For the media example, the client calls the target’s play method even though the VLC adaptee exposes playVLCFile.
- The adapter implements the target interface so the client can use it through the expected MediaPlayer type.
- It encapsulates a reference to the adaptee, typically passed into its constructor; this example uses aggregation rather than owning the legacy object’s lifecycle.
- The adapter delegates the target operation to the corresponding adaptee method, translating inputs if their formats differ.
- In UML, implementation uses a dashed line with a hollow triangle, while aggregation uses a hollow diamond.
- The main method creates a legacy VLC player, passes it to a VLC adapter, and gives the adapter to client code as a MediaPlayer.
- Calling play on the client-facing adapter delegates execution to playVLCFile, where the legacy player performs the work.
- A separate MP4 adapter implements the same target interface while encapsulating an MP4 adaptee and delegating to playMP4File.
- The client can use the same play call for both formats, producing output for the VLC and MP4 players without format-specific client logic.
- Adapters are useful when interfaces are incompatible and the client or adaptee cannot reasonably be changed; if either can be changed directly, an adapter may be unnecessary.
- A new company can still need adapters when integrating with software outside its control, including third-party libraries and external APIs.
- Payment integrations illustrate the need: PayPal, Stripe, Visa, Mastercard, and American Express can offer different interfaces for equivalent payment tasks.
- The adapter changes how a class is accessed without rewriting the existing class; code fully controlled by one organization can instead expose a suitable interface directly.
- Dog size and hair length are independent features: a small dog can have long hair, and a large dog can have short hair.
- Modeling every feature combination as a subclass causes class growth; with m options across n features, the simplified total is m^n.
- The dog example’s abstract superclass defines getFoodAmount and getGroomingNeeds, but combinations such as small/short-haired and large/long-haired require separate subclasses.
- Even a two-option, two-feature version requires four subclasses, illustrating why inheritance is a poor fit without a true hierarchy.
- A state-based Dog class stores size and hair length as fields, such as enums passed through its constructor, and computes food and grooming needs at runtime.
- One configurable class can represent combinations that otherwise require multiple statically defined subclasses.
- Composition over inheritance is an umbrella idea in which objects can be combined through composition or aggregation instead of encoding every variation in subclasses.
- Inheritance remains appropriate for a genuine hierarchy, such as Animal → Mammal → Dog → Poodle.
- A graphical window may need independent features such as a border, scrolling, and a close control, including combinations of those features.
- A plain Window implementation can be extended separately for a bordered window or a scrollable window, but Java does not allow a class to inherit from both feature subclasses.
- Re-inheriting from the base window to combine features means reimplementing feature code, undermining the intended reuse.
- Three yes/no features—border, scrolling, and closability—produce 2^3 = 8 possible combinations before accounting for additional variations.
- The Decorator pattern wraps a base Window implementation and adds features in separate decorator classes instead of creating a subclass for every combination.
- A bordered-window decorator implements the Window interface, stores a reference to a base Window, and adds a border-thickness field.
- getWidth delegates to the wrapped window and adds twice the border thickness when the border appears on both sides.
- setWidth receives the decorated window’s total width and subtracts both borders before delegating the inner width to the base window.
- The decorator’s field and constructor use the Window interface, so the wrapper can contain any implementation of Window rather than depending on one concrete class.
- UML represents the decorator’s implementation of the interface with a dashed hollow-triangle arrow and its aggregated Window reference with a hollow diamond.
- Because a decorator uses composition rather than inheritance, it must implement every method in the interface even when many methods simply delegate.
- Wrapping a 500-unit base window with 10-unit borders produces a total width of 520; setting the decorated width to 500 makes the inner width 480.
- The PriceTag interface defines setAmount and getAmount, and a base implementation stores the original amount.
- A discounted-price decorator implements PriceTag, encapsulates another PriceTag, and stores its own discount instead of duplicating the base amount field.
- setAmount delegates directly to the wrapped tag, while getAmount retrieves the base amount, subtracts the discount, and prevents a negative result.
- A call to getAmount on the decorator first reaches the wrapper, delegates to the underlying tag, then returns the modified value.
- Decorators can be nested: a $100 price tag wrapped by two $20 discount decorators returns $60.
- Each wrapper holds a reference to the previous object rather than copying it, forming a linked chain from the outer decorator to the base tag.
- An unwrap method can expose the encapsulated tag when code needs access to the underlying object.
- If unwrap exists only on a concrete decorator and not on the PriceTag interface, code holding a PriceTag reference must downcast before calling it.
- An extended interface such as TextPriceTag can add methods for tax amount, tax rate, or the pre-tax price; Java uses extends for interface inheritance.
- The extended interface can have its own implementation and decorators, alongside decorators for the original PriceTag interface.
- Java I/O uses decorator-style wrappers around streams and readers, including BufferedInputStream and PushbackInputStream.
- InputStream sources can include files or piped output from another program, while wrappers add buffering or the ability to push input back.
- Decorator order can matter: a window close control may depend on a border being present to contain its clickable icon.
- A decorator may be incompatible with the object or features already wrapped, so combinations must be managed deliberately.
- Without inheritance, a decorator cannot access a wrapped class’s protected fields directly; access requires methods such as getters or an intentionally broader visibility level.
- Despite these trade-offs, decorators avoid feature-combination subclass growth while allowing behavior to be layered at runtime.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Muhammad Sayeed Ghani.