Lesson 4
Real-world Application of Structural Patterns in JavaScript
Real-world Application of Structural Patterns

We are progressing in our understanding of Structural Patterns. In this lesson, we’ll see how to apply them in a practical example by creating a GUI library. So far, we’ve explored the Adapter, Composite, and Decorator Patterns independently. Now, we’ll integrate these patterns within a GUI library context to form a cohesive project. Note that these patterns are abstract and can be applied in various other scenarios beyond GUI libraries.

Adapter Pattern Recap

Let's quickly revisit the Adapter Pattern. This pattern allows two incompatible interfaces to work together. We accomplish this by creating an adapter class that converts one interface to another. In the context of our GUI library, consider the following classes:

JavaScript
1class WinButton { 2 render() { 3 console.log("Rendering a button in a Windows style."); 4 } 5} 6 7class MacButton { 8 display() { 9 console.log("Rendering a button in a Mac style."); 10 } 11}

Our WinButton class has a render method, while the MacButton class has a display method. To adapt MacButton to work within systems expecting a WinButton interface, we apply the Adapter Pattern by creating an adapter class:

JavaScript
1class ButtonAdapter extends WinButton { 2 constructor(button) { 3 super(); 4 this.button = button; 5 } 6 7 render() { 8 this.button.display(); 9 } 10}

Here, ButtonAdapter adapts MacButton to the WinButton interface, allowing it to be used interchangeably. This allows the MacButton instance to be treated like a WinButton.

Intermediate Adapter Steps

First, let's create the MacButton and wrap it with the ButtonAdapter:

JavaScript
1const macButton = new MacButton(); 2const winButton = new ButtonAdapter(macButton); 3winButton.render(); // Output: Rendering a button in a Mac style.

This code instantiates a MacButton and adapts it using ButtonAdapter, enabling it to render with a Windows style.

Composite Pattern Recap

The Composite Pattern helps us compose objects into tree structures to represent part-whole hierarchies. This allows clients to treat individual objects and compositions of objects uniformly. For our GUI library, we use the Composite Pattern to manage components:

JavaScript
1class Component { 2 render() { 3 throw new Error("Method 'render()' must be implemented."); 4 } 5}

Let's create a Container class that will act as a composite class containing other components:

JavaScript
1class Container extends Component { 2 constructor() { 3 super(); 4 this.components = []; 5 } 6 7 add(component) { 8 this.components.push(component); 9 } 10 11 remove(component) { 12 this.components = this.components.filter(comp => comp !== component); 13 } 14 15 render() { 16 this.components.forEach(component => component.render()); 17 } 18}

We also need a Button class that will be a concrete component:

JavaScript
1class Button extends Component { 2 render() { 3 console.log("Rendering a button."); 4 } 5}

The Container can hold and manage multiple components, such as buttons, efficiently, implementing the Composite Pattern.

Intermediate Composite Steps

Next, we create a Container and add multiple Button elements to it:

JavaScript
1const container = new Container(); 2container.add(new Button()); 3container.add(new Button()); 4container.render(); 5// Output: 6// Rendering a button. 7// Rendering a button.

This code creates a container and adds two buttons to it, allowing the entire container to be rendered, demonstrating the Composite Pattern in action.

Bringing It All Together

By combining the Adapter and Composite Patterns, we create a flexible GUI library. We have adaptive buttons and composite containers managing multiple GUI elements. This way, we can render complex interfaces with ease, maintaining compatibility and modularity.

Here is a glimpse of how the final structure looks:

JavaScript
1const macButton = new MacButton(); 2const winButton = new ButtonAdapter(macButton); 3winButton.render(); 4// Output: Rendering a button in a Mac style. 5 6const container = new Container(); 7container.add(new Button()); 8container.add(new Button()); 9container.render(); 10// Output: 11// Rendering a button. 12// Rendering a button.

In this code snippet, we create and adapt a MacButton to a WinButton, then render it. We also create a Container and add multiple Button elements to it, demonstrating the combination of both Adapter and Composite Patterns in a practical scenario.

Decorator Pattern Recap

The Decorator Pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class. This pattern is particularly useful for enhancing the functionality of GUI components.

Let's consider the following simple Button class:

JavaScript
1class Button extends Component { 2 render() { 3 console.log("Rendering a button."); 4 } 5}

To dynamically add behavior, such as adding a border or enabling/disabling the button, we use the Decorator Pattern:

JavaScript
1class ButtonDecorator extends Component { 2 constructor(button) { 3 super(); 4 this.button = button; 5 } 6 7 render() { 8 this.button.render(); 9 } 10}

This ButtonDecorator class encapsulates the Button object and can add additional behavior.

Adding Behavior with Decorators

We can now create specific decorators to enhance our Button:

