Flyweight is a structural design pattern that allows programs to support vast quantities of objects by keeping their memory consumption low.
The pattern achieves it by sharing parts of object state between multiple objects. In other words, the Flyweight saves RAM by caching the same data used by different objects.
Conceptual Example
This example illustrates the structure of the Flyweight design pattern and focuses on the following questions:
What classes does it consist of?
What roles do these classes play?
In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world Swift use case.
Example.swift: Conceptual example
import XCTest
/// The Flyweight stores a common portion of the state (also called intrinsic
/// state) that belongs to multiple real business entities. The Flyweight
/// accepts the rest of the state (extrinsic state, unique for each entity) via
/// its method parameters.
class Flyweight {
private let sharedState: [String]
init(sharedState: [String]) {
self.sharedState = sharedState
}
func operation(uniqueState: [String]) {
print("Flyweight: Displaying shared (\(sharedState)) and unique (\(uniqueState) state.\n")
}
}
/// The Flyweight Factory creates and manages the Flyweight objects. It ensures
/// that flyweights are shared correctly. When the client requests a flyweight,
/// the factory either returns an existing instance or creates a new one, if it
/// doesn't exist yet.
class FlyweightFactory {
private var flyweights: [String: Flyweight]
init(states: [[String]]) {
var flyweights = [String: Flyweight]()
for state in states {
flyweights[state.key] = Flyweight(sharedState: state)
}
self.flyweights = flyweights
}
/// Returns an existing Flyweight with a given state or creates a new one.
func flyweight(for state: [String]) -> Flyweight {
let key = state.key
guard let foundFlyweight = flyweights[key] else {
print("FlyweightFactory: Can't find a flyweight, creating new one.\n")
let flyweight = Flyweight(sharedState: state)
flyweights.updateValue(flyweight, forKey: key)
return flyweight
}
print("FlyweightFactory: Reusing existing flyweight.\n")
return foundFlyweight
}
func printFlyweights() {
print("FlyweightFactory: I have \(flyweights.count) flyweights:\n")
for item in flyweights {
print(item.key)
}
}
}
extension Array where Element == String {
/// Returns a Flyweight's string hash for a given state.
var key: String {
return self.joined()
}
}
class FlyweightConceptual: XCTestCase {
func testFlyweight() {
/// The client code usually creates a bunch of pre-populated flyweights
/// in the initialization stage of the application.
let factory = FlyweightFactory(states:
[
["Chevrolet", "Camaro2018", "pink"],
["Mercedes Benz", "C300", "black"],
["Mercedes Benz", "C500", "red"],
["BMW", "M5", "red"],
["BMW", "X6", "white"]
])
factory.printFlyweights()
/// ...
addCarToPoliceDatabase(factory,
"CL234IR",
"James Doe",
"BMW",
"M5",
"red")
addCarToPoliceDatabase(factory,
"CL234IR",
"James Doe",
"BMW",
"X1",
"red")
factory.printFlyweights()
}
func addCarToPoliceDatabase(
_ factory: FlyweightFactory,
_ plates: String,
_ owner: String,
_ brand: String,
_ model: String,
_ color: String) {
print("Client: Adding a car to database.\n")
let flyweight = factory.flyweight(for: [brand, model, color])
/// The client code either stores or calculates extrinsic state and
/// passes it to the flyweight's methods.
flyweight.operation(uniqueState: [plates, owner])
}
}
Output.txt: Execution result
FlyweightFactory: I have 5 flyweights:
Mercedes BenzC500red
ChevroletCamaro2018pink
Mercedes BenzC300black
BMWX6white
BMWM5red
Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (["BMW", "M5", "red"]) and unique (["CL234IR", "James Doe"] state.
Client: Adding a car to database.
FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared (["BMW", "X1", "red"]) and unique (["CL234IR", "James Doe"] state.
FlyweightFactory: I have 6 flyweights:
Mercedes BenzC500red
BMWX1red
ChevroletCamaro2018pink
Mercedes BenzC300black
BMWX6white
BMWM5red
Real World Example
Example.swift: Real world example
import XCTest
import UIKit
class FlyweightRealWorld: XCTestCase {
func testFlyweightRealWorld() {
let maineCoon = Animal(name: "Maine Coon",
country: "USA",
type: .cat)
let sphynx = Animal(name: "Sphynx",
country: "Egypt",
type: .cat)
let bulldog = Animal(name: "Bulldog",
country: "England",
type: .dog)
print("Client: I created a number of objects to display")
/// Displaying objects for the 1-st time.
print("Client: Let's show animals for the 1st time\n")
display(animals: [maineCoon, sphynx, bulldog])
/// Displaying objects for the 2-nd time.
///
/// Note: Cached object of the appearance will be reused this time.
print("\nClient: I have a new dog, let's show it the same way!\n")
let germanShepherd = Animal(name: "German Shepherd",
country: "Germany",
type: .dog)
display(animals: [germanShepherd])
}
}
extension FlyweightRealWorld {
func display(animals: [Animal]) {
let cells = loadCells(count: animals.count)
for index in 0..<animals.count {
cells[index].update(with: animals[index])
}
/// Using cells...
}
func loadCells(count: Int) -> [Cell] {
/// Emulates behavior of a table/collection view.
return Array(repeating: Cell(), count: count)
}
}
enum Type: String {
case cat
case dog
}
class Cell {
private var animal: Animal?
func update(with animal: Animal) {
self.animal = animal
let type = animal.type.rawValue
let photos = "photos \(animal.appearance.photos.count)"
print("Cell: Updating an appearance of a \(type)-cell: \(photos)\n")
}
}
struct Animal: Equatable {
/// This is an external context that contains specific values and an object
/// with a common state.
///
/// Note: The object of appearance will be lazily created when it is needed
let name: String
let country: String
let type: Type
var appearance: Appearance {
return AppearanceFactory.appearance(for: type)
}
}
struct Appearance: Equatable {
/// This object contains a predefined appearance of every cell
let photos: [UIImage]
let backgroundColor: UIColor
}
extension Animal: CustomStringConvertible {
var description: String {
return "\(name), \(country), \(type.rawValue) + \(appearance.description)"
}
}
extension Appearance: CustomStringConvertible {
var description: String {
return "photos: \(photos.count), \(backgroundColor)"
}
}
class AppearanceFactory {
private static var cache = [Type: Appearance]()
static func appearance(for key: Type) -> Appearance {
guard cache[key] == nil else {
print("AppearanceFactory: Reusing an existing \(key.rawValue)-appearance.")
return cache[key]!
}
print("AppearanceFactory: Can't find a cached \(key.rawValue)-object, creating a new one.")
switch key {
case .cat:
cache[key] = catInfo
case .dog:
cache[key] = dogInfo
}
return cache[key]!
}
}
extension AppearanceFactory {
private static var catInfo: Appearance {
return Appearance(photos: [UIImage()], backgroundColor: .red)
}
private static var dogInfo: Appearance {
return Appearance(photos: [UIImage(), UIImage()], backgroundColor: .blue)
}
}
Output.txt: Execution result
Client: I created a number of objects to display
Client: Let's show animals for the 1st time
AppearanceFactory: Can't find a cached cat-object, creating a new one.
Cell: Updating an appearance of a cat-cell: photos 1
AppearanceFactory: Reusing an existing cat-appearance.
Cell: Updating an appearance of a cat-cell: photos 1
AppearanceFactory: Can't find a cached dog-object, creating a new one.
Cell: Updating an appearance of a dog-cell: photos 2
Client: I have a new dog, let's show it the same way!
AppearanceFactory: Reusing an existing dog-appearance.
Cell: Updating an appearance of a dog-cell: photos 2