DEV Community

Cover image for Design patterns: Part Two - A brief explanation of structural pattern
andres paladines
andres paladines

Posted on

Design patterns: Part Two - A brief explanation of structural pattern

The picture was taken from boredpanda.

Hello everyone!

Starting my second post with one of the most basic but never unnecessary topics; Design patterns.

The previous post targets Creational Patterns, and this one will focus on Structural Patterns.

Let's start listing them again:

.
├── Creational Patterns (previous post)
│   ├── Singleton
│   ├── Factory Method
│   ├── Builder
│   ├── Abstract Factory
│   ├── Prototype
├── Structural Patterns (current post)
│   ├── Adapter
│   ├── Decorator
│   ├── Facade
│   ├── Composite
│   ├── Proxy
├── Behavioral Patterns (Working on it)
│   ├── Observer
│   ├── Strategy
│   ├── Command
│   ├── Chain of Responsibility
│   ├── Mediator
│   ├── State
│   ├── Template Method
.
Enter fullscreen mode Exit fullscreen mode

Structural Patterns

This post will focus on Structural Patterns and how to construct them with swift and javascript. So, Let's start!

What does that mean by Structural pattern?

It is how classes and objects are composed to form larger structures. These patterns help ensure that if one part of a system changes, the entire structure doesn’t need to change. They facilitate design by identifying a simple way to realize relationships among entities.


Adapter

Basically, it allows incompatible interfaces to work together by acting as a bridge.
Use case: Adapting a UIKit View inside SwiftUI.

Swift (Adapting UIKit to SwiftUI):

// This is simple SwiftUI view representing a label
class UILabelView: UIView {
    private let label: UILabel

    init(text: String) {
        self.label = UILabel()
        super.init(frame: .zero)
        self.label.text = text
        self.label.textAlignment = .center
        self.addSubview(label)
        label.ytanslatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: self.centerYAnchor)
        ])
}

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented.")
    }
}
Enter fullscreen mode Exit fullscreen mode

Use case: Adapting a Firebase logger to have extra features in case it cannot be extended (Open close principle, not edit but extend object functionalities).

JavaScript:

class FirebaseAnalytics {
    logEvent(name, parameters) {
        console.log(`Logging event to Firebase: ${name}, parameters: ${JSON.stringify(parameters)}`);
    }
}

class FirebaseAdapter {
    constructor() {
        this.firebaseAnalytics = new FirebaseAnalytics();
    }

    trackEvent(name, properties) {
        this.firebaseAnalytics.logEvent(name, properties);
    }

    trackUser(name, email) {
        this.trackEvent(name, { method: email });
    }

}

// Usage
const analyticsService = new FirebaseAdapter();
analyticsService.trackEvent("user_signup", { method: "email" });
analyticsService.trackUser("John", "Doe");

Enter fullscreen mode Exit fullscreen mode

Decorator

Adds extra responsibilities to an object dynamically without altering its core functionality.
Use case: Adding different logging levels (info, debug, error) dynamically.

Swift:

protocol Logger {
    func log(message: String)
}

class BasicLogger: Logger {
    func log(message: String) {
        print(message)
    }
}

class LoggerDecorator: Logger {
    private let wrapped: Logger

    init(_ wrapped: Logger) {
        self.wrapped = wrapped
    }

    func log(message: String) {
        wrapped.log(message: message)
    }
}

class InfoLogger: LoggerDecorator {
    override func log(message: String) {
        super.log(message: "INFO: \(message)")
    }
}

class ErrorLogger: LoggerDecorator {
    override func log(message: String) {
        super.log(message: "ERROR: \(message)")
    }
}

// Usage
let logger: Logger = InfoLogger(ErrorLogger(BasicLogger()))
let loggerInfo: Logger = InfoLogger(BasicLogger())
logger.log(message: "An error occurred") //ERROR: INFO: An error occurred
loggerInfo.log(message: "An info message was sent") //INFO: An info message was sent
Enter fullscreen mode Exit fullscreen mode

JavaScript:

class BasicLogger {
    log(message) {
        console.log(message);
    }
}

class LoggerDecorator {
    constructor(wrapped) {
        this.wrapped = wrapped;
    }

    log(message) {
        this.wrapped.log(message);
    }
}

class InfoLogger extends LoggerDecorator {
    log(message) {
        super.log(`INFO: ${message}`);
    }
}

class ErrorLogger extends LoggerDecorator {
    log(message) {
        super.log(`ERROR: ${message}`);
    }
}

// Usage
const logger = new ErrorLogger(new InfoLogger(new BasicLogger()));
const loggerInfo = new InfoLogger(new BasicLogger());
logger.log("An error occurred");
loggerInfo.log("This is an info message");

