Structural Patterns
By the Syncfusion DevTutorials team · Last updated
Learn adapter, decorator, facade and proxy with QuickBite examples, then adapt two map services to one interface and add a caching decorator.
When you travel to another country, your phone charger may not fit the wall socket. You do not need to change the charger or the wall socket. Instead, you use a small adapter between them.
Classes face a similar problem. QuickBite uses services and libraries from different companies. These services often use different method names and interfaces. Structural patterns provide common ways to connect classes without modifying their existing code.
In simple words: structural patterns are adapters, wrappers, and middle layers that help different classes work together smoothly.
What You Will Learn
In this lesson, you will learn four structural patterns:
- Adapter: change one interface so that it matches another.
- Decorator: add new behavior by wrapping an object without changing its code.
- Facade: provide one simple interface to hide a group of classes.
- Proxy: act as a replacement for an object and control access to it.
All four patterns use the same idea from the OOP and SOLID lessons: code works with an interface instead of directly using a concrete class. This makes it possible to place another object in between when needed.
Adapter: Make It Fit
QuickBite's code expects distances in kilometres through a DistanceService interface. However, an older map library returns distances in metres and uses a different method name.
An adapter is a small class that makes these two interfaces work together. It implements the interface that QuickBite expects and internally calls the old library.
The Example
OldMapsAdapter implements the DistanceService interface that QuickBite expects. Inside its km() method, it calls the old library's distanceInMeters() and divides by 1000.
The adapter implements the new interface and holds the old API inside
Follow one call from Indiranagar to Koramangala:
The code behind the animation:
An adapter makes an old API fit the new interface
// The interface QuickBite's code expects.
interface DistanceService {
km(fromArea: string, toArea: string): number;
}
// An old library we cannot change. It speaks metres, with a different name.
class OldMapsApi {
distanceInMeters(a: string, b: string): number {
return a === b ? 0 : 4200;
}
}
// The adapter: implements the new interface, uses the old API inside.
class OldMapsAdapter implements DistanceService {
constructor(private readonly api: OldMapsApi) {}
km(fromArea: string, toArea: string): number {
return this.api.distanceInMeters(fromArea, toArea) / 1000;
}
}
const maps: DistanceService = new OldMapsAdapter(new OldMapsApi());
console.log(maps.km("Indiranagar", "Koramangala"));
Expected output:
4.2
The old API answers 4200 metres. The adapter turns that into 4.2 kilometres.
Why It Helps
Neither side needs to be changed. If QuickBite later switches to a different map provider, you only need to create a new adapter.
Without an adapter, every caller would need to convert metres into kilometres on its own. Over time, someone could forget to do the conversion correctly.
Decorator: Wrap to Add
A decorator wraps around an object and adds extra behavior to it, similar to wrapping a gift. The decorator uses the same interface as the original object, so callers cannot tell whether they are using the original object or the decorated one.
The Example
Here, LoggedNotifier adds a log line to any notifier, without editing SmsNotifier. It has the same send() method and keeps the real notifier inside, in a field called inner. Step through one message:
Here is the same idea in code:
A decorator adds a feature without changing the class
interface Notifier {
send(to: string, text: string): string;
}
class SmsNotifier implements Notifier {
send(to: string, text: string): string { return `SMS to ${to}: ${text}`; }
}
// Same interface. Wraps another Notifier. Adds one thing: a log line.
class LoggedNotifier implements Notifier {
readonly log: string[] = [];
constructor(private readonly inner: Notifier) {}
send(to: string, text: string): string {
this.log.push(`sending to ${to}`);
return this.inner.send(to, text);
}
}
const logged = new LoggedNotifier(new SmsNotifier());
console.log(logged.send("98450 12345", "Order placed"));
console.log(logged.log);
Expected output:
SMS to 98450 12345: Order placed
[
"sending to 98450 12345"
]
The caller gets "SMS to 98450 12345: Order placed", and the wrapper's log holds one line: "sending to 98450 12345".
Stacking Decorators
Because each decorator has the same interface, you can wrap a wrapper. Each layer adds one thing. Here, Shout turns the text into capitals and WithSignature adds " - QuickBite". Watch the message pass through WithSignature(Shout(Sms)):
The code behind the animation:
Decorators stack, like layers of wrapping
interface Notifier {
send(text: string): string;
}
class Sms implements Notifier {
send(text: string): string { return `SMS: ${text}`; }
}
class Shout implements Notifier {
constructor(private readonly inner: Notifier) {}
send(text: string): string { return this.inner.send(text.toUpperCase()); }
}
class WithSignature implements Notifier {
constructor(private readonly inner: Notifier) {}
send(text: string): string { return this.inner.send(`${text} - QuickBite`); }
}
const plain = new Sms();
const fancy = new WithSignature(new Shout(new Sms()));
console.log(plain.send("rider is near"));
console.log(fancy.send("rider is near"));
Expected output:
SMS: rider is near
SMS: RIDER IS NEAR - QUICKBITE
| Notifier | Result for "rider is near" |
|---|---|
Sms alone | SMS: rider is near |
WithSignature(Shout(Sms)) | SMS: RIDER IS NEAR - QUICKBITE |
The innermost Sms object was never modified. The two outer layers simply added uppercase text and a signature.
Easy way to remember the difference: an adapter changes the interface (a different shape). A decorator keeps the same interface and adds extra behavior. A proxy also keeps the same interface, but controls access to the object.
Facade: One Front Desk
When you stay at a hotel, you do not contact housekeeping, the kitchen, and the laundry separately. Instead, you call the front desk.
A facade works the same way in programming. It provides one simple method that callers use, while it communicates with multiple classes behind the scenes.
The Example
The checkout page only calls placeOrder(). It does not need to know about four separate classes: Menu, Payments, Kitchen and Riders. Step through one dosa order:
Here is the same idea in code:
A facade hides many steps behind one method
class Menu { priceOf(dish: string): number { return dish === "Dosa" ? 120 : 80; } }
class Payments { charge(amount: number): string { return `charged ${amount}`; } }
class Kitchen { cook(dish: string): string { return `cooking ${dish}`; } }
class Riders { assign(): string { return "rider Ravi assigned"; } }
// The facade: one simple method for the checkout page.
class OrderFacade {
private readonly menu = new Menu();
private readonly payments = new Payments();
private readonly kitchen = new Kitchen();
private readonly riders = new Riders();
placeOrder(dish: string): string[] {
const price = this.menu.priceOf(dish) + 30;
return [this.payments.charge(price), this.kitchen.cook(dish), this.riders.assign()];
}
}
for (const step of new OrderFacade().placeOrder("Dosa")) console.log(step);
Expected output:
charged 150
cooking Dosa
rider Ravi assigned
The facade returns three lines: "charged 150", "cooking Dosa" and "rider Ravi assigned". The 150 is the dosa price of 120 plus the delivery fee of 30.
Why It Helps
If the order steps change, only the facade changes. The checkout page, the mobile app and the admin tool all keep calling placeOrder().
Proxy: A Guard at the Door
A proxy stands in front of one object and has the same interface. It decides whether, or when, to pass a call through.
Checking Access
AdminOnlyReports has the same dailySales() method as the real SalesReports. Before passing the call on, it checks the caller's role. Watch an admin and a guest ask for the same report:
The code behind the animation:
A proxy stands in front and checks who is asking
interface Reports {
dailySales(): string;
}
class SalesReports implements Reports {
dailySales(): string { return "Today: 1,240 orders"; }
}
// Same interface. Decides whether to let the call through.
class AdminOnlyReports implements Reports {
constructor(private readonly real: Reports, private readonly role: string) {}
dailySales(): string {
if (this.role !== "admin") return "Access denied";
return this.real.dailySales();
}
}
const real = new SalesReports();
console.log(new AdminOnlyReports(real, "admin").dailySales());
console.log(new AdminOnlyReports(real, "guest").dailySales());
Expected output:
Today: 1,240 orders
Access denied
The admin sees "Today: 1,240 orders". The guest sees "Access denied".
Creating Only When Needed
A proxy can also delay creating something expensive until it is really used. This is called a lazy proxy.
LazyMenuImages stands in for MenuImages, which loads 300 images when it is created. The proxy waits until someone asks for an image. Step through a page that opens and then asks for the first image twice:
Here is the same idea in code:
A lazy proxy: create the expensive object only when needed
class MenuImages {
constructor() { console.log("loading 300 images..."); }
first(): string { return "dosa.jpg"; }
}
class LazyMenuImages {
private real: MenuImages | null = null;
first(): string {
if (this.real === null) this.real = new MenuImages(); // made on first use
return this.real.first();
}
}
const images = new LazyMenuImages();
console.log("page opened");
console.log(images.first());
console.log(images.first());
Expected output:
page opened
loading 300 images...
dosa.jpg
dosa.jpg
The order of events is: "page opened", then "loading 300 images...", then "dosa.jpg" twice. "loading 300 images..." happens only once, and only when the first image is requested.
The chart puts all four patterns side by side. Look at what sits in the middle and whether the caller sees the same interface as the object behind it:
Adapter and facade give the caller a different interface. Decorator and proxy keep the same one.
Add a wrapper only when you need it, because each one is another class to understand. Use an adapter, decorator, facade or proxy for a real need: a different API, extra behavior, hiding steps or controlling access.
Lab: Two Map Services and a Cache
QuickBite uses two different map services. One service returns distances in metres, while the other returns a route object with distances in kilometres. Both services are already provided in the code. Do not modify them.
Predict the Output
Here is a decorator example in C#. Read the code and try to predict the output before running it.
Common Mistakes
| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| Converting in every caller | api.distanceInMeters(...) / 1000 in multiple files. | Create one adapter that handles the conversion. | Adding the conversion directly often seems like the fastest solution. |
| Editing a class to add a feature | Adding caching logic inside GeoClient. | Use a CachedDistance decorator around it. | Modifying an existing class can feel easier than creating a wrapper. |
| Decorator with a different interface | The wrapper uses sendWithLog() instead of send(). | The wrapper should keep the same send() method. | Renaming methods may seem clearer at first. |
| A facade that does too much | OrderFacade contains fifty different methods. | Keep only a few simple methods and split responsibilities when needed. | New features often get added to the facade over time. |
| Incomplete cache key | The key uses only the starting location. | The key should include all values that affect the result. | Early tests may use only one route, hiding the problem. |
Practice
What to Remember
- Adapter: changes the interface so a class can work with the interface you need.
- Decorator: keeps the same interface and adds extra behavior. Multiple decorators can be stacked together.
- Facade: provides one simple entry point for working with multiple classes.
- Proxy: keeps the same interface and controls access to an object or delays its creation.
- All four patterns work because code interacts with interfaces instead of directly depending on concrete classes.
Further Reading
These are the sources used to check this lesson:
- Refactoring.Guru: Adapter: the adapter pattern with diagrams.
- Refactoring.Guru: Decorator: wrapping to add behaviour.
- Refactoring.Guru: Facade: one front door for a subsystem.
- Refactoring.Guru: Proxy: access control and lazy loading.
Next: Behavioural Patterns. You will learn how objects share work: strategy, observer, state, command and chain of responsibility.