Table of contents
    blog cover

    One Design Pattern a Week: Week 1

    Software Engineer
    Software Engineer
    Design Pattern
    Design Pattern
    Welcome back to my "One Design Pattern a Week" series!

    Try to solve this real problem: Configuration Management

    Imagine you're developing a large web application. This application requires various configuration settings such as database credentials, API keys, and feature toggles. These settings should be loaded once (because it takes time) and be accessible throughout the application. 

    Here are some potential issues:
    • Multiple Instances: If the configuration settings are loaded in multiple places, it can lead to inconsistencies and increased memory usage.
    • Global Access: You need a way to access these settings globally without passing them around as parameters.

    Take a moment to think about how you would solve this problem. How can you ensure that there's only one instance of the configuration manager accessible throughout your application?

    Singleton Pattern

    The Singleton pattern is a creational design pattern that addresses the issues mentioned above. It ensures that a class has only one instance and provides a global point of access to it. This is particularly useful when exactly one object is needed to coordinate actions across the system.

    Key Characteristics:
    • Single Instance: Only one instance of the class is created.
    • Global Access Point: Provides a way to access the instance globally.
    • Lazy Initialization: The instance is created only when it is needed.

    Implementing Singleton in Ruby

    // language: ruby
    class ConfigurationManager  
      @instance = nil  
      private_class_method :new # Prevent user from using ConfigurationManager.new  
    
      def self.instance    
        @instance ||= new # Only call new when instance is nil  
      end  
      
      def initialize    
        @settings = {}  
      end  
      
      def load_settings(file)    
        # Simulate loading settings from a file    
        @settings = { db: 'localhost', api_key: '12345' }    
        puts "Settings loaded from #{file}"  
      end  
    
      def get_setting(key)    
        @settings[key]  
      end
    end

    Or you can use Ruby's built-in module called 'singleton', which makes it easy to implement the Singleton pattern.
    // language: ruby
    require 'singleton'
    
    class ConfigurationManager  
      include Singleton  
    
      def initialize    
        @settings = {}  
      end  
    
      def load_settings(file)    
        @settings = { db: 'localhost', api_key: '12345' }    
        puts "Settings loaded from #{file}"  
      end  
    
      def get_setting(key)    
        @settings[key]  
      end
    end

    Usage
    // language: ruby
    config_manager = ConfigurationManager.instance
    config_manager.load_settings('config.yml')
    
    another_config_manager = ConfigurationManager.instance
    puts another_config_manager.get_setting(:db)  # Output: localhost
    
    # Both instances are the sameputs 
    config_manager.equal?(another_config_manager)  # Output: true

    More Practical Examples

    1. Logger System
    A logging system often needs a single point of access to write log entries consistently across the application.

    // language: ruby
    require 'singleton'
    class Logger
      include Singleton
      def initialize
        @log_file = File.open('application.log', 'a')
      end
    
      def log(message)
        @log_file.puts(message)
        @log_file.flush
      end
    end
    
    # Usage
    logger = Logger.instance
    logger.log('This is a log message.')

    2. Cache Manager
    A cache manager can maintain a single instance to store frequently accessed data, reducing redundant data fetching.

    // language: ruby
    require 'singleton'
    class CacheManager
      include Singleton
      def initialize
        @cache = {}
      end
    
      def store(key, value)
        @cache[key] = value
      end
    
      def fetch(key)
        @cache[key]
      end
    end
    
    # Usage
    cache = CacheManager.instancecache.store('user_1', { name: 'John Doe', age: 30 })
    another_cache = CacheManager.instance
    puts another_cache.fetch('user_1')  # Output: {:name=>"John Doe", :age=>30}

    3. Application Settings
    A single point to manage and retrieve application-wide settings.
    // language: ruby
    require 'singleton'
    class AppSettings
      include Singleton
    
      def initialize
        @settings = { theme: 'dark', language: 'en' }
      end
    
      def get_setting(key)
        @settings[key]
      end
    
      def set_setting(key, value)
        @settings[key] = value
      end
    end
    
    # Usage
    settings = AppSettings.instance
    puts settings.get_setting(:theme)  # Output: dark
    
    settings.set_setting(:theme, 'light')
    puts settings.get_setting(:theme)  # Output: light

    Multi-Thread/Process Applications

    In a multi-threaded or multi-process environment, ensuring that only one instance of a Singleton is created can be challenging. Here's how you can handle it in Ruby:

    Multi-Threaded Applications
    To make the singleton thread safe, you can use Ruby's built-in Mutex class to synchronize access to the instance creation.
    // language: ruby
    require 'singleton'
    require 'thread'
    
    class ThreadSafeSingleton
      @instance = nil
      @mutex = Mutex.new
    
      private_class_method :new
    
      def self.instance
        return @instance if @instance
    
        @mutex.synchronize do
          @instance ||= new
        end
    
        @instance
      end
    
      def initialize
        # initialization code
      end
    end
    
    # Usage in a multi-threaded environment
    threads = 10.times.map do
      Thread.new do
        singleton = ThreadSafeSingleton.instance
        puts singleton.object_id
      end
    end
    threads.each(&:join)

    In this implementation, I use a Mutex to ensure that only one thread can execute the instance creation code at a time. This prevents race conditions where multiple threads might attempt to create an instance simultaneously.

    Multi-Process Applications
    Handling Singleton in a multi-process environment can be tricky because each process has its own memory space, meaning that the Singleton instance in one process won't be the same as in another. 
    However, there are strategies to manage a Singleton-like behavior across processes. However, I don't include these strategies in this post because they come with their own set of complexities and potential pitfalls

    Conclusion

    The Singleton pattern is a powerful tool for managing shared resources, ensuring only one instance of a class exists, and providing a global access point to that instance.

    Happy coding!
    Created at 2024-09-21 15:13:53 +0700

    Related blogs