Enter fullscreen mode Exit fullscreen mode

Facade

With this, we create a simple interface that covers a complex object or objects, making it/them easier to use.
Use case: Providing a simplified interface to a complex set of APIs.

Swift:

class ComplexSystemA {
    func operationA1() { print("Operation A1") }
    func operationA2() { print("Operation A2") }
}

class ComplexSystemB {
    func operationB1() { print("Operation B1") }
    func operationB2() { print("Operation B2") }
}

class Facade {
    private let systemA = ComplexSystemA()
    private let systemB = ComplexSystemB()

    func operationOne() {
        systemA.operationA1()
        systemB.operationB1()
    }

    func operationTwo() {
        systemA.operationA2()
        systemB.operationB2()
    }
}

// Usage
let facade = Facade()
facade.operationOne()

Enter fullscreen mode Exit fullscreen mode

JavaScript:

class ComplexSystemA {
    operationA1() { console.log("Operation A1"); }
    operationA2() { console.log("Operation A2"); }
}

class ComplexSystemB {
    operationB1() { console.log("Operation B1"); }
    operationB2() { console.log("Operation B2"); }
}

class Facade {
    constructor() {
        this.systemA = new ComplexSystemA();
        this.systemB = new ComplexSystemB();
    }

    simplifiedOperationOne() {
        this.systemA.operationA1();
        this.systemB.operationB1();
    }
}

// Usage
const facade = new Facade();
facade.simplifiedOperation();

Enter fullscreen mode Exit fullscreen mode

Composite

This pattern allow us to compose a tree of objects to represent part or whole hierarchies.
Use case: Implementing a File System Structure:
In a file system, files and directories can be treated uniformly. Both can be added, removed, or displayed. Directories can contain files or other directories.

Swift:

//The protocol will define a common interface for leaf and composite objects.
protocol FileSystemComponent {
    var name: String { get }
    func display(indent: String)
}

//File represents the leaf objects
class File: FileSystemComponent {
    var name: String

    init(name: String) {
        self.name = name
    }

    func display(indent: String) {
        print("\(indent)- \(name)")
    }
}

//Directory represents the composite objects
class Directory: FileSystemComponent {
    var name: String
//`components` represents a list of new leafs or composite objects
    private var components = [FileSystemComponent]()

    init(name: String) {
        self.name = name
    }

    func add(component: FileSystemComponent) {
        components.append(component)
    }

    func display(indent: String) {
        print("\(indent)+ \(name)")
        for component in components {
            component.display(indent: indent + "  ")
        }
    }
}

//Finally, the usage of these objecs:
let file1 = File(name: "File1.txt")
let file2 = File(name: "File2.txt")
let file3 = File(name: "File3.txt")

let directory1 = Directory(name: "Directory1")
directory1.add(component: file1)

let directory2 = Directory(name: "Directory2")
directory2.add(component: file2)
directory2.add(component: file3)

let rootDirectory = Directory(name: "RootDirectory")
rootDirectory.add(component: directory1)
rootDirectory.add(component: directory2)

rootDirectory.display(indent: "|")

/*
The result will be the following:
|  + Directory1
|    - File1.txt
|  + Directory2
|    - File2.txt
|    - File3.txt
*/
Enter fullscreen mode Exit fullscreen mode

JavaScript:

class FileSystemComponent {
    constructor(name) {
        this.name = name;
    }

    display(indent) {
        throw new Error("This method must be overridden!");
    }
}

class File extends FileSystemComponent {
    constructor(name) {
        super(name);
    }

    display(indent) {
        console.log(`${indent}- ${this.name}`);
    }
}

class Directory extends FileSystemComponent {
    constructor(name) {
        super(name);
        this.components = [];
    }

    add(component) {
        this.components.push(component);
    }

    display(indent) {
        console.log(`${indent}+ ${this.name}`);
        this.components.forEach(component => {
            component.display(indent + "  ");
        });
    }
}

const file1 = new File("File1.txt");
const file2 = new File("File2.txt");
const file3 = new File("File3.txt");

const directory1 = new Directory("Directory1");
directory1.add(file1);

const directory2 = new Directory("Directory2");
directory2.add(file2);
directory2.add(file3);

const rootDirectory = new Directory("RootDirectory");
rootDirectory.add(directory1);
rootDirectory.add(directory2);

rootDirectory.display("|");

/*
The result will be the following:
|  + Directory1
|    - File1.txt
|  + Directory2
|    - File2.txt
|    - File3.txt
*/
Enter fullscreen mode Exit fullscreen mode

Proxy

