The flyweight pattern

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.

Roles

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:

  • We need to manipulate a lot of small similar objects
  • The cost (the memory/execution time) of this manipulation is high

Design

The following class diagram represents the generic structure of the pattern:

Design

Participants

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.

Note

The extrinsic state: This is the state that belongs to the context of the object (external) or unique to that instance.

The intrinsic state: This is the state that belongs to the flyweight object and should be permanent or immutable (internal).

Collaboration

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.

Illustration

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.

Implementation

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:

Implementation

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:

Implementation

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:

Implementation

On my MacBook Pro 15 Retina Late 2013, the average time is 0.804 seconds:

Implementation

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.

Note

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
    }
  }
}

Usage

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:

  1. We first generate a random number that returns a value that will correspond to the index of a color available in the colors array (defined in the AbstractPerfTest.swift file).
  2. Then, we tell the factory to return a flyweight with the appropriate color.
  3. Then, we generate the extrinsic state (x position, y position, width, and height).
  4. Once the loop is complete, we display the number of generated rectangles.

Performance results

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:

Performance results

Then, click on the play button that is visible on the right-hand side of the highlighted line:

Performance results

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:

Performance results

As compared to an average of 0.877 seconds without using the pattern:

Performance results

After reviewing the results, you'll see that generating rectangles are better when (best performance first):

  • The flyweight pattern with a Dictionary object is used to manage our shared objects
  • The flyweight pattern with a NSDictionary object is used to manage our shared objects
  • The flyweight pattern with a NSCache object is used to manage our shared objects
  • No pattern is applied

In 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset