Skip to content
ioob.dev
Go back

Design Patterns Part 7 — Adapter, Facade, Composite: Patterns for Organizing Structure

· 10 min read
Design Pattern Series (7/8)
  1. Design Patterns Part 1 — Why Learn Patterns
  2. Design Patterns Part 2 — Strategy and State
  3. Design Patterns Part 3 — Observer and Command
  4. Design Patterns Part 4 — Template Method and Chain of Responsibility
  5. Design Patterns Part 5 — Factory Method, Abstract Factory, Builder
  6. Design Patterns Part 6 — Decorator and Proxy
  7. Design Patterns Part 7 — Adapter, Facade, Composite: Patterns for Organizing Structure
  8. Design Patterns Part 8 — Singleton, Iterator, Prototype: Frequently Used but Often Misunderstood
Table of contents

Table of contents

What It Means to Organize Structure

Most patterns covered so far focused on behavior (Behavioral) or creation (Creational). They were patterns concerned with how objects behave or how they are created. The three patterns in this part are different in nature. They are Structural patterns.

Structural patterns deal with how to compose existing classes or objects. The goal is to rearrange the relationships between classes — without changing how the code works — so that the system becomes easier to understand and modify.

Adapter inserts a translator between incompatible interfaces, Facade places a single simple window in front of a complex subsystem, and Composite lets you treat individual objects and collections of objects in the same way. All three share the common trait of organizing structure without touching existing code.

Adapter — Fitting Mismatched Puzzle Pieces

The Problem

During any project, the moment inevitably comes when you need to use an external library. The problem is that the library’s interface differs from what your system expects.

For example, your system uses a Logger interface, but the newly adopted monitoring library exposes a completely different API called MonitoringClient. You cannot modify the monitoring library’s code, and rewriting your entire system is too costly. The Adapter pattern bridges this gap.

An Adapter is a middle layer that connects incompatible interfaces. Think of a travel power adapter you plug into an electrical outlet. Just as you need a converter between a Korean plug and a European outlet, code sometimes needs an interface converter.

Object Adapter vs Class Adapter

There are two ways to implement an Adapter.

Object Adapter: Holds the target object as a field while implementing the interface the client expects. Internally delegates and translates calls to that object. Based on composition.

Class Adapter: Inherits from the target class while simultaneously implementing the goal interface. Requires multiple inheritance, so in Java/Kotlin it is only possible by combining an interface and a class.

The structure of both approaches compared in a diagram:

classDiagram
    class Target {
        <<interface>>
        +request()
    }
    class Adaptee {
        +specificRequest()
    }
    class ObjectAdapter {
        -adaptee: Adaptee
        +request()
    }
    class ClassAdapter {
        +request()
    }

    Target <|.. ObjectAdapter : implements
    ObjectAdapter --> Adaptee : delegates
    Target <|.. ClassAdapter : implements
    Adaptee <|-- ClassAdapter : extends

In practice, the object adapter is used almost exclusively. The class adapter consumes an inheritance slot, and if the target changes, the adapter class itself must be rebuilt. The object adapter only requires swapping the field, making it far more flexible.

Code Example — Integrating an External Library

Let us make the scenario concrete. Our system uses a NotificationSender interface.

interface NotificationSender {
    fun send(userId: String, message: String)
}

But the newly adopted external library provides a completely different API.

// External library — we cannot modify this
public class SlackWebhookClient {
    public void postMessage(String channel, String text, String iconEmoji) {
        // Slack API call
    }
}

Parameter names differ, method names differ, and the number of arguments differs. The Adapter bridges this gap.

class SlackNotificationAdapter(
    private val slackClient: SlackWebhookClient,
    private val channelResolver: (String) -> String
) : NotificationSender {

    override fun send(userId: String, message: String) {
        val channel = channelResolver(userId)
        slackClient.postMessage(channel, message, ":bell:")
    }
}

SlackNotificationAdapter implements the NotificationSender interface while internally delegating to SlackWebhookClient. The logic for converting userId to a Slack channel is injected externally via channelResolver. This way, even if the channel mapping logic changes, the adapter does not need modification.

