Home Articles

Singleton Design Pattern

Let us discuss how Singleton Design Pattern in this article:

Singleton Design Pattern

Singleton pattern can be instantiated only once. It’s widely used pattern in Apple platform, there are certain examples: UIDevice, UserDefaults, UIApplication, FileManager, Bundle etc. A singleton type must be instantiated only once which means it should be hiding all initializers. Singletons looks similar to global variables but they have few differences:

  • They are created only on first access
  • They make it easy to change app architecture, you just need to replace shared to instance.

There are some differences as well:

  • Harder to test
  • Introduces hidden dependency
  • Issues in achieving concurrency. If multiple threads access the shared resource then it leads to readers writers problem
  • Performance bottleneck if multiple threads are using it in parallel

Intent

The singleton pattern ensures that only one object of a given type exists in the application.

What are the benefits?

It can be used to manage objects that encapsulate a shared resource.

The problem addressed by the singleton pattern arises when you have an object that you don’t want duplicated throughout an application.

When should you use this pattern?

The singleton pattern should be used when creating further objects doesn’t increase the number of resources available or when you want to consolidate an activity such as logging.

When should you avoid this pattern?

If there are not multiple components that require access to a shared resource

Pattern correct implementation check

When there is only one instance of a given type and when that instance cannot be copied and cloned and when further instances cannot be created. Singleton can’t be created of value types. The singleton pattern works only with reference types, which means that only classes are supported. Structs and other values types don’t work because they are copied when they are assigned to a new variable. The only way to copy a reference type is to create a new instance via its initializer or to rely on it implementing the NSCopying protocol, that’s why initializer of singleton is kept private.

Any related patterns

Object pool pattern uses this, can be used with other patterns as well.

Structure

Design Pattern Structure

User is Codable struct which conforms to Identifiable protocol as well for some requirement. User type already has identifier property but to satisfy protocol requirement it had to create identifier property using type extension, it’s called extension adapter. Extensions are a convenient way to add new methods and computed properties to any type in Swift without modifying its implementation.

Implementation

Best example of Singleton in iOS is AppDelegate. It’s a root object that manages interactions with OS. Singletons might be access from multiple threads at the same time, so we must protect at concurrent access.


    final class FeaturedTopics {
        private var topics: [String: Any] = [:]
        
        static let shared = FeaturedTopics()
        
        private init() {}
        
        subscript(key: String) -> Any? {
            get {
                return topics[key]
            }
            set {
                topics[key] = newValue
            }
        }
        
        func reset() {
            topics.removeAll()
        }
    }
    

swift static properties are thread safe thus accessing static property from multiple thread concurrently won’t end up in creating multiple instances.

Now issue with this class is if we read and write from multiple threads then it can end up in race condition. Neither dictionary nor Array is thread safe.

Let’s try to use FeaturedTopics


  func testFeaturedTopics() {
        let count = 200
        for index in 0..<count {
            FeaturedTopics.shared[String(index)] = index
        }
        
        // The dispatch queue executes the submitted blocks in parallel and waits for all iterations to complete before returning
        DispatchQueue.concurrentPerform(iterations: count) { (index) in
            if let indexNumber = FeaturedTopics.shared[String(index)] as? Int {
                print(indexNumber)
            }
        }
        
        FeaturedTopics.shared.reset()
        
        // Re-add the values concurrently
        DispatchQueue.concurrentPerform(iterations: count) { (index) in
            print("index \(index)")
            FeaturedTopics.shared[String(index)] = index
        }
    }
  

While setting values concurrently it ends up in race condition as dispatch queue executes the submitted blocks in parallel. Race condition occurs when multiple threads tries to modify singleton internal state.

To make it thread safe, we can create serial queue, so at a time only one thread can execute.


    final class FeaturedTopics {
        private let serialQueue = DispatchQueue(label: "serialQueue")
        private var topics: [String: Any] = [:]
        static let shared = FeaturedTopics( )
        private init() {}
        subscript(key: String) -> Any? {
            get { 
                var topic: Any?
                serialQueue.sync {
                    topic = topics[key]
                }
                return topic
            }
            set {
                serialQueue.sync {
                    topics[key] = newValue
                }
            }
        }
        func reset() {
            topics.removeAll()
        }
    }
    

Although serial queue resolves race condition but it slows down execution. Now we need to improve performance, it could be achieved using readers-writers problem. Readers writers problem suggests to use dispatch queue barrier while writing and sync while reading.


    final class FeaturedTopics {
        private let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
        private var topics: [String: Any] = [:]
        
        static let shared = FeaturedTopics()
        
        private init() {}
        
        subscript(key: String) -> Any? {
            get {
                var topic: Any?
                // conocurrent queue allows executing read operations in parallel
                concurrentQueue.sync {
                    topic = self.topics[key]
                }
                return topic
            }
            set {
                // barrier turns concurrent queue into serial queue temporarily
                // As a result no other thread can modify internal dictionary while this block is executing
                concurrentQueue.async(flags: .barrier) {
                    self.topics[key] = newValue
                }
            }
        }
        
        func reset() {
            topics.removeAll()
        }
    }

In reader-writer pattern, barrier is not used with “read” operations because reads are allowed to happen concurrently with respect to other “reads”, without impacting thread-safety. You could use barrier with “reads”, but it would unnecessarily reduce performance if multiple “read” requests happened to be called at the same time. When any set operation is in queue, it would first finish existing executing task, then will perform set operation, during set operation no other thread is allowed to read and write.

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.