Design Patterns Handbook - Part II
In our previous article, we explored five creational design patterns. Now, we'll delve into seven structural design patterns.
Let's get started!
Structural Design Patterns
Structural Design Patterns, as explained in “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, focus on how objects and classes are composed to create flexible and robust software systems. These patterns highlight the relationships and interactions between different components, providing a framework for creating complex structures that are decoupled and maintainable.
There are seven widely recognized structural design patterns:
Adapter Pattern
Bridge Pattern
Composite Pattern
Decorator Pattern
Facade Pattern
Flyweight Pattern
Proxy Pattern
Next, we will examine each pattern in detail, including their use cases and code examples.
The Adapter Pattern
Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate
As shown above, the adapter simply converts an incompatible interface to a compatible form without changing the client and the customer.
The wheels on the car aren't fit to go on the train track. But that doesn't mean the car should be fitted with the train wheels. Instead, you can create a carrier that runs on the train wheels, and carry the car on the carrier to move it along the train track instead.
By doing so, your front-end and back-end aren't tied to each other. You are able to decouple the front-end components from your back-end API and treat them as independent entities as you are now using the adapter to pass data around your components.
Let's say we have this back-end API response:
and then we have this React component that accepts different props:
In this example, the Notification component accepts different prop names and also expects different values on statusText
, and typeText
.
\=> To match or adapt the React component from the back-end API response, we create the adapter.
As I mentioned above, I got inspired by the builder design pattern, which is the result!
As you can see, we are now adapting the back-end API response to fit with the Notification component.
We have also added some extra logic mapping for FE purposes such as the statusText
and typeText
.
And here's how we used it with react-query
! We’re using react-query’s select method to adapt from the raw data to what we need on this screen.
This isn't limited to react-query
and can be applied to simple fetch
API calls or whatever you are using to fetch data from the back-end.
It's a flexible and organized way to handle the mapping without too much distraction on your component level.
The Adapter design pattern has always been there. I'm pretty sure you have been using it on your projects, or work but just in a different implementation.
You might have implemented it with a so-called "mapper function" which is just a simple function instead of having a class.
This implementation is something I have created that was inspired by numerous things in works and persona projects.
The bottom line is, there are many implementations to achieve this. A design pattern gives you an idea of how to design to solve a recurring problem. It doesn't limit you on how you want to design the implementation.
The Bridge Pattern
Bridge is a structural design pattern that lets you split the large class or a set of closely related classes into two separate hierarchies - abstraction and implementation - which can be developed dependently of each other.
Problem
Say you have a geometric Shape
class with a pair of subclasses: Circle
and Square
. You want to extend this class hierarchy to incorporate colors. So you plan to create Red
and Blue
shape subclasses. However, since you already have two subclasses, you need to create 4 class combinations such as BlueCircle
and RedSquare
.
Adding new shape types and colors to the hierarchy will grow it exponentially. For example, to add a triangle shape you would need to introduce 2 subclasses, one for each color. After that, adding a new color would require creating three subclasses, one for each shape type. The further we go, the worse it becomes.
Solution
This problem occurs because we are trying to extend the shape classes into two independent dimensions: by form and by color. That is a very common issue with class inheritance.
The Bridge attempts to solve this problem by switching inheritance to the object composition. What this means is that you extract one of the dimensions into a separate class hierarchy, so that the original classes will reference an object of the hierarchy, instead of having all of its state and behaviors within one class.
Following this approach, we can extract the color-related code into its own class with two subclasses: Red
and Blue
. The Shape
class then gets a reference field pointing to one of the color objects. Now the shape can delegate any color-related work to the linked color object. The reference will act as a bridge between the Shape
and Color
classes. From now on, adding new colors won't require changing the shape hierarchy, and vice versa.
What is exactly the Bridge Pattern?
The Bridge is a software design pattern approach that creates a bridge
between the two separate hierarchies and allows them to have multiple variations and evolve independently, increasing the system's flexibility.
In the big system, adding a new feature will make the number of class combinations grow exponentially, requiring more classes. Then, the bride can be a solution to deal with this problem by allowing classes to be divided into 2 hierarchies called abstraction
and implementation
:
abstraction
: a high-level interface that references aimplementation
hierarchy.implementation
: low-level implementation details.
Now, the client can work with the abstraction without being tightly coupled to a specific implementation. This pattern favors object composition over class inheritance, following the principle "prefer composition over inheritance".
Simple Typescript Example
Let's say you have to build a design system that has many components and supports different themes. You are also expecting many changes in any theme details as well as component specifications. It's time to utilize the Bridge Pattern!
Here, the Theme
interface serves as the implementation side of the Bridge Pattern. It declares the provideColorSet()
method.
// Implementation
interface Theme {
provideColorSet(): void
}
// Concrete Implementation
class LightTheme implements Theme {
provideColorSet(): void {
console.log('Provide color set using light colors...')
}
}
class DarkTheme implements Theme {
provideColorSet(): void {
console.log('Provide color set using dark colors...')
}
}
class HighContrastTheme implements Theme {
provideColorSet(): void {
console.log('Provide color set using high contrast colors...')
}
}
The three different theme classes are concrete implementations of the Theme interface (LightTheme
, DarkTheme
, and HighContrastTheme
). Each class implements the provideColorSet()
method with specific logic for providing color sets.
The Component
interface represents the abstraction
side of the Bridge Pattern. It declares the render()
method, which represents the high-level behavior that relies on the theme.
// Abstraction
interface Component {
render(): void
}
// Refined Abstraction
class Button implements Component {
private readonly theme: Theme
constructor(theme: Theme) {
this.theme = theme
}
render(): void {
this.theme.provideColorSet()
console.log('Drawing a button.')
}
}
class Dialog implements Component {
private readonly theme: Theme
constructor(theme: Theme) {
this.theme = theme
}
render(): void {
this.theme.provideColorSet()
console.log('Drawing a dialog.')
}
}
class Form implements Component {
private readonly theme: Theme
constructor(theme: Theme) {
this.theme = theme
}
render(): void {
this.theme.provideColorSet()
console.log('Drawing a form.')
}
}
The Button
, Dialog
, and Form
classes are refined abstractions that implement the Component
interface. Each refined abstraction depends on the Theme
interface, which acts as a bridge to different concrete implementations of the theme. This separation allows for flexible composition and runtime selection of themes without modifying the components themselves.
On the client side, the instance of concrete implementations of Theme
, and the instances of refined abstraction of Component
is created. Then, the concrete theme implementation is passed as a parameter to the concrete refined abstraction.
// Client Code
const lightTheme: Theme = new LightTheme()
const lightButton: Component = new Button(lightTheme)
lightButton.render()
All combinations of any element and any theme are possible, still easy to manage complexity, and facilitate future modifications in the design system.
const darkTheme: Theme = new DarkTheme()
const darkDialog: Component = new Dialog(darkTheme)
darkDialog.render()
const highContrastTheme: Theme = new HighContrastTheme()
const highContrastForm: Component = new Form(highContrastTheme)
highContrastForm.render()
Use it or Avoid it
When to use it
This is especially useful if there are multiple variations and dimensions of change and you anticipate different variations or extensions in both the abstraction
and implementation
hierarchies.
if you want the flexibility to switch implementations at runtime, this pattern can be a good choice.
When to avoid it
if you have a straightforward scenario with only one abstraction
and one corresponding implementation
, introducing the Bridge pattern is unnecessary.
Also, if abstraction
and implementation
are tightly coupled so that they are unlikely to change independently, using this pattern might introduce unnecessary complexity with clear benefits.
In short, the Bridge pattern helps to manage complexity, improve flexibility, and decouple abstractions from their implementations.
The Composite Pattern
Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects.
Problem
Using the Composite makes sense only when the core model of your app can be represented as a tree.
For example, imagine that you have two types of objects: Products
and Boxes
. A Box can contain multiple products as well as a number of small boxes. These little boxes can also hold some or even small boxes and so on.
Say you decide to create an ordering system that uses these classes. Orders could contain simple products without any wrapping, as well as boxes stuffed with products... and other boxes. How would you determine the total price of such an order?
You could try the direct approach: unwrap all the boxes, go over all the products, and then calculate the total. That could be doable in the real world, but in the program, it's not as simple as running a loop. You have to know these classes Products
and Boxes
you are going through, the nesting level of the boxes and other nasty details beforehand. All of this makes the direct approach either too awkward or even impossible.
Solution
The Composite pattern suggests that you work with Products and Boxes through a common interface that declares the method for calculating the total prices.
How would this method work? For a product, it would simply return the product's price. For a box, it would go over each item the box contains, ask its price, and return the total for this box. If one of these items were a smaller box, that box would also start going over its contents, until the price of all inner components were calculated. A box could even add some extra cost to the final price, such as packaging cost.
The greatest benefit of this approach is that you don't need to care about the concrete classes of objects that composite the tree. You don't need to know whether an object is a simple product or a sophisticated box. You can treat them all the same via the common interface. When you call a method, the objects themselves pass the request down the tree.
What is exactly the Composite Pattern?
The Composite Pattern is a design pattern in object-oriented programming that allows you to treat individual objects and groups of objects uniformly. It lets you compose objects into tree structures to represent part-whole hierarchies.
The pattern consists of 2 main components: the Component
and the Composite
. The Component
is the interface that defines the common operations that can be performed on both leaf and composite objects. The Composite is the class that implements the Component
interface and contains other Composites and/or leaf objects.
The Composite Pattern is useful when you have a hierarchy of objects and you want to treat them all the same way, regardless of whether they are individual objects or collections of objects. For example, an employee hierarchy, deeply nested menu, or file system.
One of the key benefits of the Composite Pattern is that it allows you to add new objects to the hierarchy without changing the interface of existing objects. You can also define complex hierarchies that can be navigated using simple operations.
Applying Composite Pattern to React
In React, we can use the Composite Pattern to create a hierarchy of components, where each component can be a leaf component or a composite component that can contain other components.
Let's say we want to create a menu component that can display nested items. Each item can either be a sub-menu or a leaf node with a label or a URL.
First, we will define our MenuItem
interface:
interface MenuItem {
label: string;
url?: string;
subItems?: MenuItem[];
}
Each menu item has a label, and can optionally have a url
and/or subItems
. If subItems is defined, it should be an array of MenuItem
representing the sub-menu.
Next, we will define our Menu
component:
interface MenuProps {
items: MenuItem[];
}
const Menu: React.FC<MenuProps> = ({ items }) => {
return (
<ul>
{items.map((item) => (
<MenuItemComponent key={item.label} item={item} />
))}
</ul>
);
};
The Menu component takes an array of MenuItem
as its items
prop. It maps over the items
array and renders a MenuItemComponent
for each item.
Now, we will define our MenuItemComponent
:
interface MenuItemComponentProps {
item: MenuItem;
}
const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item }) => {
const hasSubItems = !!item.subItems;
const content = hasSubItems ? (
<Menu items={item.subItems} />
) : (
<a href={item.url}>{item.label}</a>
);
return <li>{content}</li>;
};
The MenuItemComponent
takes a MenuItem
as its item
prop. If the item has sub-items, it renders another Menu
component with the subItems
array as its items
prop. Otherwise, it renders an anchor tag with the url
and label
of the item.
With these components in place, we can create our menu hierarchy like this:
const menuItems: MenuItem[] = [
{
label: "Home",
url: "/",
},
{
label: "About",
url: "/about",
},
{
label: "Products",
subItems: [
{
label: "Product 1",
url: "/products/1",
},
{
label: "Product 2",
url: "/products/2",
},
{
label: "Product 3",
subItems: [
{
label: "Product 3.1",
url: "/products/3/1",
},
{
label: "Product 3.2",
url: "/products/3/2",
},
],
},
],
},
];
const App: React.FC = () => {
return <Menu items={menuItems} />;
};
Here, we define an menuItems
array with our menu hierarchy. We pass this array to the Menu
component as its items
prop. Finally, we render the Menu
component inside our App
component.
With this setup, our menu component will recursively render the nested menu hierarchy. if you want to add or remove menu items, we can simply modify the menuItems
array. This is the power of the Composite Pattern - it allows us to treat nested collections of objects as if they were individual objects, making our code flexible and easier to reason about.
The Decorator Pattern
Decorator is a structural design pattern that lets you attachs new behaviours to objects by placing these objects inside special wrapper objects that contain the behaviours.
Problem
Imagine you are working on a notification library that lets other programs notify their users about important events.
The initial version of the library was based on Notifier
class that had only a few fields, a constructor, and a single send
method. The method could accept a message argument from a client and send a message to the list of emails that were passed to the Notifier via its constructor. A third-party app that acted as a client was supposed to create and configure the object once, and then use it each time something important happened.
At some points, you realize that users of the library expect more than just email notifications. Many of them would like to receive an SMS about critical issues. Others would like to be notified on Facebook, of course, the corporate users would love to get Slack notifications.
How hard can that be? You extended a Notifier
class and put the additional notification methods into new subclasses. Now the client was supposed to instantiate the desired notification class and use it for all further notifications.
But then someone reasonably asked you, "Why can't you use several notification types at once? if your house is on fire, you would probably want to be informed through every channel"
You tried to address that problem by creating special subclasses that combined several notification methods within one class. However, it quickly became apparent that this approach would bloat the code immensely, not only the library code but the client code as well.
You have to find some other ways to structure notification classes so that their number won't accidentally break some Guinness record.
Solution
Extending a class is the first thing that comes to mind when you need to alter an object's behavior. However, inheritance has several serious caveats that you need to be aware of.
Inheritance is static. You can't alter the behavior of an existing object at runtime. You can only replace the whole object with another one that is created from a different subclass.
Subclasses can have just one parent class. In most languages, inheritance doesn't let a class inherit behaviors of multiple classes at the same time.
One of the ways to overcome these caveats is by using Aggregation and Composition instead of Inheritance. Both of the alternatives work almost the same way: one object has a reference to another and delegates it some work, whereas with inheritance, the object itself is able to do that work, inheriting the behaviors from its superclass.
With this new approach, you can easily substitute the linked "helper" object with another, changing the behaviors of the container at runtime. An object can use the behavior of various classes, having references to multiple objects and delegating them to all kinds of works. Aggregation/composition is the key principle behind many design patterns, including Decorator. On that note, let's return to the pattern discussion.
"Wrapper" is the alternative nickname for the Decorator pattern that clearly expresses the main idea of the pattern. A wrapper is an object that can be linked with some target object. The wrapper contains the same set of methods as the target and delegates to it all requests it receives. However, the Wrapper may alter the result by doing something either before or after it passes the request to the target.
When does a simple wrapper become the real decorator? As I mentioned, the wrapper implements the same interface as the wrapped object. That's why from the client's perspective these objects are identical. Make the wrapper's reference field accept any object that follows that interface. This will let you cover an object in multiple wrappers, adding the combined behavior of all the wrappers to it.
In our notifications example, let's leave the simple email notification behavior inside the base Notifier
class, but turn all other notification methods into decorators.
The code would need to wrap a basic notifier object into a set of decorators that match the client's preferences. The resulting objects will be structured as a stack.
The last decorator in the stack would be the object that the client actually works with. Since all decorators implement the same interface as the base notifier, the rest of the client code won't care whether it works with the "pure" notifier object or the decorated one.
We could apply the same approach to other behaviors such as formatting messages or composing the recipient list. The client can decorate the object with any custom decorator, as long as they follow the same interface with the others.
What exactly is the Decorator Pattern?
The Decorator Pattern is the design pattern that allows you to add functionality to an existing object without altering its structure. It's sometimes also called the Wrapper Pattern since it involves wrapping an object in another object to add behaviors. By wrapping an object in a decorator object, you can add new behavior to the object without modifying its original implementation. This allows you to create small, focused classes that do one thing well, and then combine them using decorators to create more complex behavior.
The Decorator Pattern is widely used in modern web development, particularly in front-end frameworks like React and Angular. In React, for example, components can be wrapped in higher-order components (HOCs) to add behavior to them.
HOCs - are essentially decorators that wrap a component and add behavior to it. For example, you could create an HOC that adds login functionality to a component, or one that adds authentication checks. HOCs are used extensively in React to create reusable code that can be applied to many different components.
Example of Decorator Design Pattern in Typescript
Let's dive into how we can implement the Decorator Pattern in Typescript, using a coffee shop as our example:
Step 1: Create the Component interface
First, we define our "Component" interface, Coffee, which describes methods that will be implemented by both the base component and the decorators:
interface Coffee {
cost(): number;
description(): string;
}
Step 2: Implement the Component interface
Next, we create the SimpleCoffee
class that implements the Coffee
interface. This is our base component when we start adding more behaviors.
class SimpleCoffee implements Coffee {
cost(): number {
return 1;
}
description(): string {
return "Simple coffee";
}
}
Step 3: Create the Base Decorator Class
Then we define our CoffeeDecorator
base class, which also implements the Coffee interface. The primary purpose of this class is to define the wrapping interface for all concrete decorators.
abstract class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}
abstract cost(): number;
abstract description(): string;
}
Step 4: Implement Concrete Decorators
Next, we create concrete decorator classes MilkDecorator
, SugarDecorator
, WhippedCreamDecorator
and CaramelDecorator
, to extend the behavior of our SimpleCoffee
.
class MilkDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.5;
}
description(): string {
return this.coffee.description() + ", milk";
}
}
class SugarDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.2;
}
description(): string {
return this.coffee.description() + ", sugar";
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.7;
}
description(): string {
return this.coffee.description() + ", whipped cream";
}
}
class CaramelDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.6;
}
description(): string {
return this.coffee.description() + ", caramel";
}
}
Step 5: Use the Decorators
Finally, we can use the decorators to add additional behavior to our SimpleCoffee
:
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);
coffee = new CaramelDecorator(coffee);
console.log(`Cost: $${coffee.cost()}`); // Outputs: Cost: $3.0
console.log(`Description: ${coffee.description()}`); // Outputs: Description: Simple coffee, milk, sugar, whipped cream, caramel
With these decorators, we have added milk, sugar, whipped cream, and caramel to our coffee dynamically, without modifying its underlying code. You can find the full code in this gist.
\=> The Decorator Pattern provides a powerful tool for dynamically extending the functionality of objects, keeping our code flexible, and manageable, and following the Open/Closed Principle. However, like all patterns, it’s not a silver bullet. Always consider if the added complexity is worthwhile for each specific use case. Happy decorating!
The Facade Pattern
Facade is a structural design pattern that provides a simplified interface to the library, a framework, or any other complex set of classes.
Problem
Imagine you must make your code work with a broad set of objects that belong to a sophisticated library or framework. Ordinarily, you'd need to initialize all of those objects, keep track of dependencies, execute methods in the correct order, and so on.
As a result, the business logic of your classes would become tightly coupled with the implementation details of 3rd-party classes, making it hard to comprehend and maintain.
Solution
A facade is a class that provides a simple interface to a complex subsystem which contains lots of moving parts. A facade might provide limited functionality in comparison to working with the subsystem correctly. However, it includes only those features that clients really care about.
Having a facade is handy when you need to integrate your app with a sophisticated library that has dozens of features, but you just need a tiny bit of its functionality.
For instance, an app that uploads funny videos with cats to social media could potentially use a professional video conversion library. However, all that it really needs is a class with a single method encode(filename, format)
. After creating such a class and connecting it with the video conversion library, you will have your first facade.
What is the Facade Design Pattern?
The Facade design pattern is a structural pattern that provides a simple interface to a complex system of structs, libraries, or subsystems. It's used to hide the system's complexity and provide an easy-to-use interface to the clients. The facade pattern is also known as a wrapper or an interface pattern. The Facade pattern also promotes loose coupling between the client code and the system, making it easier to modify the system without affecting the client code.
The objectives of the Facade Pattern are the following:
Provide a simple interface to a complex subsystem.
Encapsulate the complexity of the subsystem.
Reduce coupling between the client and the subsystem.
The advantages of the Facade pattern are:
It simplifies the interface to the complex system, making it easier for clients to use.
It hides the system's complexity, making it easier to maintain and modify the system without affecting the client.
It decouples the client from the system, making it easier to maintain and modify the system without affecting the client.
The disadvantages of the Facade pattern are:
It can create an additional layer of abstraction, which can add complexity to the system.
It can make the system less flexible, as the Facade limits the client's access to the underlying system.
Example of the Facade Design Pattern in Javascript
Let's pretend we are building a simple game and one of our first implementations of something will be a definition of a Human
class. Other parts of our code will expect this human class to contain several body parts as properties: Eye
, Ear
, Arm
, Leg
, Feet
, Nose
, Mouth
, Neck
, and Stomach
class Human {
constructor(opts = {}) {
this.leftEye = opts.leftEye
this.rightEye = opts.rightEye
this.leftArm = opts.leftArm
this.rightArm = opts.rightArm
this.leftFoot = opts.leftFoot
this.rightFoot = opts.rightFoot
this.leftEar = opts.leftEar
this.rightEar = opts.rightEar
this.nose = opts.nose
this.mouth = opts.mouth
this.neck = opts.neck
this.stomach = opts.stomach
}
}
class Eye {}
class Ear {}
class Arm {}
class Leg {}
class Feet {}
class Nose {}
class Mouth {}
class Neck {}
class Stomach {}
Let's now define a Profile
class which has a method or setting of its profile character called setCharacter
:
class Profile {
setCharacter(character) {
validateCharacter(character)
this.character = character
}
}
In a real-world scenario, of course, it would be dozens of times longer than this so just keep in mind that we are focusing solely on the pattern and the problem it solves by presenting only the relative code.
We included an validateCharacter
at the beginning of our setCharacter
function because it's a necessary component of any software to validate constructed pieces to ensure that errors don't occur unknowingly. This also happens to be a great setup to demonstrate the usefulness of a Facade
.
Here is the implementation:
function validateCharacter(character) {
if (!character.leftEye) throw new Error(`Missing left eye`)
if (!character.rightEye) throw new Error(`Missing right eye`)
if (!character.leftEar) throw new Error(`Missing left ear`)
if (!character.rightEar) throw new Error(`Missing right ear`)
if (!character.nose) throw new Error(`Missing nose`)
if (!character.mouth) throw new Error(`Missing mouth`)
if (!character.neck) throw new Error(`Missing neck`)
if (!character.stomach) throw new Error(`Missing stomach`)
}
So, inside our validateCharacter
call it to proceed to check every part of the human body and throw an error if at least one body part is missing.
Let's try to use our code as if we were the client:
const bob = new Human()
const bobsProfile = new Profile()
bobsProfile.setCharacter(bob)
Running the code will result in an error: Error missing left eye
So, how does the client fix this? Easy! They just need to construct every single body part and be responsible for making them exist in the instance. Here is the code:
const bobsLeftEye = new Eye()
const bobsRightEye = new Eye()
const bobsLeftEar = new Ear()
const bobsRightEar = new Ear()
const bobsNose = new Nose()
const bobsMouth = new Mouth()
const bobsNeck = new Neck()
const bobsStomach = new Stomach()
const bobsLeftArm = new Arm()
const bobsRightArm = new Arm()
const bobsLeftLeg = new Leg()
const bobsRightLeg = new Leg()
const bobsLeftFoot = new Feet()
const bobsRightFoot = new Feet()
const bob = new Human()
bob.leftEye = bobsLeftEye
bob.rightEye = bobsRightEye
bob.leftEar = bobsLeftEar
bob.rightEar = bobsRightEar
bob.nose = bobsNose
bob.mouth = bobsMouth
bob.neck = bobsNeck
bob.stomach = bobsStomach
bob.leftArm = bobsLeftArm
bob.rightArm = bobsRightArm
bob.leftLeg = bobsLeftLeg
bob.rightLeg = bobsRightLeg
bob.leftFoot = bobsLeftFoot
bob.rightFoot = bobsRightFoot
Now our code runs without errors. However, if you went along hands-on you might have noticed that you only cared about creating a character and the profile, so we encountered a couple of unpleasant issues here:
The code is longer
The code is more complex
The code is not very easy to use for the client (in comparison to the original three liner)
Redundancy - This is redundancy in multiple areas. one hard hit to the redundancy is the repetitive mentioning of
bob
throughout our codeWe gave the ball to the client (in other words we forced users of our code to become responsible for constructing and passing in every single body). A lot of time this becomes a good thing - it's not uncommon that we want to give the client code the ability to call the shots. But this situation is much different. There is no point here in having them do the work if they choose to go straight to creating the profile and proceed solely with the profile's features.
Now, let's make our Facade
and define how it will solve our unpleasant issues for clients:
class Profile {
setCharacter(character) {
if (!character.leftEye) character.leftEye = new Eye()
if (!character.rightEye) character.rightEyye = new Eye()
if (!character.leftEar) character.leftEar = new Ear()
if (!character.rightEar) character.rightEar = new Ear()
if (!character.nose) character.nose = new Nose()
if (!character.mouth) character.mouth = new Mouth()
if (!character.neck) character.neck = new Neck()
if (!character.stomach) character.stomach = new Stomach()
this.character = character
}
}
const bob = new Human()
const bobsProfile = new Profile()
bobsProfile.setCharacter(bob)
Our Profile
becomes the Facade
itself and effectively encapsulates every implementation of body parts as a fallback so that the client code only needs to focus on the interface provided by the Profile
.
We not only give them the option to skip the unnecessary steps to construct and set each body part, but we also give them the option to be fully in control of that if they want to on their own.
Also, notice that the user of our code is tightly coupled to the interface that Profile
exposes.
Differences Between the Adapter and Flyweight Pattern
Facade vs Adapter
Sometimes the facade design pattern can be mistaken for the Adapter
design pattern. But the difference is that the Facade
may expose an entirely new interface for the client to use whereas the Adapter
’s intent is to be backward compatible with previous interfaces when seeking to extend with newer properties or behavior.
Facade vs Flyweight
In the facade, the client code is given an interface to work with that represents an entire system of objects (which can contain new copies of identical objects) while the client in the flyweight pattern is given an interface to produce objects that intend to be shared when identical which is an effective approach to preserve memory.
The Flyweight pattern
Flyweight is a structural design pattern that lets you fit more objects into the available amount of RAM by sharing common parts of state between multiple objects instead of keeping all of the data in each object.
Problem
To have some fun after long working hours, you decided to create a simple game: players would be moving around the map and shooting each other. You choose to implement a realistic particle system and make it a distinctive feature of the game. Vast quantities of bullets and shrapnel from explosions should fly over the map and deliver a thrilling experience to the player.
Upon its completion, you pushed the last commit, built the game, and sent it to your friend for a test drive. Although the game was running flawlessly on your machine, your friend wasn't able to play for long. On his computer, the game kept crashing after a few minutes of gameplay. After spending several hours digging through debug logs, you discovered that the game crashed because insufficient amount of RAM. It turned out that your friend's rig was much less powerful than your computer, and that's why the problem emerged so quickly on his machine.
The actual problem was related to your particle system. Each particle, such as the bullet, a missable, or a piece of shrapnel was presented by a separate object containing plenty of data. At some point, when the carnage on a player's screen reaches its climax, the newly created particle no longer fits into the remaining RAM, so the program crashes.
Solution
On closer inspection of the Particle
class, you may notice that the color and sprite fields consume a lot of memory than other fields. what's worse is that these two fields store almost identical data across all particles. For example, all bullets have the same color and sprite.
Other parts of a particle's state, such as coordinates, movement vector, and speed, are unique to each particle. After all, the values of these fields change over time. This data represents the always-changing context in which the particle exists, while the color and sprite remain constant for each particle.
The constant data of an object is usually called the intrinsic state. It lives within the object; other objects can only read it, not change it. The rest of the object's state, often altered "from the outside" by other objects, is called the extrinsic state.
The Flyweight pattern suggests that you stop storing the extrinsic state inside the object. Instead, you should pass this state on to specific methods that rely on it. Only the intrinsic state stays within the object, letting you it reuse in different contexts. As a result, you would need fewer of these objects since they only differ in the intrinsic state, which has much fewer variations in the extrinsic.
Let's return to our game. Assuming that we had extracted the extrinsic state from our particle class, only three different objects would suffice to represent all particles in the game: a bullet, a missile, and a piece of shrapnel. As you have probably guessed by now, an object that only stores the intrinsic is called a flyweight.
What is the Flyweight Design Pattern?
First, let's see the definition provided by the Gang of Four book:
A flyweight is a shared object that can be used in multiple contexts simultaneously. The flyweight acts as an independent object in each context: it's distinguishable from an instance of the object that is not shared.
On the other hand, flyweight objects can not make assumptions about the object in which they operate. The key concept here is the distinction between intrinsic and extrinsic states. The intrinsic state is stored in the flyweight; it consists of information that is independent of the flyweight's context, allowing it to be shared. The extrinsic state depends on the flyweight's context and varies with it, therefore, it can not be shared. Client objects are responsible for passing the extrinsic state to the flyweight when it is needed.
An example of the Flyweight Design Pattern
If you have used a Javascript library before, there is a good chance you have worked directly on some variation of a flyweight pattern given to you whether it was through a Javascript library, framework, or even the DOM.
Let's take a look at this array of objects that represents objects as DOM elements:
const elems = [
{
tagName: 'div',
style: { width: '28.5px', height: '20px' },
children: [
{ tagName: 'input', style: { width: '28.5px', height: '20px' } },
{ tagName: 'input', style: { width: '28.5px', height: '20px' } },
{ tagName: 'select', style: { width: '28.5px', height: '20px' } },
{ tagName: 'input', style: { width: '28.5px', height: '20px' } },
],
},
]
If you look at the children
array, notice three objects are structurally identical:
This is already an issue because if we were to continue this practice and our objects grow larger our program would take a big hit to the performance because it will create three separate objects in memory although they are all structurally equivalent. Imagine if there were 1000?
When we go over real examples of the flyweight design pattern this is basically what the flyweight intends to do behind the scenes:
const inputElement = {
tagName: 'input',
style: { width: '28.5px', height: '20px' },
}
const elems = [
{
tagName: 'div',
style: { width: '28.5px', height: '20px' },
children: [
inputElement,
inputElement,
{ tagName: 'select', style: { width: '28.5px', height: '20px' } },
inputElement,
],
},
]
Notice how inputElement
is mentioned multiple times.
The Proxy Design Pattern
Proxy is a structural design pattern that lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original objects.
Problem
Why would you want to control access to an object? Here is an example: you have a massive object that consumes a vast amount of system resources. You need it from time to time, but not always.
You could implement lazy initialization: create this object only when it's actually needed. All of the object's clients would need to execute some deferred initialization code. Unfortunately, this would probably cause a lot of code duplication.
In an ideal world, we would want to put this code directly into our object's class, but that isn't always possible. For instance, the class may be part of a closed 3rd-party library.
Solution
The Proxy pattern suggests that you create a new proxy class with the same interface as an original service object. Then you update your app so that it passes the proxy object to all of the original object's clients. Upon receiving the request from the client, the proxy creates a real service object and delegates all the work to it.
But what is the benefit? If you need to execute something either before or after the primary logic of the class, the proxy lets you do it without changing that class. Since the proxy implements the same interface as the original class, it can be passed to any client that expects a real service object.
Real-World Analogy
A credit card is a proxy for a bank account, a proxy for a cash bundle. Both implement the same interface: they can be used to make a payment. A consumer feels great because there is no need to carry loads of cash around. A shop owner is also happy since the income from the transaction gets added electronically to the shop's bank account without the risk of losing a deposit or getting robbed on the way to the bank.
What is the Proxy Design Pattern?
The first thing we will do, as with the other design patterns, is to look at the definition offered by the book "Design Patterns" by the Gang of Four:
Provide a surrogate or placeholder for another object to control access to it.
A proxy can perform additional operations (such as controlled access, lazy loading, etc) before accessing a real object. The most common uses of the proxy pattern according to its purpose are as follows:
Virtual Proxy: controls access to resources that are expensive to create, such as objects that require a lot of memory or processing time. It's used for lazy loading of an object.
Protection Proxy: controls access to an object by providing different permissions to different users.
Remote Proxy: allows access to an object that resides in a different space. For example, this proxy could handle all the necessary communication between the client and the server on different machines.
Let's look at the UML class diagram to understand each of its elements of:
Subject: Defines a common interface for
RealSubject
andProxy
so thatProxy
can be used in place ofRealSubject
.RealSubject: The real objects that the proxy represents.
Proxy: Maintain a reference that allows the proxy to access the
RealSubject
. Provides an identical interface to that ofSubject
so it can substituteRealSubject
. Controls access toRealSubject
and can be responsible for its creation and deletion.
An example of a Proxy Design Pattern in React
Now, you should have a clear idea about the proxy design pattern. It's time to get your hands dirty and gain hands-on experience with React using Proxy.
React comes with its built-in features set to optimize the performance and overall responsiveness of the app. However, when combining the proxy design pattern with these features, you can further enhance the performance of the app with well-controlled object interactions and data access.
Enhanced Security
Proxy design patterns can be helpful in a React application to enhance security by providing layers of control over object interactions and access to sensitive data. Below, you can find a simple demonstration of this concept.
import { useEffect, useState } from 'react';
const fetchUserData = () => {
// Place the network request here
console.log('Data fetching...');
return {
username: 'john_doe',
password: 'user_password',
};
};
const useSecureUserData = () => {
const [userData, setUserData] = useState(null);
const isUserAdmin = () => {
// Implement your authorization logic here
// For simplicity, assume logged user is admin
return true;
};
useEffect(() => {
const userProxy = new Proxy(fetchUserData(), {
get(target, prop) {
// Check if the user is authorized to access the user password
if (prop === 'password' && !isUserAdmin()) {
console.warn('Unauthorized access to password!');
return null; // Return null or handle unauthorized access
}
return target[prop];
},
});
setUserData(userProxy);
}, []);
return userData;
};
export const UserProfile = () => {
const userData = useSecureUserData();
if (!userData) {
return <div>Loading...</div>;
}
return (
<div>
<p>Username: {userData.username}</p>
<p>Password: {userData.password}</p>
</div>
);
};
In this simple example, the useSecureUserData
custom hook fetches the user data and creates a proxy object to validate user data before returning it. Here the fetchUserData
returns the real object and userProxy
is the Proxy object. Finally UserProfile
component works as the client calling the proxy object.
While this example illustrates using a Proxy object for access management, it’s important to note that using Higher-Order Components (HOC), the Context API, or router guards are common and sometimes more conventional approaches in React. The choice depends on the specific requirements of your application and the desired level of access control.
Roundup
I hope you found this to be valuable. Look out for Behavioral Patterns in the next article.
Happy Coding ❤️
References
https://javascript.plainenglish.io/how-i-use-adapter-pattern-in-reactjs-cb331e9bef0c
https://365kim.medium.com/bridge-pattern-in-typescript-structural-design-pattern-2-65e519d3f578
https://blog.bitsrc.io/decorator-design-pattern-in-typescript-701dfbf24420
https://medium.com/gitconnected/facade-design-pattern-in-golang-with-unit-tests-7089b558b94e
https://medium.com/better-programming/the-facade-design-pattern-in-javascript-d3843853b238
https://betterprogramming.pub/the-power-of-flyweight-design-pattern-in-javascript-5593ae9fa858
https://levelup.gitconnected.com/understanding-the-proxy-design-pattern-4cdee9a43d25
https://blog.bitsrc.io/proxy-design-pattern-with-react-c0b465980fbf
Subscribe to my newsletter
Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Tuan Tran Van
Tuan Tran Van
I am a developer creating open-source projects and writing about web development, side projects, and productivity.