This pattern is used to provide controlled access to an object by creating a proxy object that performs actions such as access control, lazy initialization, logging, or caching.
Use case: Image is downloaded from a URL, cached on the device, and displayed in a SwiftUI view.

Swift:

protocol Image {
    func display() -> UIImage?
}

class RealImage: Image {
    private var filename: String
    private var url: URL
    private var image: UIImage?

    init(filename: String, url: URL) {
        self.filename = filename
        self.url = url
        self.image = loadImageFromDisk(filename: filename)
        if self.image == nil {
            self.image = loadImageFromURL(url: url)
        }
    }

    private func loadImageFromDisk(filename: String) -> UIImage? {
        let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let filePath = path.appendingPathComponent(filename)
        return UIImage(contentsOfFile: filePath.path)
    }

    private func loadImageFromURL(url: URL) -> UIImage? {
        guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
            return nil
        }
        saveImageToDisk(image: image, filename: filename)
        return image
    }

    private func saveImageToDisk(image: UIImage, filename: String) {
        let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let filePath = path.appendingPathComponent(filename)
        if let data = image.jpegData(compressionQuality: 1.0) {
            try? data.write(to: filePath)
        }
    }

    func display() -> UIImage? {
        return image
    }
}

class ProxyImage: Image {
    private var realImage: RealImage?
    private var filename: String
    private var url: URL

    init(filename: String, url: URL) {
        self.filename = filename
        self.url = url
    }

    func display() -> UIImage? {
        if realImage == nil {
            realImage = RealImage(filename: filename, url: url)
        }
        return realImage?.display()
    }
}

//Presenting ProxyImage in a SwiftUI view
struct ContentView: View {
    @State private var image: UIImage? = nil
    private let proxyImage = ProxyImage(filename: "kitty.jpg", url: URL(string: "https://www.boredpanda.com/blog/wp-content/uploads/2021/06/CK9sRAEMEo1-png__605.jpg")!)

    var body: some View {
        VStack {
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 300, height: 300)
            } else {
                Text("Loading image...")
                    .onAppear {
                        self.image = self.proxyImage.display()
                    }
            }
        }
    }
}

#Preview {
    ContentView()
}

Enter fullscreen mode Exit fullscreen mode

Here we'll create an entire web page to make understandable and testable at once.

JavaScript:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Proxy Pattern Example</title>
    <style>
        img {
            max-width: 300px;
            height: auto;
        }
    </style>
</head>
<body>
    <div id="app">
        <p>Loading image...</p>
    </div>

    <script>
        class Image {
            display(callback) {
                throw new Error("This method must be overridden!");
            }
        }

        class RealImage extends Image {
            constructor(filename, url) {
                super();
                this.filename = filename;
                this.url = url;
                this.image = this.loadImageFromLocalStorage(filename);
                if (!this.image) {
                    this.loadImageFromURL(url, (img) => {
                        this.image = img;
                        this.saveImageToLocalStorage(filename, img.src);
                        callback(img);
                    });
                } else {
                    callback(this.image);
                }
            }

            loadImageFromLocalStorage(filename) {
                const imageData = localStorage.getItem(filename);
                if (imageData) {
                    const image = new window.Image();
                    image.src = imageData;
                    return image;
                }
                return null;
            }

            loadImageFromURL(url, callback) {
                const xhr = new XMLHttpRequest();
                xhr.open('GET', url, true);
                xhr.responseType = 'blob';
                xhr.onload = function () {
                    if (xhr.status === 200) {
                        const reader = new FileReader();
                        reader.onloadend = function () {
                            const image = new window.Image();
                            image.src = reader.result;
                            callback(image);
                        }
                        reader.readAsDataURL(xhr.response);
                    }
                };
                xhr.send();
            }

            saveImageToLocalStorage(filename, data) {
                localStorage.setItem(filename, data);
            }

            display(callback) {
                if (this.image) {
                    callback(this.image);
                }
            }
        }

        class ProxyImage extends Image {
            constructor(filename, url) {
                super();
                this.realImage = null;
                this.filename = filename;
                this.url = url;
            }

            display(callback) {
                if (this.realImage === null) {
                    this.realImage = new RealImage(this.filename, this.url, callback);
                } else {
                    this.realImage.display(callback);
                }
            }
        }

        document.addEventListener("DOMContentLoaded", () => {
            const proxyImage = new ProxyImage("example.jpg", "./gato2.jpg");
            proxyImage.display((imageElement) => {
                const app = document.getElementById("app");
                app.innerHTML = "";  // Clear the loading text
                app.appendChild(imageElement);
            });
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

These examples shows how to implement and use each of the specified structural patterns in Swift and JavaScript with use cases.

Next post will focus on Behavioral Patterns. See you next time!

Top comments (0)