The Visitor Pattern

What is a Visitor Pattern? The Visitor Pattern helps add new capabilities to a composite of objects. Source What problems does it solve? The Visitor Pattern helps solve following problems: Separation of Concerns: The Visitor Pattern separates algorithms from the objects on which they operate. This allows for clean code organization by keeping algorithms and operations separate from the data structures they operate on. Extensibility: It allows you to add new operations to existing object structures without modifying those structures. This is especially useful when dealing with complex object hierarchies where adding new functionality directly to the classes would lead to code bloat and tight coupling. Traversal of Object Structures: It provides a way to traverse complex object structures while performing some action on each element of the structure. This is particularly useful in scenarios where you need to process every element of a data structure in a specific order or with a specific algorithm. Real-world code example // Element protocol representing the items on the menu protocol MenuItem { func accept(visitor: OrderVisitor) } // Concrete item types class Coffee: MenuItem { let name: String let price: Double init(name: String, price: Double) { self.name = name self.price = price } func accept(visitor: OrderVisitor) { visitor.visit(self) } } class Tea: MenuItem { let name: String let price: Double init(name: String, price: Double) { self.name = name self.price = price } func accept(visitor: OrderVisitor) { visitor.visit(self) } } class Pastry: MenuItem { let name: String let price: Double init(name: String, price: Double) { self.name = name self.price = price } func accept(visitor: OrderVisitor) { visitor.visit(self) } } // Visitor protocol defining the operations to be performed on menu items protocol OrderVisitor { func visit(_ item: Coffee) func visit(_ item: Tea) func visit(_ item: Pastry) } // Concrete visitor implementing operations on menu items class TotalCostVisitor: OrderVisitor { var totalCost = 0.0 func visit(_ item: Coffee) { totalCost += item.price } func visit(_ item: Tea) { totalCost += item.price } func visit(_ item: Pastry) { totalCost += item.price } } class ItemDetailsVisitor: OrderVisitor { var details = "" func visit(_ item: Coffee) { details += "Coffee: \(item.name), Price: $\(item.price)\n" } func visit(_ item: Tea) { details += "Tea: \(item.name), Price: $\(item.price)\n" } func visit(_ item: Pastry) { details += "Pastry: \(item.name), Price: $\(item.price)\n" } } // Example usage let items: [MenuItem] = [Coffee(name: "Espresso", price: 2.5), Tea(name: "Green Tea", price: 2.0), Pastry(name: "Croissant", price: 3.0)] let totalCostVisitor = TotalCostVisitor() for item in items { item.accept(visitor: totalCostVisitor) } print("Total cost of the order: $\(totalCostVisitor.totalCost)") let itemDetailsVisitor = ItemDetailsVisitor() for item in items { item.accept(visitor: itemDetailsVisitor) } print("Order details:") print(itemDetailsVisitor.details)

April 1, 2024 · 3 min · Dmytro Chumakov

The Memento Pattern

What is a Memento Pattern? The Memento Pattern helps return an object to one of its previous states; for instance, if the user requests an “undo” operation. Source What problems does it solve? The Memento Pattern helps solve following problems: Undo/Redo Functionality: Memento allows you to capture an object’s state at a specific point in time and store it externally. This enables you to implement undo/redo functionality by restoring the object to its previous state. Checkpointing: In applications where users need to save progress or create checkpoints (such as in games or long processes), the Memento Pattern allows you to save the state of an object at various intervals so that users can return to those points later. Real-world code example // Memento: Represents the state of the TextEditor struct TextEditorMemento { let text: String } // Originator: Creates and stores states in Memento objects class TextEditor { private var text: String = "" func setText(_ text: String) { self.text = text } func getText() -> String { return text } func save() -> TextEditorMemento { return TextEditorMemento(text: text) } func restore(fromMemento memento: TextEditorMemento) { self.text = memento.text } } // Caretaker: Manages the mementos class TextEditorHistory { private var history: [TextEditorMemento] = [] private let editor: TextEditor init(editor: TextEditor) { self.editor = editor } func save() { let snapshot = editor.save() history.append(snapshot) } func undo() { guard let lastSnapshot = history.popLast() else { print("Nothing to undo.") return } editor.restore(fromMemento: lastSnapshot) } func printHistory() { print("Text Editor History:") for (index, snapshot) in history.enumerated() { print("Step \(index + 1): \(snapshot.text)") } print("Current text: \(editor.getText())") } } // Example usage let textEditor = TextEditor() let history = TextEditorHistory(editor: textEditor) textEditor.setText("Hello, World!") history.save() textEditor.setText("This is a Swift example.") history.save() textEditor.setText("Using Memento Pattern.") history.save() history.printHistory() history.undo() print("After Undo:") history.printHistory() Thank you for reading! 😊

