Programming - The Singleton Pattern

The singleton pattern is one the best known and easiest to implement design pattern, but it is also the most misused ones and it often leads to the introduction of a global state in the application. 

A singleton pattern makes it easy to retrieve an instance from any point of the application but at the same time it will create dependencies that can become difficult to maintain or adapt. 

The most common implementation of the Singleton

When designing a singleton in PHP, we are likely to use the following guidelines: 
- Change the visibility of the constructor class to prevent instantiation outside of the class. 
- Create a static method that provides the instance: 




class Singleton {
   private static $instance = null;

   private function __construct()
   {
   }

   private function __clone()
   {
   }

   public static function getInstance()
   {
      if (self::$instance === null) {
         self::$instance = new self();
      }
      return self::$instance;
   }
}

$singleton = Singleton::getInstance();

// Generates a fatal error in PHP 7
// "Call to private Singleton::__construct() from invalid context"
$singleton = new Singleton();


This type of implementation has a major flaw. The use a static method means that anyone can have access to the instance (e.g. unauthorized access to the data of a database through a simple SQL query). 


Solution

Instead of using a static method, you should opt for a factory pattern. The factory pattern can return an instance of the class and if called multiple times, it returns the instance already created. The problem with this type of solution is that the constructor class becomes public, which makes it possible to create other instances out of the singleton class. To avoid this type of issue in PHP 7, it is possible to create an anonymous class within the factory pattern: 


class Factory {
    private $instance = null;
     
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new class() {
                private $var;
                public function get() {
                    return $this->var;
                }
                public function set($value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();  
$o->set(5);
$o2 = $factory->getInstance();

// Affiche "int(5)"
var_dump($o2->get());


Type hinting is not possible when using an anonymous class of the object(class@anonymous) type, unless you implement an interface first: 


interface MyInterface {
    public function get(): int;
    public function set(int $value);
}

class Factory {
    private $instance = null;
    
    public function getInstance(): MyInterface
    {
        if ($this->instance === null) {
            $this->instance = new class() implements MyInterface {
                private $var;
                public function get(): int {
                    return $this->var;
                }
                public function set(int $value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

function display(MyInterface $object)
{
    var_dump($object->get());
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);
$o2 = $factory->getInstance();

display($o2);


It's now possible to instantiate several factories and therefore get several instances from our singleton. We need to prevent our class from being instantiated more than once and keep our instance safe within the factory pattern. The only problem is that anonymous classes differ from one instance to another and furthermore the use of the static method makes it difficult to identify new instances. Edit the code: 



class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new class() {
                private static $instantiated = false;
                
                public function __construct()
                {
                    if (self::$instantiated) {
                        throw new RuntimeException('Could not instantiate class');
                    }
                    self::$instantiated = true;
                }
                
                private $var;
                
                public function get(): int {
                    return $this->var;
                }
                
                public function set(int $value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);

$factory2 = new Factory();

$o2 = $factory->getInstance();
var_dump($o2->get());


Given that our class can't be instantiated more than once, it no longer necessary to keep the anonymous class. We can edit the code to: 


class Singleton {
    private static $instantiated = false;
    private $var;
    
    public function __construct()
    {
        if (self::$instantiated === true) {
            throw new RuntimeException('Could not instantiate class');
        }
        self::$instantiated = true;
    }

    private function __clone()
    {
    }
    
    public function get(): int {
        return $this->var;
    }
    
    public function set(int $value) {
        $this->var = $value;
    }
}

class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new Singleton();
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);

$factory2 = new Factory();
// Generates the message: "Could not instantiate class"
$o2 = $factory2->getInstance();


The method described above has a major limitation. Once the class has been instantiated (an object has been created), you won't be able to test it. This problem also exists with the singleton pattern, and it is not uncommon to add a resetInstance() static method to create new instances. 

So what's the point of using a singleton? The singleton pattern is often used for its simplicity and performance, especially when you only have a only have a single instance to manage.

No comments:

Post a Comment