The calling side does not need to know about the adapter — it only depends on NotificationSender.

class OrderService(private val notifier: NotificationSender) {

    fun completeOrder(orderId: String, userId: String) {
        // Order processing logic...
        notifier.send(userId, "Order $orderId has been completed.")
    }
}

OrderService has no idea whether it is Slack, email, or SMS. It only knows the interface.

When Adapters Are Needed

Here are the representative situations where the Adapter pattern shines.

There are also points to watch out for. If too many adapters pile up, it becomes hard to trace “where does this call actually go?” If it is just a simple name change, it is better to unify the interface without an adapter. The principle is to use them only when there is a genuine interface incompatibility.

Facade — One Simple Door in Front of Complexity

When Subsystems Get Complex

As systems grow, situations arise where you need to combine multiple classes to perform a single function. For example, placing an order requires checking inventory, requesting payment, scheduling shipping, and sending notifications. Each service is well separated, but the client code that orchestrates them becomes complex.

The Facade pattern provides a simplified interface in front of a complex subsystem. Just as a building’s facade hides its internal structure, the client only needs to look at one Facade.

flowchart LR
    C["Client"] --> F["Facade"]
    F --> S1["Inventory Service"]
    F --> S2["Payment Service"]
    F --> S3["Shipping Service"]
    F --> S4["Notification Service"]

    style F fill:#f9f,stroke:#333,stroke-width:2px

Code Example — Order Processing Facade

Individual services composing the subsystem each do their own job.

class InventoryService {
    fun reserve(productId: String, quantity: Int): Boolean {
        // Check and reserve inventory
        return true
    }
    fun release(productId: String, quantity: Int) {
        // Cancel reservation
    }
}

class PaymentService {
    fun charge(userId: String, amount: Long): String {
        // Process payment, return transaction ID
        return "txn-12345"
    }
    fun refund(transactionId: String) {
        // Process refund
    }
}

class ShippingService {
    fun schedule(orderId: String, address: String): String {
        // Schedule shipping, return tracking number
        return "track-67890"
    }
}

class NotificationService {
    fun sendOrderConfirmation(userId: String, orderId: String) {
        // Send order confirmation notification
    }
}

Combining these services directly means the client code must know all four services. It must also coordinate the call order and handle rollbacks on failure.

The Facade consolidates and hides this complexity.

class OrderFacade(
    private val inventory: InventoryService,
    private val payment: PaymentService,
    private val shipping: ShippingService,
    private val notification: NotificationService
) {
    fun placeOrder(userId: String, productId: String, quantity: Int,
                   amount: Long, address: String): OrderResult {

        // 1. Check inventory
        if (!inventory.reserve(productId, quantity)) {
            return OrderResult.OutOfStock
        }

        // 2. Process payment
        val txnId = try {
            payment.charge(userId, amount)
        } catch (e: Exception) {
            inventory.release(productId, quantity)
            return OrderResult.PaymentFailed
        }

        // 3. Schedule shipping
        val orderId = "order-${System.currentTimeMillis()}"
        val trackingNumber = shipping.schedule(orderId, address)

        // 4. Send notification
        notification.sendOrderConfirmation(userId, orderId)

        return OrderResult.Success(orderId, trackingNumber, txnId)
    }
}

sealed class OrderResult {
    data class Success(val orderId: String, val trackingNumber: String,
                       val transactionId: String) : OrderResult()
    data object OutOfStock : OrderResult()
    data object PaymentFailed : OrderResult()
}

The client just calls OrderFacade.placeOrder(). No need to worry about the sequence of inventory checks, payments, shipping, and notifications or failure handling.

val facade = OrderFacade(inventory, payment, shipping, notification)
val result = facade.placeOrder("user-1", "prod-100", 2, 39800L, "Seoul, Gangnam-gu...")

when (result) {
    is OrderResult.Success -> println("Order complete: ${result.orderId}")
    is OrderResult.OutOfStock -> println("Out of stock")
    is OrderResult.PaymentFailed -> println("Payment failed")
}