March 22, 2024 · 2 min · Dmytro Chumakov

The Interpreter Pattern

What is an Interpreter Pattern? The Interpreter Pattern helps implement a simple language and defines a class based representation for its grammar along with an interpreter to interpret its sentences. Source What problems does it solve? The Interpreter Pattern helps solve following problems: Language Interpretation: When you have a language or syntax that needs to be interpreted, such as mathematical expressions, regular expressions, or domain-specific languages (DSLs), the Interpreter Pattern helps in implementing the logic to interpret and execute these expressions. Extensibility: The Interpreter Pattern allows for easy addition of new grammar rules or language constructs without modifying the core interpreter logic. This promotes extensibility, enabling the interpreter to support new features or languages with minimal changes. Separation of Concerns: It separates the grammar definition from the interpretation logic. This separation of concerns makes the codebase modular and easier to maintain. Changes to the grammar or language rules do not affect the interpretation logic, and vice versa. Real-world code example // Define the protocol for the expression protocol Expression { func interpret() -> Int } // Concrete expression for a number class NumberExpression: Expression { private var value: Int init(_ value: Int) { self.value = value } func interpret() -> Int { return value } } // Concrete expression for addition class AdditionExpression: Expression { private var left: Expression private var right: Expression init(_ left: Expression, _ right: Expression) { self.left = left self.right = right } func interpret() -> Int { return left.interpret() + right.interpret() } } // Concrete expression for subtraction class SubtractionExpression: Expression { private var left: Expression private var right: Expression init(_ left: Expression, _ right: Expression) { self.left = left self.right = right } func interpret() -> Int { return left.interpret() - right.interpret() } } // Concrete expression for multiplication class MultiplicationExpression: Expression { private var left: Expression private var right: Expression init(_ left: Expression, _ right: Expression) { self.left = left self.right = right } func interpret() -> Int { return left.interpret() * right.interpret() } } // Concrete expression for division class DivisionExpression: Expression { private var left: Expression private var right: Expression init(_ left: Expression, _ right: Expression) { self.left = left self.right = right } func interpret() -> Int { let divisor = right.interpret() if divisor != 0 { return left.interpret() / divisor } else { // Handle division by zero error fatalError("Division by zero") } } } // Usage let expression = AdditionExpression( MultiplicationExpression(NumberExpression(2), NumberExpression(3)), DivisionExpression(NumberExpression(10), NumberExpression(5)) ) // Interpret the expression let result = expression.interpret() print("Result: \(result)") Thank you for reading! 😊

March 18, 2024 · 2 min · Dmytro Chumakov

The Flyweight Pattern

