Categories(Types) of Design pattern swift ios
The three main categories of design patterns in Swift iOS are:
Creational patterns: responsibility is to create objects / controls the creation of objects.
- Singleton Pattern: Ensures that a class has only one instance and provides a global point of access to that instance.
- Factory Method Pattern: Defines an interface for creating objects, allowing subclasses to decide which class to instantiate.
- Abstract Factory Pattern: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
- Builder Pattern: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
- Prototype Pattern: Creates new objects by copying an existing object, avoiding the overhead of creating objects from scratch.
Structural patterns: Structural Design pattern is a way to combine or arrange different classes and objects to form a comples or bigger structure to solve a particular requirement.
- Adapter Pattern: Converts the interface of a class into another interface that clients expect, enabling classes with incompatible interfaces to work together.
- Bridge Pattern: Decouples an abstraction from its implementation, allowing both to evolve independently.
- Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies, making it easier to work with individual objects and compositions.
- Decorator Pattern: Dynamically adds responsibilities to objects, providing a flexible alternative to subclassing for extending functionality.
- Facade Pattern: Provides a simplified interface to a complex subsystem, making it easier to use and understand.
- Flyweight Pattern: Shares instances of objects to support large numbers of fine-grained objects efficiently.
- Proxy Pattern: provide a substitute or placeholder for another object to control access to the original object.
Behavioral patterns: These patterns deal with the communication between objects. They provide a way to define the relationships between objects and to coordinate their activities.
- Chain of Responsibility Pattern: Creates a chain of objects that can handle requests, avoiding coupling the sender with its receivers.
- Command Pattern: Turns a request into a stand-alone object, allowing parameterization of clients with different requests.
- Interpreter Pattern: Defines a grammar for a language and an interpreter to interpret sentences in the language.
- Iterator Pattern: Provides a way to access elements of a collection without exposing its underlying representation.
- Mediator Pattern: Defines an object that centralizes communication between multiple objects, reducing direct dependencies between them.
- Memento Pattern: Captures and restores an object’s internal state, allowing it to be restored to a previous state.
- Observer Pattern: Defines a dependency between objects, ensuring that when one object changes state, all its dependents are notified and updated automatically.
- State Pattern: Allows an object to change its behavior when its internal state changes, enabling cleaner, more maintainable conditional logic.
- Strategy Pattern: Defines a family of algorithms, encapsulates each one and makes them interchangeable. Clients can choose an algorithm from this family without modifying their code.
- Template Method Pattern: Defines the structure of an algorithm in a superclass but lets subclasses override specific steps of the algorithm.
- Visitor Pattern: Separates an algorithm from an object structure, allowing new operations to be added without modifying the objects themselves.
Singleton Pattern: The Singleton design pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance throughout the application. In Swift iOS development, it is commonly used to manage shared resources and data that should have a single instance, such as network managers, data managers, logging systems, and more.
Here’s how you can implement the Singleton pattern in Swift:
- Private Initializer: To prevent multiple instances of the class from being created, you need to make the class’s initializer private.
- Static Property for the Shared Instance: Create a static property that holds the single instance of the class. This property should be lazily initialized to ensure the instance is created only when it’s needed.
class Singleton {
static let sharedInstance = Singleton()
private init() {}
func doSomething() {
// Do something here.
}
}
let instance = Singleton.sharedInstance
instance.doSomething()
In this example, the sharedInstance property is a static property that is initialized to a new instance of the Singleton class. The init() method is private, so it cannot be called from outside the class. This ensures that only one instance of the Singleton class can be created.
The doSomething() method is a public method that can be called from anywhere in the code. This method does something with the singleton instance.
Drawbacks of using the singleton pattern:
- Testability: The singleton pattern can make classes more difficult to test. This is because the singleton instance is global, and it can be difficult to mock the instance in unit tests.
- Thread safety: The singleton pattern can be difficult to make thread-safe. This is because the singleton instance is shared between all threads, and it is important to ensure that the instance is accessed and modified in a thread-safe manner.
- Tight coupling: As Singletons are accessed globally, it can lead to tight coupling between classes, making it difficult to change dependencies or substitute implementations.
Factory design pattern:
The Factory design pattern is a creational pattern that provides an interface for creating objects without specifying their concrete classes. It allows the client code to create objects based on certain conditions or configurations, promoting loose coupling and flexibility in the codebase. In Swift iOS, you can implement the Factory pattern using a protocol and a concrete factory class.
Let’s create an example of a simple factory that creates different types of bank card systems with different types of credit cards: Silver, Gold, and Platinum.
Step-by-Step Implementation
- Define the Protocol: Create an abstract
CardType
class with a property and method that subclasses must implement. - Implement Concrete Classes: Create subclasses (
SilverCard
,GoldCard
,PlatinumCard
) that implement thesetCreditLimit()
method. - Create a Factory: Implement a factory class that contains a method to create different card types based on input parameters.
- Use the Factory in Context (e.g., Bank): The
Bank
class can use this factory to create card instances.
// Step 1: Define the protocol
protocol CardType {
var creditLimit: Double { get set }
func setCreditLimit()
}
// Step 2: Concrete classes
class SilverCard: CardType {
var creditLimit: Double = 1000.0
func setCreditLimit() {
// Set the credit limit specific to SilverCard
creditLimit = 1500.0
}
}
class GoldCard: CardType {
var creditLimit: Double = 2000.0
func setCreditLimit() {
// Set the credit limit specific to GoldCard
creditLimit = 2500.0
}
}
class PlatinumCard: CardType {
var creditLimit: Double = 3000.0
func setCreditLimit() {
// Set the credit limit specific to PlatinumCard
creditLimit = 3500.0
}
}
// Step 3: Create the Factory
class CardFactory {
static func createCard(type: String) -> CardType? {
switch type {
case "Silver":
return SilverCard()
case "Gold":
return GoldCard()
case "Platinum":
return PlatinumCard()
default:
return nil
}
}
}
// Step 4: Use the Factory in a Bank class
class Bank {
func issueCard(type: String) -> CardType? {
let card = CardFactory.createCard(type: type)
card?.setCreditLimit() // Set the credit limit for the created card
return card
}
}
// Example Usage
let bank = Bank()
if let myCard = bank.issueCard(type: "Gold") {
print("Issued a \(type(of: myCard)) with a limit of \(myCard.creditLimit)")
}
Example 2:-
protocol Shape {
func draw()
}
class Circle: Shape {
func draw() {
print("Drawing a circle.")
}
}
class Square: Shape {
func draw() {
print("Drawing a square.")
}
}
class Triangle: Shape {
func draw() {
print("Drawing a triangle.")
}
}
Create the ShapeFactory class that implements the factory logic:
class ShapeFactory {
enum ShapeType {
case circle
case square
case triangle
}
static func createShape(type: ShapeType) -> Shape {
switch type {
case .circle:
return Circle()
case .square:
return Square()
case .triangle:
return Triangle()
}
}
}
Usage of the ShapeFactory to create objects:
let shape1 = ShapeFactory.createShape(type: .circle)
shape1.draw() // Output: "Drawing a circle."
let shape2 = ShapeFactory.createShape(type: .square)
shape2.draw() // Output: "Drawing a square."
let shape3 = ShapeFactory.createShape(type: .triangle)
shape3.draw() // Output: "Drawing a triangle."
In this example, the ShapeFactory
acts as a factory for creating different shapes without exposing the concrete implementations of the Circle
, Square
, and Triangle
classes to the client code. The client only needs to know about the Shape
protocol, promoting loose coupling and making it easier to add or modify shapes in the future.
Use Cases of the Factory Pattern
1. Managing Object Creation for Similar Types
When a system needs to create objects of a family with similar properties or behavior but differs in specific attributes. The Factory Pattern encapsulates the creation logic, making the system more maintainable and extensible.
Example: Vehicle Manufacturing
- A car manufacturing system can have a factory that creates vehicles (e.g.,
Car
,Bike
,Truck
) based on the user's input. - This avoids exposing the complex logic for object creation to the client code.
2. Dependency Inversion Principle
When a high-level module should not depend on low-level modules, but both should depend on abstractions. The Factory Pattern ensures that the client code works with interfaces, not specific implementations.
Example: Logger System
- An application may require different loggers (
FileLogger
,DatabaseLogger
,ConsoleLogger
). - A factory can decide which logger to instantiate based on configuration.
The Factory Pattern is a creational design pattern used to create objects without specifying the exact class of the object that will be created. It provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
3. Avoiding Complex Initialization Logic
When object creation requires extensive setup or configuration, the Factory Pattern can hide this complexity from the client code.
Example: Database Connections
- A factory can manage the creation of different database connections (e.g., MySQL, PostgreSQL) based on environment variables or runtime parameters.
4. Dynamic and Configurable Object Creation
When objects need to be created dynamically at runtime based on specific conditions, the Factory Pattern provides a clean way to manage this.
Example: Shape Drawing Application
An application may need to create different shapes (Circle
, Rectangle
, Triangle
) based on user input.
The Factory Pattern is a creational design pattern used to create objects without specifying the exact class of the object that will be created. It provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
5. When Subclass Instantiation is Required
If you need to return objects of specific subclasses depending on the situation, the Factory Pattern helps avoid tight coupling between the code and specific subclasses.
Example: Payment Gateways
- For an e-commerce application, the system might need to support different payment methods (e.g.,
PayPal
,Stripe
,CreditCard
). - The factory can decide which payment method to use at runtime based on the user’s choice.
The Factory pattern becomes especially useful when you have multiple classes that implement a common protocol, and the client code does not need to be aware of the specific concrete class being instantiated. Instead, the factory class takes care of selecting the appropriate implementation based on the input or context.
Builder design pattern
The Builder pattern lets us construct complex objects step by step. The Builder doesn’t allow other objects to access the product while it’s being built.
let’s think about how to create a House
object. To build a simple house, you need to construct four walls and a floor, install a door, fit a pair of windows, and build a roof. But what if you want a bigger, brighter house, with a backyard and other goodies (like a heating system, plumbing, and electrical wiring)?
The simplest solution is to extend the base House
class and create a set of subclasses to cover all combinations of the parameters. But eventually you’ll end up with a considerable number of subclasses.
There’s another approach that doesn’t involve breeding subclasses. You can create a giant constructor right in the base House
class with all possible parameters that control the house object. While this approach indeed eliminates the need for subclasses, it creates another problem.
In most cases most of the parameters will be unused, making the constructor calls pretty ugly. For instance, only a fraction of houses have swimming pools, so the parameters related to swimming pools will be useless nine times out of ten.
Solution
The Builder pattern suggests that you extract the object construction code out of its own class and move it to separate protocol called builders.
The director is only responsible for executing the building steps in a particular sequence. It’s helpful when producing products according to a specific order or configuration.(Like in example: BaseHouse and LuxaryHouse deliverd by director) Strictly speaking, the director class is optional, since the client can control builders directly.
The director works with any builder instance that the client code passes to it. This way, the client code may alter the final type of the newly assembled product. The director can construct several product variations using the same building steps.
// House struct representing the final product
struct House {
var walls: String
var roof: String
var doors: Int
var windows: Int
// ... other properties of a house// Description method for the House
func description() -> String {
return "This house has \(walls) walls, a \(roof) roof, \(doors) doors, and \(windows) windows."
}
}
// HouseBuilder protocol defining the blueprint for building a House
protocol HouseBuilder {
func buildWalls()
func buildRoof()
func buildDoors()
func buildWindows()
func getResult() -> House
}
// Concrete HouseBuilder for a basic house
class BasicHouseBuilder: HouseBuilder {
private var house: House
init() {
house = House(walls: "", roof: "", doors: 0, windows: 0)
}
// Methods to set various parts of the house
func buildWalls(_ walls: String) {
self.walls = walls
}
func buildRoof(_ roof: String) {
self.roof = roof
}
func buildDoors(_ doors: Int) {
self.doors = doors
}
func buildWindows(_ windows: Int) {
self.windows = windows
}
// Method to get the final House object
func getResult() -> House {
return House(walls: walls, roof: roof, doors: doors, windows: windows /*...other properties*/)
}
}
// HouseDirector class to direct the HouseBuilder in constructing houses
class HouseDirector {
private var builder: HouseBuilder
init(builder: HouseBuilder) {
self.builder = builder
}
func constructBasicHouse() -> House {
builder.buildWalls("Brick")
builder.buildRoof("Tiled")
builder.buildDoors(1)
builder.buildWindows(2)
// ... additional steps for a basic house
return builder.getResult()
}
func constructLuxuryHouse() -> House {
let builder = HouseBuilder()
builder.buildWalls("Marble")
builder.buildRoof("Glass")
builder.buildDoors(4)
builder.buildWindows(10)
// ... additional steps for a luxury house
return builder.getResult()
}
}
// Using the HouseDirector to create different types of houses
let director = HouseDirector()
let basicHouse = director.constructBasicHouse()
print(basicHouse.description()) // Output: This house has Brick walls, a Tiled roof, 1 door, and 2 windows.
let luxuryHouse = director.constructLuxuryHouse()
print(luxuryHouse.description()) // Output: This house has Marble walls, a Glass roof, 4 doors, and 10 windows.
Applicability
Use the Builder pattern to get rid of a “telescoping constructor”.
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// ...
Say you have a constructor with ten optional parameters. Calling such a beast is very inconvenient; therefore, you overload the constructor.
The Builder pattern lets you build objects step by step, using only those steps that you really need. After implementing the pattern, you don’t have to cram dozens of parameters into your constructors anymore.
Use the Builder pattern when you want your code to be able to create different representations of some product (for example, stone and wooden houses).
we can use Builder when creating complex Composite trees because you can program its construction steps to work recursively.
Pros and Cons
- You can construct objects step-by-step, defer construction steps or run steps recursively.
- You can reuse the same construction code when building various representations of products.
- Single Responsibility Principle. You can isolate complex construction code from the business logic of the product..
Prototype design pattern
The Prototype design pattern is used to create new objects by copying an existing object, known as the prototype.
This pattern allows you to produce new instances by duplicating or cloning an existing one.
Let’s say we have a Car
class, and we want to create multiple cars with similar properties without constructing them each time from the ground up.
Example 1 :
// Prototype: the object that will be cloned
class Car: NSCopying {
var model: String
var color: String
init(model: String, color: String) {
self.model = model
self.color = color
}
// Implementing NSCopying protocol method
func copy(with zone: NSZone? = nil) -> Any {
return Car(model: self.model, color: self.color)
}
}
// Usage
let originalCar = Car(model: "SUV", color: "Blue")
// Create a new car by cloning the original
if let clonedCar = originalCar.copy() as? Car {
clonedCar.color = "Red" // Modifying the cloned car's color
print("Original Car Color: \(originalCar.color)") // Outputs "Blue"
print("Cloned Car Color: \(clonedCar.color)") // Outputs "Red"
}
Example 2 :
Consider you have a Person
class:
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
Now, let’s implement the Prototype design pattern to create a Person
object by cloning an existing instance.
- Creating a Prototype:
protocol Copyable {
func copy() -> Self
}
extension Person: Copyable {
func copy() -> Self {
return Person(name: self.name, age: self.age)
}
}
- Here, we’ve created a
Copyable
protocol with acopy()
method. We've extended thePerson
class to conform to this protocol. Thecopy()
method creates a new instance ofPerson
with the same properties as the original object.
- Using the Prototype to Create Copies:
let originalPerson = Person(name: "Alice", age: 30)
let clonedPerson = originalPerson.copy()
- We create an original
Person
object namedoriginalPerson
. - Then, we use the
copy()
method to create a clone oforiginalPerson
calledclonedPerson
.
- Validation:
print(originalPerson.name) // Output: "Alice"
print(clonedPerson.name) // Output: "Alice"
- Both
originalPerson
andclonedPerson
have the same values for thename
property, indicating that the clone was successfully created using the Prototype pattern.
Facade design pattern
The Facade design pattern is a structural type of design pattern that is used to hide the system’s complexity from the client. Facade means the face of something. It shields the user or client from the complex details of the system and provides them with a simplified view of it which is easy to use.
The below image shows a complex system with a number of subsystems part of it and the client does not need to know about them, the client just cares about the simplified interface sitting between the client and the system in order to hide the complexity. I am quite confident that if you are working in the industry, you would already be using this design pattern. I reckon it is the simplest and the most widely used design pattern.
The facade interface hides the complexity of the system on the right
Let’s take an example of a Movie ticket booking system. To build the movie ticket booking system, it would require a couple of services to complete its execution end-to-end. let’s first jot down those services.
- Theatre service which returns a number of shows and seats in each show which are available for the given movie.
- Payment service to make the payment against the booked seat(s).
- Invoice service to generate the invoice for the payment made.
- Notification service to send out the required notification to the concerned end user.
There could be many other services, but for the sake of simplicity, we will go with these 4 services (subsystems) of the booking system. Let’s assume the client is a website/mobile app which interacts with our system to book a ticket on the end user’s request.
Problems with the complex system if the Facade design pattern is not implemented.
- The client would need to interact with all the services to complete the ticket booking.
- The client is forced to take care of any change done in any service. Let’s say the Invoice service used to return invoiceId, but now it is modified to return invoiceid along with the ticketId. The client needs to incorporate these changes at its end and it is directly interacting with the invoice service.
- If there is a need for a fifth service to complete the ticket booking, the client needs to invoke that service in its flow.
Facade provides a simple interface with one service to book a ticket
import Foundation
class TheatreService {
func getTheatreWithSeat(_ movieId: String) -> String {
// Dummy implementation
return "Theatre123"
}
}
class PaymentService {
func makePayment(_ theatreId: String, _ userId: String) -> Bool {
// Dummy implementation
return true
}
}
class InvoiceService {
func generateInvoice(_ userId: String, _ theatreId: String) {
// Dummy implementation
print("Invoice generated for user \(userId) at theatre \(theatreId)")
}
}
class NotificationService {
func sendNotification(_ userId: String, _ theatreId: String, success: Bool) {
if success {
print("Notification: Booking successful for user \(userId) at theatre \(theatreId)")
} else {
print("Notification: Booking failed for user \(userId) at theatre \(theatreId)")
}
}
}
class TicketBookingService {
private let theatreService: TheatreService
private let paymentService: PaymentService
private let invoiceService: InvoiceService
private let notificationService: NotificationService
init() {
self.theatreService = TheatreService()
self.paymentService = PaymentService()
self.invoiceService = InvoiceService()
self.notificationService = NotificationService()
}
func book(userId: String, movieId: String) -> String {
let theatreId = theatreService.getTheatreWithSeat(movieId)
let paymentDone = paymentService.makePayment(theatreId, userId)
if paymentDone {
invoiceService.generateInvoice(userId, theatreId)
notificationService.sendNotification(userId, theatreId, success: true)
} else {
notificationService.sendNotification(userId, theatreId, success: false)
}
return theatreId
}
}
// Usage
let ticketService = TicketBookingService()
let theatreId = ticketService.book(userId: "User123", movieId: "Movie456")
print("Theatre ID: \(theatreId)")
Features of Facade Design Pattern
Following are some of the important features of Facade Design Pattern-:
1. Simplified Interface
The Facade Pattern offers a simplified interface to a complex subsystem, making it easier for clients to use. This high-level interface encapsulates the complexities of the subsystem, presenting only the essential functionalities to the client.
2. Loose Coupling
By introducing a facade, the pattern reduces the dependencies between the client and the complex subsystem. This loose coupling enhances the flexibility of the system, making it easier to change the subsystem without affecting the client code.
3. Improved Readability and Usability
The facade provides a clear, high-level interface, which improves the readability and usability of the client code. It abstracts away the intricate details and operations, making the codebase cleaner and more understandable.
4. Encapsulation
The Facade Pattern encapsulates the detailed operations of the subsystem, exposing only what is necessary for the client. This encapsulation hides the internal complexity, ensuring that the subsystem’s intricate workings are not exposed to the client.
5. Layered Architecture
The facade acts as an intermediary layer between the client and the subsystem, promoting a layered architectural approach. This separation of concerns makes the system more modular and easier to maintain.
In summary, the Facade Design Pattern is used to manage and encapsulate complexity, improve maintainability, promote loose coupling, and enhance the overall readability of code in large and intricate software systems.
Real-World Example
A real-world example of the Facade Pattern in Swift is found in the UIKit
framework. When you interact with a complex user interface, you often use high-level interfaces like UIView
or UIViewController
that encapsulate the underlying complexity of rendering and managing the user interface components. These interfaces provide simplified access to complex subsystems for creating and managing UI elements.
Simplifying Access to Complex Systems
Use Case: In software applications with multiple subsystems (e.g., multimedia processing, database connections, or libraries with many functionalities), the Facade pattern creates a single interface to access these functionalities easily.
Example:
- A media conversion tool that uses libraries for decoding, encoding, and file I/O operations can have a
MediaConverterFacade
class. This facade hides the complex steps behind simple methods likeconvertVideo()
orconvertAudio()
.
Providing a Unified API for Libraries
Use Case: When an application uses third-party libraries, the Facade pattern can create a unified API, making it easier to switch or modify the underlying libraries without changing the client code.
Example:
- A payment gateway integration might have a
PaymentFacade
to unify methods likeprocessCreditCardPayment()
orprocessPayPalPayment()
, even if internally it uses different APIs.
// Define the subsystems
class PaymentSubsystem {
func processPayment(amount: Double) {
print("Payment processed for amount: \(amount)")
}
}
class OrderSubsystem {
func placeOrder(items: [String]) {
print("Order placed with items: \(items)")
}
}
class CustomerSubsystem {
func addCustomer(name: String, email: String) {
print("Customer added with name: \(name) and email: \(email)")
}
}
// Define the facade
class OnlineStore {
let paymentSubsystem = PaymentSubsystem()
let orderSubsystem = OrderSubsystem()
let customerSubsystem = CustomerSubsystem()
func checkout(items: [String], amount: Double, name: String, email: String) {
paymentSubsystem.processPayment(amount: amount)
orderSubsystem.placeOrder(items: items)
customerSubsystem.addCustomer(name: name, email: email)
}
}
// Example usage
let onlineStore = OnlineStore()
onlineStore.checkout(items: ["Shirt", "Pants"], amount: 100.0, name: "John Doe", email: "johndoe@example.com")
// Define the API service endpoints
class UserService {
func getUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
// implementation of getUser endpoint
}
}
class ProductService {
func getProduct(id: Int, completion: @escaping (Result<Product, Error>) -> Void) {
// implementation of getProduct endpoint
}
}
class OrderService {
func placeOrder(productIds: [Int], userId: Int, completion: @escaping (Result<Order, Error>) -> Void) {
// implementation of placeOrder endpoint
}
}
// Define the facade
class OnlineStore {
let userService = UserService()
let productService = ProductService()
let orderService = OrderService()
func getUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
userService.getUser(id: id, completion: completion)
}
func getProduct(id: Int, completion: @escaping (Result<Product, Error>) -> Void) {
productService.getProduct(id: id, completion: completion)
}
func placeOrder(productIds: [Int], userId: Int, completion: @escaping (Result<Order, Error>) -> Void) {
orderService.placeOrder(productIds: productIds, userId: userId, completion: completion)
}
}
// Example usage
let onlineStore = OnlineStore()
onlineStore.getUser(id: 1) { result in
// handle result of getUser
}
onlineStore.getProduct(id: 2) { result in
// handle result of getProduct
}
onlineStore.placeOrder(productIds: [3, 4], userId: 5) { result in
// handle result of placeOrder
}
class MyView: UIView {
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let imageView = UIImageView()
init(title: String, description: String, image: UIImage) {
super.init(frame: .zero)
setupTitleLabel(title)
setupDescriptionLabel(description)
setupImageView(image)
// setup constraints for subviews
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: self.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
descriptionLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
descriptionLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
imageView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
private func setupTitleLabel(_ title: String) {
titleLabel.text = title
titleLabel.font = UIFont.boldSystemFont(ofSize: 20)
titleLabel.numberOfLines = 0
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
}
private func setupDescriptionLabel(_ description: String) {
descriptionLabel.text = description
descriptionLabel.font = UIFont.systemFont(ofSize: 16)
descriptionLabel.numberOfLines = 0
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(descriptionLabel)
}
private func setupImageView(_ image: UIImage) {
imageView.image = image
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
}
func update(title: String?, description: String?, image: UIImage?) {
if let newTitle = title {
titleLabel.text = newTitle
}
if let newDescription = description {
descriptionLabel.text = newDescription
}
if let newImage = image {
imageView.image = newImage
}
}
}
By using the Facade pattern in this way, we are able to provide a simplified UIView for the client code that abstracts away the complexity of interacting with the subviews and layout, and makes it easier to use the custom view as a whole.
class RootViewController: UIViewController {
private let loginViewController = LoginViewController()
private let homeViewController = HomeViewController()
override func viewDidLoad() {
super.viewDidLoad()
// setup subviews
addChild(loginViewController)
view.addSubview(loginViewController.view)
loginViewController.didMove(toParent: self)
addChild(homeViewController)
view.addSubview(homeViewController.view)
homeViewController.didMove(toParent: self)
// setup constraints for subviews
NSLayoutConstraint.activate([
loginViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
loginViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
loginViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
loginViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
homeViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
homeViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
homeViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
homeViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// show login view controller
showLoginViewController()
}
private func showLoginViewController() {
loginViewController.view.isHidden = false
homeViewController.view.isHidden = true
}
private func showHomeViewController() {
loginViewController.view.isHidden = true
homeViewController.view.isHidden = false
}
func login() {
// perform login logic
// ...
// show home view controller
showHomeViewController()
}
func logout() {
// perform logout logic
// ...
// show login view controller
showLoginViewController()
}
}
By using the Facade pattern in this way, we are able to provide a simplified interface for the client code to interact with the login and home view controllers, and abstract away the complexity of managing the subview controllers and their views.
class MyViewModel {
private let apiService: ApiService
private let cacheService: CacheService
init(apiService: ApiService, cacheService: CacheService) {
self.apiService = apiService
self.cacheService = cacheService
}
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
if let cachedData = cacheService.getCachedData() {
completion(.success(cachedData))
} else {
apiService.fetchData { [weak self] result in
switch result {
case .success(let data):
self?.cacheService.cacheData(data)
completion(.success(data))
case .failure(let error):
completion(.failure(error))
}
}
}
}
}
class VideoEditor {
private let audioService: AudioService
private let videoService: VideoService
private let exportService: ExportService
init(audioService: AudioService, videoService: VideoService, exportService: ExportService) {
self.audioService = audioService
self.videoService = videoService
self.exportService = exportService
}
func exportVideo(from url: URL, with audio: Bool, completion: @escaping (Result<URL, Error>) -> Void) {
videoService.loadVideo(from: url) { [weak self] result in
switch result {
case .success(let videoAsset):
if audio {
self?.audioService.loadAudio { [weak self] result in
switch result {
case .success(let audioAsset):
self?.videoService.addAudio(to: videoAsset, with: audioAsset) { [weak self] result in
switch result {
case .success(let composedVideoAsset):
self?.exportService.exportVideo(asset: composedVideoAsset) { result in
completion(result)
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
} else {
self?.exportService.exportVideo(asset: videoAsset) { result in
completion(result)
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
Adapter design pattern
The Adapter Design Pattern, it helps incompatible interfaces to work together. Think of it like a charging adapter that lets you plug a device with one type of plug into an outlet with a different type of socket.
Real-World Example: Charging Adapter
Scenario: You have a new laptop with a USB-C port, but you only have a charger with an old USB-A plug. You need to use an adapter to connect your laptop to the charger.
Key Components:
- Client (Your Laptop): Needs a USB-C charger.
- Adaptee (Old USB-A Charger): Has a USB-A plug.
- Adapter (Charging Adapter): Converts the USB-A plug to a USB-C plug.
Laptop (Client) --> Charging Adapter (Adapter) --> Old USB-A Charger (Adaptee)
[USB-C Port] <-- [USB-C to USB-A Converter] <-- [USB-A Plug]
Swift Code Example
Here’s a simple implementation of the Adapter pattern in Swift:
// MARK: - The Target Protocol (Desired Interface)
protocol USBCPort {
func connectWithUSBCCable()
}
// MARK: - The Adaptee (Existing Component with Incompatible Interface)
class OldUSBCharger {
func connectWithUSBCable() {
print("Charging with USB-A cable.")
}
}
// MARK: - The Adapter (Makes the Adaptee Compatible with the Target)
class USBAdapter: USBCPort {
private var oldCharger: OldUSBCharger
init(oldCharger: OldUSBCharger) {
self.oldCharger = oldCharger
}
func connectWithUSBCCable() {
// Adapter translates the request to the adaptee's method
oldCharger.connectWithUSBCable()
}
}
// MARK: - Client Code
let oldCharger = OldUSBCharger()
let adapter = USBAdapter(oldCharger: oldCharger)
// The laptop (client) uses the USB-C interface to charge
adapter.connectWithUSBCCable()
Explanation of Code:
- USBPort (Target Interface): The desired interface (
USBPort
) that the client (laptop) expects. - OldUSBCharger (Adaptee): The existing component that has a different interface (
connectWithUSBCable
). - USBAdapter (Adapter): Adapts the
OldUSBCharger
to be compatible with theUSBPort
interface. - Client Code: Uses the adapter to connect the laptop with the old charger.
Summary:
- Adapter Pattern: Allows incompatible interfaces to work together.
- Real-World Analogy: A charging adapter converting USB-A to USB-C.
- Key Components: Client, Adaptee, Adapter.
Decorator Design Pattern
The Decorator Pattern is like dressing up an object in layers of clothing. Each piece of clothing adds something new without changing the person inside (the object).
Imagine :
- Person (Component): This is the core object. Think of it as a simple person (or object) with basic features.
- Clothes (Decorator): Each piece of clothing you add (like a jacket or hat) adds something new to the person. It doesn’t change who they are but adds extra style or functionality.
- Layering (ConcreteDecorator): You can keep adding clothes (layers) on top of the person to keep making them look different without altering the original person.
Swift Example in Simple Steps:
Let’s say we start with a simple coffee and want to add extras like milk and sugar:
- Start with plain coffee (Component):
class SimpleCoffee {
func cost() -> Double { return 5.0 }
func description() -> String { return "Simple Coffee" } }
2. Now add some milk (Decorator):
class MilkDecorator {
private var coffee: SimpleCoffee
init(coffee: SimpleCoffee) { self.coffee = coffee }
func cost() -> Double { return coffee.cost() + 1.5 }
func description() -> String { return coffee.description() + ", Milk" } }
3. Add some sugar (Decorator):
class SugarDecorator {
private var coffee: SimpleCoffee
init(coffee: SimpleCoffee) {
self.coffee = coffee
}
func cost() -> Double { return coffee.cost() + 0.5 }
func description() -> String { return coffee.description() + ", Sugar" } }
4. Layering extras (ConcreteDecorator):
- Start with Simple Coffee:
"Simple Coffee"
= cost: 5.0 - Add Milk:
"Simple Coffee, Milk"
= cost: 6.5 - Add Sugar:
"Simple Coffee, Milk, Sugar"
= cost: 7.0
Key Takeaway:
- The base object stays the same, but by layering “decorators” (like clothes or add-ons), you add extra features, like milk or sugar to coffee.
- You don’t change the original object, you just wrap it to add new behavior.
Composite Design Pattern
UML Diagram:
Allows to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
In Simple: This pattren help in a scenerio we have object inside object (tree like structure)
Components of the Composite Pattern
- Component: An interface or abstract class declaring operations common to both simple and complex elements.
- Leaf: A concrete class that implements the Component interface. Represents individual objects in the composition.
- Composite: A concrete class that also implements the Component interface and contains a collection of Leaf objects and/or other Composite objects. It defines behavior for components having children.
Let’s illustrate the Composite Design Pattern with a simple example of a file system, where File
is a Leaf and Directory
is a Composite.
import Foundation
// Component
protocol FileSystemItem {
func getSize() -> Int
}
// Leaf
class File: FileSystemItem {
var name: String
var size: Int
init(name: String, size: Int) {
self.name = name
self.size = size
}
func getSize() -> Int {
return size
}
}
// Composite
class Directory: FileSystemItem {
var name: String
var items: [FileSystemItem] = []
init(name: String) {
self.name = name
}
func add(item: FileSystemItem) {
items.append(item)
}
func remove(item: FileSystemItem) {
items.removeAll { \$0 as AnyObject === item as AnyObject }
}
func getSize() -> Int {
// Sum sizes of all items in the directory
return items.reduce(0) { \$0 + \$1.getSize() }
}
}
// Example usage
let file1 = File(name: "File1.txt", size: 100)
let file2 = File(name: "File2.txt", size: 200)
let directory1 = Directory(name: "Directory1")
directory1.add(item: file1)
directory1.add(item: file2)
let directory2 = Directory(name: "Directory2")
let file3 = File(name: "File3.txt", size: 300)
directory2.add(item: directory1)
directory2.add(item: file3)
// Output sizes
print("Size of \(directory1.name): \(directory1.getSize()) bytes") // Outputs: 300
print("Size of \(directory2.name): \(directory2.getSize()) bytes") // Outputs: 600
Explanation
- FileSystemItem: This protocol defines the common interface for both files and directories, specifically the
getSize()
method. - File: A concrete class implementing
FileSystemItem
. EachFile
has a name and size. - Directory: Another concrete class implementing
FileSystemItem
, but it can containFile
objects (leaves) and otherDirectory
objects (composites). ThegetSize()
method adds up the sizes of all contained items.
Benefits of the Composite Pattern
- Uniformity: Clients can work with components (leaves and composites) in a uniform way.
- Tree Structure: Representing complex hierarchical structures becomes simpler and more manageable.
- Flexibility: You can add or remove components without affecting client code.
Come common use cases:
1. File Systems
- Use Case: Managing directories and files in a file system.
- Explanation: A
Directory
can contain bothFile
objects and otherDirectory
objects, allowing clients to navigate and perform operations such as calculating total size or searching for files without needing to know the difference between files and directories.
2. Graphic User Interfaces (GUIs)
- Use Case: Building complex UI components.
- Explanation: UI elements like panels, buttons, and windows can be combined into nested structures. For example, a
Panel
can containButton
elements and otherPanel
elements, making it easy to manage layout and behavior as a single composite item.
3. Document Representation
- Use Case: Representing documents composed of sections, paragraphs, and plain text elements.
- Explanation: Each section can contain paragraphs and images, while paragraphs can contain text. This allows for uniform handling of different elements when performing operations like exporting or rendering.
4. Organization Structures
- Use Case: Displaying hierarchical relationships within an organization.
- Explanation: Employees (leaves) can be grouped into teams or departments (composites). Each department can have sub-departments, allowing for a clear representation of relationships and collective operations like calculating total salaries or reporting structures.
5. Game Development
- Use Case: Managing game characters and groups of characters.
- Explanation: A game character can have individual attributes, while groups of characters (like a team) can aggregate behaviors or properties, allowing for operations like moving all characters or applying effects to a group.
6. Company Assets Management
- Use Case: Organizing physical or digital assets in a company.
- Explanation: Assets can be categorized into individual items (like computers) and groups (like departments’ assets), enabling operations like inventory management or reporting.
7. Menu Systems
- Use Case: Building hierarchical menu systems for applications.
- Explanation: Menus can contain submenus and menu items. This structure allows handling interactions uniformly, whether they are leaf menu items or composite submenus.
8. Network Protocols
- Use Case: Representing protocols with various layers.
- Explanation: Protocol layers (like TCP/IP) can consist of multiple protocols and sub-protocols. Clients can interact with both individual protocols and composite structures through a uniform interface.
9. Data Processing Pipelines
- Use Case: Implementing processing steps in data transformation or analysis.
- Explanation: Processing steps can be simple data transforms (leaves) or complex grouped transformations (composites) that apply a series of operations, enabling flexible and manageable workflows.
Ref link: https://refactoring.guru/design-patterns/composite
Bridge design pattern
The Bridge design pattern is a structural pattern used to separate the abstraction (interface) from its implementation so that both can vary independently. It allows the two to evolve separately and makes it easier to extend and maintain large systems by decoupling abstractions and implementations.
In simpler terms, the Bridge pattern enables you to create two hierarchies: one for the abstraction (interface or high-level functionality) and another for the implementation (concrete implementation details). It helps in avoiding a direct coupling between them, allowing changes in one hierarchy to not affect the other.
Here’s an example of the Bridge pattern in Swift for an entertainment system with devices and remote controls:
- Device Implementations (Implementor):
// Implementor Protocol
protocol Device {
func isEnabled() -> Bool
func enable()
func disable()
func getVolume() -> Int
func setVolume(_ volume: Int)
}
// Concrete Implementations
class TV: Device {
var enabled = false
var volume = 10
func isEnabled() -> Bool {
return enabled
}
func enable() {
enabled = true
}
func disable() {
enabled = false
}
func getVolume() -> Int {
return volume
}
func setVolume(_ volume: Int) {
self.volume = volume
}
}
class Radio: Device {
var on = false
var volume = 5
func isEnabled() -> Bool {
return on
}
func enable() {
on = true
}
func disable() {
on = false
}
func getVolume() -> Int {
return volume
}
func setVolume(_ volume: Int) {
self.volume = volume
}
}
- Abstraction (Abstraction):
// Abstraction Protocol
protocol RemoteControl {
var device: Device { get set }
func togglePower()
func volumeUp()
func volumeDown()
}
// Refined Abstraction
class BasicRemote: RemoteControl {
var device: Device
init(device: Device) {
self.device = device
}
func togglePower() {
if device.isEnabled() {
device.disable()
} else {
device.enable()
}
}
func volumeUp() {
let volume = device.getVolume()
device.setVolume(volume + 1)
}
func volumeDown() {
let volume = device.getVolume()
device.setVolume(volume - 1)
}
}
- Usage:
let tv = TV()
let tvRemote = BasicRemote(device: tv)
tvRemote.togglePower() // Turns on TV
tvRemote.volumeUp() // Increases TV volume
tvRemote.volumeDown() // Decreases TV volume
let radio = Radio()
let radioRemote = BasicRemote(device: radio)
radioRemote.togglePower() // Turns on Radio
radioRemote.volumeUp() // Increases Radio volume
radioRemote.volumeDown() // Decreases Radio volume
In this example:
Device
protocol represents the implementation hierarchy (TV and Radio are concrete implementations).RemoteControl
protocol represents the abstraction hierarchy (BasicRemote is a refined abstraction).- The
BasicRemote
class acts as a remote control that can control different devices, and it operates independently of the device's actual implementation.
This example demonstrates how the Bridge pattern separates the abstraction (remote control) from its implementation (TV and Radio), allowing them to vary independently. Adding new devices or remote controls won’t affect the existing implementations, promoting flexibility and ease of extension in large systems.
Memento design pattern
Provides an ability to revert an object to a previous state i.e: undo capacity
The Memento design pattern is a behavioral pattern used to capture and store the current state of an object so that it can be restored later without breaking encapsulation. This is particularly useful for implementing undo and redo operations.
Imagine you’re working on a drawing app where users can create masterpieces. The Memento pattern allows you to capture the state of their drawing at any point in time (like a snapshot) and restore it later if they make a mistake or want to experiment.
Components of the Memento Pattern
- Originator (
DrawingCanvas
): The object whose state needs to be saved and restored. - Memento (
DrawingMemento
): The object that stores the state of theDrawingCanvas
. - Caretaker (
DrawingViewController
): The object that keeps track of the mementos (snapshots of the state).
Benefits:
- Undo/Redo Functionality: Easily implement undo/redo functionality in your app.
- Save/Restore State: Allow users to save and restore the state of their work across sessions.
- Encapsulation: Keeps the originator’s internal state hidden from other objects.
Example: Drawing App
// Originator: DrawingCanvas
// The DrawingCanvas is where you draw lines.
// It has methods to add lines, create mementos (snapshots of its state),
// and restore its state from a memento.
class DrawingCanvas {
private var lines: [Line] = [] // Array of lines drawn
func addLine(_ line: Line) {
lines.append(line)
}
// Memento creation (capturing state)
func createMemento() -> DrawingMemento {
return DrawingMemento(lines: lines)
}
// Restore state from memento
func restore(from memento: DrawingMemento) {
lines = memento.lines
}
}
// Memento: DrawingMemento
// The DrawingMemento holds the state of the DrawingCanvas.
// It's implemented as a simple structure containing an array of lines.
struct DrawingMemento {
private let lines: [Line]
init(lines: [Line]) {
self.lines = lines
}
}
struct Line {
// Properties of a line (e.g., start point, end point, color)
}
// Caretaker: DrawingViewController
// The DrawingViewController manages the drawing actions and
// maintains a history of mementos to implement undo functionality.
class DrawingViewController: UIViewController {
private let canvas = DrawingCanvas()
private var mementos: [DrawingMemento] = [] // Stack of snapshots
@IBAction func drawLine(_ sender: Any) {
// Add a new line to the canvas
canvas.addLine(Line()) // Replace with your line creation logic
}
@IBAction func undo(_ sender: Any) {
if !mementos.isEmpty {
let memento = mementos.popLast()! // Get the latest snapshot
canvas.restore(from: memento) // Restore state from the snapshot
}
}
@IBAction func saveState(_ sender: Any) {
mementos.append(canvas.createMemento()) // Create and store a snapshot
}
}
In Short
- DrawingCanvas (Originator):
addLine(_:)
: Adds lines to the canvas.createMemento()
: Captures the current state in a memento.restore(from:)
: Restores the state from a memento.
2. DrawingMemento (Memento):
- Holds the state of the canvas (the lines).
3. DrawingViewController (Caretaker):
- Manages the drawing actions.
- Stores and retrieves mementos to implement undo functionality.
GitHub link: https://github.com/mrlegowatch/HeadFirstDesignPatternsSwift/tree/master/Swift
Visualize their interaction.
- For Adapter, create a class to adapt an incompatible API.
- For Bridge, separate the abstraction of a device (like a TV) and its implementation (Samsung, LG).
- For Composite, use a file system hierarchy.
- For Decorator, implement a dynamic feature-enhancing class for a base object.
- For Facade, encapsulate complex operations into a simple method.
- For Flyweight, implement a character pool for a text editor.
- For Proxy, simulate a proxy for accessing remote services.
Awesome Buildings Can Design Flawless Foundations Properly:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Adapter
Match interfaces of different classes - Bridge
Separates an object’s interface from its implementation - Composite
A tree structure of simple and composite objects - Decorator
Add responsibilities to objects dynamically - Facade
A single class that represents an entire subsystem - Flyweight
A fine-grained instance used for efficient sharing - Private Class Data
Restricts accessor/mutator access - Proxy
An object representing another object