Welcome to the third section of our design patterns curriculum, where we dive into Structural Patterns. These patterns are concerned with how classes and objects can be composed to form larger structures. They help in creating flexible and reusable code by defining new classes that represent relationships between existing ones.
In this tutorial, you'll learn about some of the most commonly used structural design patterns through hands-on exercises. We'll cover:
Each pattern will be explained with a real-world analogy and followed by a practical coding exercise to help you understand its application.
Real-World Analogy: Think of an adapter as a device that allows two incompatible devices to work together. For example, a USB-C to Lightning cable lets your iPhone charge using a USB-C port.
Definition: The Adapter pattern converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Real-World Analogy: Imagine you have different types of remote controls (e.g., TV, Radio) and different brands of devices. A bridge allows these remotes to control their respective devices without changing the remote's code.
Definition: The Bridge pattern separates an abstraction from its implementation so that the two can vary independently.
Real-World Analogy: Consider a tree structure where each node can be either a leaf or another subtree. A composite pattern allows you to treat individual objects and compositions of objects uniformly.
Definition: The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
Problem: You have a legacy system that uses an old API, but you need to integrate it with a new system that expects a different API.
Solution: Use the Adapter pattern to create a new interface that wraps the old API.
1// Old API2class OldAPI {3request() {4return "Old data";5}6}78// New API expected by the system9class NewAPI {10fetchData() {11// This method should fetch data from the OldAPI12}13}1415// Adapter class16class Adapter extends NewAPI {17constructor(oldApi) {18this.oldApi = oldApi;19}2021fetchData() {22return this.oldApi.request();23}24}2526// Usage27const oldApiInstance = new OldAPI();28const adapterInstance = new Adapter(oldApiInstance);29console.log(adapterInstance.fetchData()); // Output: "Old data"
Problem: You have a drawing application that supports different shapes and colors. You want to add more colors without changing the shape classes.
Solution: Use the Bridge pattern to separate the abstraction (shapes) from the implementation (colors).
1// Color interface2class Color {3applyColor() {}4}56// Concrete Colors7class Red extends Color {8applyColor() { return "Red"; }9}1011class Blue extends Color {12applyColor() { return "Blue"; }13}1415// Shape abstraction16class Shape {17constructor(color) {18this.color = color;19}2021draw() {}22}2324// Concrete Shapes25class Circle extends Shape {26draw() {27console.log(`Drawing a circle with ${this.color.applyColor()} color`);28}29}3031class Square extends Shape {32draw() {33console.log(`Drawing a square with ${this.color.applyColor()} color`);34}35}3637// Usage38const red = new Red();39const blue = new Blue();4041const circle = new Circle(red);42circle.draw(); // Output: "Drawing a circle with Red color"4344const square = new Square(blue);45square.draw(); // Output: "Drawing a square with Blue color"
Problem: You need to represent a file system where both files and directories can be treated uniformly.
Solution: Use the Composite pattern to create a tree structure of files and directories.
1// Component interface2class Component {3add(component) {}4remove(component) {}5display() {}6}78// Leaf class (File)9class File extends Component {10constructor(name) {11this.name = name;12}1314display() {15console.log(`File: ${this.name}`);16}17}1819// Composite class (Directory)20class Directory extends Component {21constructor(name) {22this.name = name;23this.children = [];24}2526add(component) {27this.children.push(component);28}2930remove(component) {31const index = this.children.indexOf(component);32if (index !== -1) {33this.children.splice(index, 1);34}35}3637display() {38console.log(`Directory: ${this.name}`);39this.children.forEach(child => child.display());40}41}4243// Usage44const root = new Directory("Root");45const documents = new Directory("Documents");46const photos = new Directory("Photos");4748const report = new File("report.txt");49const vacationPhoto = new File("vacation.jpg");5051documents.add(report);52photos.add(vacationPhoto);5354root.add(documents);55root.add(photos);5657root.display();58// Output:59// Directory: Root60// Directory: Documents61// File: report.txt62// Directory: Photos63// File: vacation.jpg
Congratulations on completing the exercises for Structural Patterns! In the next section, we'll explore Behavioral Patterns. These patterns are focused on improving communication between objects and defining how they interact.
Stay tuned for more hands-on exercises to enhance your understanding of design patterns and their practical applications in software development.
Happy coding!