What is a Flyweight Pattern? The Flyweight Pattern refers to an object that minimizes memory usage by sharing some of its data with other similar objects. Source What problems does it solve? The Flyweight Pattern helps solve following problems: Large Memory Footprint: When dealing with a large number of objects, especially if these objects share a significant amount of common state, traditional object creation can lead to excessive memory consumption. The Flyweight Pattern reduces memory usage by sharing this common state among multiple objects. Performance Overhead: Creating and managing a large number of objects can also introduce performance overhead due to memory allocation, deallocation, and initialization. By reusing shared objects and minimizing the creation of new objects, the Flyweight Pattern can improve performance. Object Creation Cost: Creating new objects can be costly in terms of time and resources, especially if the objects require complex initialization. By reusing existing objects, the Flyweight Pattern reduces the need for creating new objects, thereby reducing object creation costs. Real-world code example // Flyweight protocol defining the interface for shapes protocol Shape { func draw(at point: CGPoint) } // Concrete flyweight class representing a circle class Circle: Shape { private let radius: CGFloat private let fillColor: UIColor init(radius: CGFloat, fillColor: UIColor) { self.radius = radius self.fillColor = fillColor } func draw(at point: CGPoint) { print("Drawing Circle at (\(point.x), \(point.y)) with radius \(radius) and fill color \(fillColor)") // Actual drawing logic would go here } } // Flyweight factory class responsible for creating and managing flyweight objects class ShapeFactory { private var flyweights = [String: Shape]() func getCircle(radius: CGFloat, fillColor: UIColor) -> Shape { let key = "Circle-\(radius)-\(fillColor)" if let existingShape = flyweights[key] { return existingShape } else { let newShape = Circle(radius: radius, fillColor: fillColor) flyweights[key] = newShape return newShape } } } // Client code let shapeFactory = ShapeFactory() // Request for circles with different properties let circle1 = shapeFactory.getCircle(radius: 10, fillColor: .red) let circle2 = shapeFactory.getCircle(radius: 10, fillColor: .red) // Reusing the same circle object let circle3 = shapeFactory.getCircle(radius: 20, fillColor: .blue) // Drawing circles circle1.draw(at: CGPoint(x: 100, y: 100)) circle2.draw(at: CGPoint(x: 200, y: 200)) circle3.draw(at: CGPoint(x: 300, y: 300)) Thank you for reading! 😊

March 17, 2024 · 2 min · Dmytro Chumakov

The Chain Of Responsibility Pattern

What is a Chain Of Responsibility Pattern? The Chain Of Responsibility Pattern helps create a chain of objects to examine requests. Each object in turn examines a request and either handles it or passes onto the next object in the chain. Source What problems does it solve? The Chain Of Responsibility Pattern (CoR) helps solve following problems: Dynamic Request Handling: It enables dynamic assignment of responsibilities at runtime. Handlers can be added, removed, or reordered without affecting the client’s code. This flexibility allows for easier maintenance and extension of the system. Decoupling Sender and Receiver: In traditional systems, a sender often needs to know the exact receiver of a request, leading to tight coupling between them. The CoR pattern decouples senders from receivers by allowing multiple objects to handle a request without the sender knowing the specific handler. Real-world code example // Protocol defining the handler interface protocol PurchaseHandler { var next: PurchaseHandler? { get set } func handleRequest(amount: Double) } // Concrete handlers class SmallPurchaseHandler: PurchaseHandler { var next: PurchaseHandler? let maxAmount: Double = 100.0 func handleRequest(amount: Double) { if amount <= maxAmount { print("SmallPurchaseHandler: Purchase approved for $\(amount)") } else if let nextHandler = next { print("SmallPurchaseHandler: Passing request to next handler") nextHandler.handleRequest(amount: amount) } else { print("SmallPurchaseHandler: No handler available, purchase rejected") } } } class MediumPurchaseHandler: PurchaseHandler { var next: PurchaseHandler? let maxAmount: Double = 500.0 func handleRequest(amount: Double) { if amount <= maxAmount { print("MediumPurchaseHandler: Purchase approved for $\(amount)") } else if let nextHandler = next { print("MediumPurchaseHandler: Passing request to next handler") nextHandler.handleRequest(amount: amount) } else { print("MediumPurchaseHandler: No handler available, purchase rejected") } } } class LargePurchaseHandler: PurchaseHandler { var next: PurchaseHandler? func handleRequest(amount: Double) { print("LargePurchaseHandler: Purchase approved for $\(amount)") } } // Usage func main() { let smallHandler = SmallPurchaseHandler() let mediumHandler = MediumPurchaseHandler() let largeHandler = LargePurchaseHandler() // Connecting handlers into a chain smallHandler.next = mediumHandler mediumHandler.next = largeHandler smallHandler.handleRequest(amount: 50.0) smallHandler.handleRequest(amount: 200.0) smallHandler.handleRequest(amount: 1000.0) } Thank you for reading! 😊

March 15, 2024 · 2 min · Dmytro Chumakov