Facade’s Benefits and Overuse Warning

Facade’s greatest benefit is reduced coupling. Since the client does not directly depend on classes inside the subsystem, internal changes to the subsystem do not affect the client as long as the Facade’s interface is maintained.

Another benefit is lowering the barrier to entry. A developer newly joining a complex subsystem can quickly grasp the overall flow by looking at the Facade first.

But overuse creates problems. If a Facade starts wrapping too much, it becomes a God Object — an object that knows everything. A Facade that places orders, registers users, and compiles statistics becomes a source of complexity itself.

Principles to follow when using Facade are clear.

Difference Between Adapter and Facade

Both are “wrapping” patterns, making them easy to confuse. The key difference lies in intent.

AspectAdapterFacade
IntentInterface conversionSimplification
TargetA single class/interfaceA subsystem composed of multiple classes
New interface?Conforms to an existing interfaceDefines a new, simple interface
What the client does not knowThe actual implementation’s API shapeThe subsystem’s internal structure

Adapter converts to match an already-defined interface, while Facade takes the direction of “there is no defined interface yet, so let’s create a simple one.”

Composite — Treating Parts and Wholes the Same Way

The Tree Structure Dilemma

Think about a file system. A directory can contain files or other directories. Calculating a directory’s size requires summing all file sizes inside it, recursively entering subdirectories.

The problem arises when expressing this in code. If files and directories are completely different types, every piece of code dealing with them needs if (file) ... else if (directory) ... branching. More types mean more branches.

The Composite pattern solves this. It makes individual objects (Leaf) and composite objects (Composite) implement the same interface, so that clients can treat both uniformly without distinguishing them.

Tree Structure Diagram

Here is the Composite pattern’s structure as a class diagram.

classDiagram
    class Component {
        <<interface>>
        +getSize() Long
        +getName() String
    }
    class File {
        -name: String
        -size: Long
        +getSize() Long
        +getName() String
    }
    class Directory {
        -name: String
        -children: List~Component~
        +getSize() Long
        +getName() String
        +add(component: Component)
        +remove(component: Component)
    }

    Component <|.. File
    Component <|.. Directory
    Directory o-- Component : children

The key is that Directory holds a list of Component while also being a Component itself. This self-referential structure creates the tree.

Visualizing an actual tree structure:

flowchart TB
    ROOT["Directory: project"]
    SRC["Directory: src"]
    TEST["Directory: test"]
    MAIN["File: Main.kt\n(2KB)"]
    UTIL["File: Utils.kt\n(1KB)"]
    T1["File: MainTest.kt\n(3KB)"]

    ROOT --> SRC
    ROOT --> TEST
    SRC --> MAIN
    SRC --> UTIL
    TEST --> T1

Calling getSize() on project recursively descends through src and test, summing all file sizes. The result is 6KB.

Code Example — File System

Define the common interface. Both files and directories have a name and size.

interface FileSystemComponent {
    val name: String
    fun getSize(): Long
    fun display(indent: String = "")
}

Individual files are Leaf nodes. They cannot have children, and their size is their own.

class File(
    override val name: String,
    private val size: Long
) : FileSystemComponent {

    override fun getSize(): Long = size

    override fun display(indent: String) {
        println("${indent}📄 $name (${size}KB)")
    }
}

Directories are Composite nodes. They hold a list of children, and their size is the sum of their children’s sizes.

class Directory(
    override val name: String
) : FileSystemComponent {

    private val children = mutableListOf<FileSystemComponent>()

    fun add(component: FileSystemComponent): Directory {
        children.add(component)
        return this
    }

    fun remove(component: FileSystemComponent) {
        children.remove(component)
    }

    override fun getSize(): Long = children.sumOf { it.getSize() }

    override fun display(indent: String) {
        println("${indent}📁 $name (${getSize()}KB)")
        children.forEach { it.display("$indent  ") }
    }
}

Composing a tree from these two:

fun main() {
    val project = Directory("project").apply {
        add(Directory("src").apply {
            add(File("Main.kt", 2))
            add(File("Utils.kt", 1))
        })
        add(Directory("test").apply {
            add(File("MainTest.kt", 3))
        })
    }

    project.display()
    println("Total size: ${project.getSize()}KB")
}

The output:

📁 project (6KB)
  📁 src (3KB)
    📄 Main.kt (2KB)
    📄 Utils.kt (1KB)
  📁 test (3KB)
    📄 MainTest.kt (3KB)
Total size: 6KB

When you call project.getSize(), Directory delegates to its children’s getSize(), and if a child is also a Directory, it delegates again downward. Recursion happens naturally. The client does not need to care whether it is dealing with a file or a directory.

Composite in UI Components

The Composite pattern is also commonly seen in UI frameworks. Individual widgets like buttons and text fields, and containers like panels and layouts, all implementing the same Component interface — that is a classic Composite.

Let us build a simple UI component example.

interface UIComponent {
    fun render(depth: Int = 0)
}

class Button(private val label: String) : UIComponent {
    override fun render(depth: Int) {
        println("${"  ".repeat(depth)}[Button: $label]")
    }
}

class TextField(private val placeholder: String) : UIComponent {
    override fun render(depth: Int) {
        println("${"  ".repeat(depth)}[TextField: $placeholder]")
    }
}

class Panel(private val title: String) : UIComponent {
    private val children = mutableListOf<UIComponent>()

    fun add(component: UIComponent): Panel {
        children.add(component)
        return this
    }

    override fun render(depth: Int) {
        println("${"  ".repeat(depth)}[Panel: $title]")
        children.forEach { it.render(depth + 1) }
    }
}

Panels can contain buttons and text fields, and panels can contain other panels.

val loginForm = Panel("Login Form").apply {
    add(TextField("Email"))
    add(TextField("Password"))
    add(Panel("Button Area").apply {
        add(Button("Login"))
        add(Button("Sign Up"))
    })
}

loginForm.render()

Calling render() recursively traverses the tree, drawing the entire UI. React’s component tree, Android’s View hierarchy, and Swing’s JComponent all stand on this principle.

Points to Watch with Composite

Composite shines when the domain has a natural tree structure. File systems, UI layouts, organization charts, menu structures — the pattern fits perfectly in these cases.

There are also points to watch.

Common Ground of the Three Patterns

Adapter, Facade, and Composite are all classified as Structural patterns by the GoF (Gang of Four). The three share an interesting commonality.

They do not modify existing code. Adapter adapts an external library to our interface without modifying it. Facade places a simple window in front of a subsystem without changing its internals. Composite combines individual objects into a tree structure without changing their implementation. All create new structures by wrapping or composing existing pieces.

They reduce client complexity. With Adapter, the client does not need to know the external API’s shape. With Facade, it does not need to know the subsystem’s internal classes. With Composite, it does not need to know whether something is an individual or composite object. All three patterns work in the direction of reducing what the client must know.

Overuse backfires. If ten adapters pile up, it becomes hard to trace call paths. If a Facade grows bloated, it becomes a God Object. If a Composite goes too deep, debugging becomes painful. It is worth recalling the principle that patterns are tools, not goals.

Decision Criteria in Practice

When wondering which of the three to use, a single question finds the fork in the road.

These three patterns are not mutually exclusive. In real projects, you might use Adapters inside a Facade, or have Composite nodes communicate with external systems through a Facade. Rather than memorizing patterns in isolation, understanding what problem each is a tool for is more important.


We have now covered three patterns for organizing structure. Adapter is a translator between mismatched interfaces, Facade is a simple door in front of a complex subsystem, and Composite is a tree structure that treats individuals and wholes the same way. In the next part, we wrap up the series by covering frequently used but often misunderstood patterns — Singleton, Iterator, and Prototype.

-> Part 8: Singleton, Iterator, Prototype


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Design Patterns Part 6 — Decorator and Proxy
Next Post
Design Patterns Part 8 — Singleton, Iterator, Prototype: Frequently Used but Often Misunderstood