JavaScript
1class BorderDecorator extends ButtonDecorator { 2 render() { 3 this.button.render(); 4 this.addBorder(); 5 } 6 7 addBorder() { 8 console.log("Adding border."); 9 } 10}
JavaScript
1class EnabledDecorator extends ButtonDecorator { 2 render() { 3 if (this.isEnabled()) { 4 this.button.render(); 5 } else { 6 console.log("Button is disabled."); 7 } 8 } 9 10 isEnabled() { 11 // Assuming some logic here to determine if the button is enabled 12 return true; // Just for demonstration 13 } 14}

We have now created two decorators — BorderDecorator and EnabledDecorator — that add specific behaviors to buttons.

Intermediate Decorator Steps

Let's see how to use these decorators with a Button:

JavaScript
1const button = new Button(); 2const decoratedButton = new BorderDecorator(button); 3decoratedButton.render(); 4// Output: 5// Rendering a button. 6// Adding border.

This code wraps a Button object with a BorderDecorator, dynamically adding the border-rendering behavior.

To add multiple decorators, you simply stack them:

JavaScript
1const button = new Button(); 2const decoratedButton = new EnabledDecorator(new BorderDecorator(button)); 3decoratedButton.render(); 4// Output: 5// If enabled: Rendering a button. Adding border. 6// If disabled: Button is disabled.

This code adds both the border and enabled behaviors to the Button.

Combining with Composite and Adapter Patterns

Finally, let's integrate decorators into our existing structure with Adapter and Composite Patterns:

JavaScript
1const macButton = new MacButton(); 2const winButton = new ButtonAdapter(macButton); 3const decoratedWinButton = new BorderDecorator(winButton); 4decoratedWinButton.render(); 5// Output: 6// Rendering a button in a Mac style. 7// Adding border. 8 9const container = new Container(); 10const button1 = new Button(); 11const button2 = new Button(); 12const decoratedButton1 = new BorderDecorator(button1); 13const decoratedButton2 = new EnabledDecorator(button2); 14container.add(decoratedButton1); 15container.add(decoratedButton2); 16container.render(); 17// Output: 18// Rendering a button. 19// Adding border. 20// Rendering a button.

In this extended example, we wrap the adapted MacButton in a BorderDecorator. We also decorate individual Button objects before adding them to the Container.

Complete Code
JavaScript
1class WinButton { 2 render() { 3 console.log("Rendering a button in a Windows style."); 4 } 5} 6 7class MacButton { 8 display() { 9 console.log("Rendering a button in a Mac style."); 10 } 11} 12 13class ButtonAdapter extends WinButton { 14 constructor(button) { 15 super(); 16 this.button = button; 17 } 18 19 render() { 20 this.button.display(); 21 } 22} 23 24class Component { 25 render() { 26 throw new Error("Method 'render()' must be implemented."); 27 } 28} 29 30class Container extends Component { 31 constructor() { 32 super(); 33 this.components = []; 34 } 35 36 add(component) { 37 this.components.push(component); 38 } 39 40 remove(component) { 41 this.components = this.components.filter(comp => comp !== component); 42 } 43 44 render() { 45 this.components.forEach(component => component.render()); 46 } 47} 48 49class Button extends Component { 50 render() { 51 console.log("Rendering a button."); 52 } 53} 54 55class ButtonDecorator extends Component { 56 constructor(button) { 57 super(); 58 this.button = button; 59 } 60 61 render() { 62 this.button.render(); 63 } 64} 65 66class BorderDecorator extends ButtonDecorator { 67 render() { 68 this.button.render(); 69 this.addBorder(); 70 } 71 72 addBorder() { 73 console.log("Adding border."); 74 } 75} 76 77class EnabledDecorator extends ButtonDecorator { 78 render() { 79 if (this.isEnabled()) { 80 this.button.render(); 81 } else { 82 console.log("Button is disabled."); 83 } 84 } 85 86 isEnabled() { 87 // Assuming some logic here to determine if the button is enabled 88 return true; 89 } 90} 91 92const macButton = new MacButton(); 93const winButton = new ButtonAdapter(macButton); 94const decoratedWinButton = new BorderDecorator(winButton); 95decoratedWinButton.render(); 96// Output: 97// Rendering a button in a Mac style. 98// Adding border. 99 100const container = new Container(); 101const button1 = new Button(); 102const button2 = new Button(); 103const decoratedButton1 = new BorderDecorator(button1); 104const decoratedButton2 = new EnabledDecorator(button2); 105container.add(decoratedButton1); 106container.add(decoratedButton2); 107container.render(); 108// Output: 109// Rendering a button. 110// Adding border. 111// Rendering a button.

By incorporating the Decorator Pattern alongside Adapter and Composite Patterns, we've demonstrated a powerful approach to building extensible and maintainable GUI libraries. This enables you to compose flexible, dynamic behaviors within your GUI components seamlessly.

Conclusion

By merging the Adapter, Composite, and Decorator Patterns, we can build a versatile and efficient GUI library. This combination helps us create adaptive buttons and manage complex GUI elements through composite containers, showcasing how different structural patterns can be used together to enhance design and functionality. This approach not only makes the system more robust but also ensures ease of maintenance and extension.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.