Flyweight es un patrón de diseño estructural que permite a los programas soportar grandes cantidades de objetos manteniendo un bajo uso de memoria.
El patrón lo logra compartiendo partes del estado del objeto entre varios objetos. En otras palabras, el Flyweight ahorra memoria RAM guardando en caché la misma información utilizada por distintos objetos.
Ejemplo conceptual
Este ejemplo ilustra la estructura del patrón de diseño Flyweight y se centra en las siguientes preguntas:
¿De qué clases se compone?
¿Qué papeles juegan esas clases?
¿De qué forma se relacionan los elementos del patrón?
Después de conocer la estructura del patrón, será más fácil comprender el siguiente ejemplo basado en un caso de uso real de Swift.
Example.swift: Ejemplo conceptual
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: Resultado de la ejecución
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
Ejemplo del mundo real
Example.swift: Ejemplo del mundo real
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: Resultado de la ejecución
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