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
.
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.")
}
}
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");
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
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");
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()
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();
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
*/
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
*/
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()
}
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>
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)