This pattern can be used when the system needs to deal with a large number of similar objects. Instead of creating each element one by one, this pattern permits you to reuse an object that shares the same data.
The flyweight pattern is used to reduce the memory and resource usage of complex models that contain many hundreds and thousands of similar objects by reducing the number of objects created. It tries to reuse similar existing objects or creates a new one when no match is found.
This pattern can be used when:
There are three participants to the flyweight pattern, as follows:
Flyweight
: This declares an interface that contains an intrinsic state and implements methods. These methods can receive and act on the extrinsic state of the flyweights.FlyweightFactory
: This factory creates and manages a flyweight's objects. It assures that the flyweight is shared, thanks to the method that it returns a reference to the flyweight.Client
: This contains references to the used flyweight. It also contains the extrinsic state of these flyweights.Clients do not create the flyweight by themselves, but they use the FlyweightFactory
method that guarantees the sharing of flyweights.
When a client invokes a method of a flyweight, it needs to send its extrinsic state.
Suppose that we want to display 200000 rectangles on a 1024 x 768 screen. These rectangles are generated randomly; they can have a random color from a list of 10 different colors.
We need to reduce the time taken to execute the function and use as less memory as possible.
In this example, we will use an XCTest
project with the XCTest
framework and instrument tool to illustrate how this pattern will help us reduce the memory consumption.
First, open the project called Flyweight Pattern_Demo1
that you will find in the source code folder of this chapter.
Go to the Xcode project named FlyweightPattern_Demo1Tests
and click on the FlyweightPattern_Demo1Tests.swift
file, as shown in the following screenshot:
In this file, you'll see the different test methods that are already implemented. Before starting with the implementation of our flyweight pattern, let's see what we currently have.
We already have an abstract class called AbstractPerfTest
that contains some already defined properties, fields, and methods:
class AbstractPerfTest { let colors:[SKColor] = [ SKColor.yellowColor(), SKColor.blackColor(), SKColor.cyanColor(), SKColor.whiteColor(), SKColor.blueColor(), SKColor.brownColor(), SKColor.redColor(), SKColor.greenColor(), SKColor.grayColor(), SKColor.purpleColor() ] let sks = SKScene() let view = SKView(frame: NSRect(x: 0, y: 0, width: 1024, height: 768)) let maxRectWidth = 100 let maxRectHeight = 100 //must be overriden func run(){ preconditionFailure("Must be overriden") } // - MARK generate Rect Height and Width func generateRectWidth() -> Int{ return Int(arc4random_uniform(UInt32(maxRectWidth))) } func generateRectHeight() -> Int{ return Int(arc4random_uniform(UInt32(maxRectHeight))) } // - MARK generate Position X and Y func generateXPos() -> Int{ return Int(arc4random_uniform(UInt32(view.bounds.size.width))) } func generateYPos() -> Int{ return Int(arc4random_uniform(UInt32(view.bounds.size.height))) } }
There is another class called NoPattern
that inherits from this abstract class and overrides the run method:
import Foundation // Inherits from our AbstractPerfTest class // which contains default methods and init class NoPattern:AbstractPerfTest { // Execute the test override func run(){ var j:Int = 0 for _ in 1...NUMBER_TO_GENERATE { let idx = Int(arc4random_uniform(UInt32(self.colors.count- 1))) let rect = SimpleRect(color: self.colors[idx]) rect.display(generateXPos(), yPos: generateYPos(), width: generateRectWidth(), height: generateRectHeight()) j++ } print("(j) rects generated") } }
The SimpleRect
class is defined in the SimpleRect.swift
file of the NoPattern
group folder. It is an object defined by a color, x and y position, width, and height.
I will not comment too much on the NoPattern
class, but what we see here is that the run
method of the NoPattern
class generates NUMBER_TO_GENERATE
(set to 100000
by default in the FlyweightPattern_Demo1Tests.swift
file) rectangles with a random color taken from the list of the colors array (defined in the abstract class). It then generates a position and dimension for each of these rectangles.
Now, let's check the performance of the run
method.
Come back to the FlyweightPattern_Demo1Tests.swift
file and check the method named testSimpleScreenFilling_noFlyWeight()
. Here, the method will execute the code implemented in the NoPattern
class that, like the name of the method tells us, does not implement the flyweight pattern. The execution time of this method will be used as a baseline to compare the same method but with the implementation of the flyweight pattern.
So, let's execute the test by clicking on the small icon on the left-hand side of the func testSimpleScreenFilling_noFlyWeight()
function, as shown in the following screenshot:
We need to ensure that the console is visible in Xcode. While executing, you'll see that the console log with 200000 rects generated has been repeated 10 times. This proves that our code has generated 2,00,000 rectangles 10 times. By default, the self.measureBlock
closure is executed 10 times, and it calculates the standard deviation of these 10 executions to obtain an average execution time:
On my MacBook Pro 15 Retina Late 2013, the average time is 0.804 seconds:
Now, the best part is to refactor our code to reduce the time taken to generate these 2,00,000 rectangles. As you have already seen in the generic structure of the pattern, we will need a few classes to manage our flyweights.
Let's start with our flyweightRect
class. Note that the flyweightRect
and SimpleRect
classes are used in the NoPattern
class to generate rectangles that are identical.
Therefore, in the following code, you'll find our FlyweightRect
class with the definition of our rectangle. So, we have a color, x and y position, height, and width of the rectangle.
Note that because I really want to see the gain in performance, I added two fields: image
and sprite
. Because the value of these fields have a cost in terms of performance on the instantiation of the class, I added them to show you clearly that the flyweight pattern permits you to reduce the calculation costs (and memory usage) when applied.
We will add a constructor to the intrinsic state as an argument: this will be the color. We will add another display()
method that will receive extrinsic states as arguments:
import SpriteKit import Foundation class FlyweightRect { var color: SKColor! var xPos: Int? var yPos: Int? var width: Int? var height: Int? var image: NSImage? var sprite: SKSpriteNode? //the constructor contains our intrinsic state init(color: SKColor) { self.color = color self.image = NSImage() self.sprite = SKSpriteNode() } func display(xPos: Int, yPos: Int, width: Int, height: Int){ self.xPos = xPos self.yPos = yPos self.width = width self.height = height } func description() -> String { return "rect position: (self.xPos), (self.yPos) : dimension: (self.width), (self.height) : color: (self.color)" } }
Once our flyweight is defined, we can now prepare our FlyweightFactory
object. Remember that this factory will first check if we already have a rectangle that is similar to the new one that we want to position on the screen; if it is not similar, then it will create a new one:
import SpriteKit import Foundation class FlyweightRectFactory{ internal static var rectsMap = Dictionary<SKColor, FlyweightRect>() static func getFlyweightRect(color:SKColor) -> FlyweightRect{ if let result = rectsMap[color]{ return result } else { // if nil add it to our dictionnary let result = FlyweightRect(color: color) rectsMap[color] = result return result } } }
We declare a static rectsMap
variable of the type Dictionary
that will contain our shared objects and manage their existence. The dictionary Key
will contain a Color
object.
Then, we define a static method called getFlyweightRect
that will return a FlyweightRect
class.
As rectMaps[color]
returns nil
, we unwrap the optional with a if let
statement. If it is not nil
, we return the result; otherwise, we create a new flyweight with the appropriate color, add it to our dictionary, and return the result.
In the FlyweightPattern_Demo1.swift
file, you'll find several test methods that test the response time of the factory depending on the type of the object that manages our flyweights. In the project, I tested the performance of the object that manages our flyweights using the Dictionary<SKColor, FlyweightRect>
, NSMutableDictionary
, and NSCache
types.
The complete code of the FlyweightRectFactory.swift
file is as follows:
import SpriteKit import Foundation class FlyweightRectFactory { internal static var rectsMap = Dictionary<SKColor, FlyweightRect>() internal static var rectsMapNS = NSMutableDictionary() internal static var rectsMapNSc = NSCache() static func getFlyweightRect(color:SKColor) -> FlyweightRect{ if let result = rectsMap[color]{ return result }else { let result = FlyweightRect(color: color) rectsMap[color] = result return result } } static func getFlyweightRectWithNS(color: SKColor) -> FlyweightRect{ let result = rectsMapNS[color.description] if result == nil { let flyweight= FlyweightRect(color: color) rectsMapNS.setObject(flyweight, forKey: color.description) return flyweightas FlyweightRect }else { return result as! FlyweightRect } } static func getFlyweightRectWithNSc(color: SKColor) -> FlyweightRect{ let result = rectsMapNSc.objectForKey(color.description) if result == nil { let flyweight= FlyweightRect(color: color) rectsMapNSc.setObject(flyweight, forKey:color.description) return flyweight as FlyweightRect }else { return result as! FlyweightRect } } }
Using our pattern is extremely easy. You need to check the run()
method of the WithPattern.swift
file:
class WithPattern:AbstractPerfTest{ //Execute the test override func run(){ var j:Int = 0 for _ in 1...NUMBER_TO_GENERATE{ let idx = Int(arc4random_uniform(UInt32(self.colors.count- 1))) let rect = FlyweightRectFactory.getFlyweightRect(self.colors[idx]) rect.display(generateXPos(), yPos: generateYPos(), width: generateRectWidth(), height: generateRectHeight()) j++ } print("(j) rects generated") //print("nb Map: (FlyweightRectFactory.rectsMap.count)") }
We will simply make a loop to create 200000 FlyweightRect
objects (NUMBER_TO_GENERATE
is a constant defined at the top of the FlyweightPattern_Demo1Tests.swift
file).
The WithPattern
class written in the preceding does the following:
AbstractPerfTest.swift
file).To check the performance, there is an XCTest
class available in the project with the self.measureblock
closure that allows us to measure the performance of our block.
To launch all the tests available in the project, click on the Show the Test navigator button on the left-hand side, as shown in the following screenshot:
Then, click on the play button that is visible on the right-hand side of the highlighted line:
After a few seconds, all the tests would have been run and you can now check your performance results.
Come back to the FlyweightPattern_Demo1Tests.swift
file and check the end of each measureblock()
method. This is the result with the flyweight pattern using a dictionary. You can see that it took an average of 0.247 seconds to generate 200000 rectangles, as shown in the following screenshot. You'll see a text with Time xxxx
written; this is the average time taken to execute this block:
As compared to an average of 0.877 seconds without using the pattern:
After reviewing the results, you'll see that generating rectangles are better when (best performance first):
Dictionary
object is used to manage our shared objectsNSDictionary
object is used to manage our shared objectsNSCache
object is used to manage our shared objectsIn this example, we can say that generating 200000 FlyweightRect
objects is 3,55 times faster than without using the pattern.
The test project proves that Swift is faster than Objective-C and NSCache,
which are encapsulated.
The NSDictionary
object will have its own logic while handling the cache. It can create more objects inside its own hidden code structure, so it's slower than the NSDictionary
object.