GORAGOD.com

ปัญหาในการใช้งาน Singleton Pattern

ปัญหาในการใช้งาน Singleton ที่พบได้บ่อยคือ

ปัญหาการทำ unitTest เนื่องจาก ในการสร้าง Test แต่ละหน่วยทดสอบควรมี Instance แยกกัน แต่เนื่องจากการที่ Singleton จะมีเพียง Instance เดียวเท่านั้นในระหว่างการทดสอบ อาจเป็นเหตุให้ได้ผลการทดสอบที่ไม่ถูกต้องได้
class Cfg
{
    /**
     * property ของคลาส
     *
     * @var int
     */

    public $property = 1;
   
    .........................

    /**
     * @param int $dec
     * @return int
     */

    public function inc($dec)
    {
        $this->property = $this->property + $dec;
        return $this->property;
    }
}

ส่วนหนึ่งของโค้ดตัวอย่างด้านบน ที่เป็น Singleton ฟังก์ชั่น inc()
/**
 * Generated by PHPUnit_SkeletonGenerator on 2015-12-04 at 18:54:14.
 */

class CfgTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var Cfg
     */

    protected $object;

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */

    protected function setUp()
    {
        $this->object = \Cfg::getInstance();
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */

    protected function tearDown()
    {

    }

    /**
     * @covers Cfg::inc
     */

    public function testInc()
    {
        $this->assertEquals(2, $this->object->inc(1));
    }

    /**
     * @covers Cfg::inc
     */

    public function testInc2()
    {
        $this->assertEquals(2, $this->object->inc(1));
    }
}

เมื่อเขียน test (ดูจากฟังก์ชั่นที่เขียนนะครับ) testInc() และ testInc2() มีการเขียนการทดสอบเหมือนๆกันดังนั้นผลลัพท์มันควรจะเหมือนกัน แต่เมื่อทำการทดสอบ ผลลัพท์ของ testInc2() จะไม่ผ่าน เนื่องจากมันจะเอาผลการทดสอบจาก testInc() มารวมด้วย

การแก้ไข อาจต้องเพิ่มฟังก์ชั่น reset() ลงใน Singleton อีกสักตัวใช้ทำหน้าที่ในการทำให้ Singleton กลับเป็นค่าเริ่มต้นและเรียกใช้ฟังก์ชั่นก่อนการทดสอบ
class Cfg
{

    .........................


    public function reset()
    {
        $this->property = 1;
    }
}

และเมื่อเขียน test
class CfgTest extends PHPUnit_Framework_TestCase
{
    ......

    public function testInc()
    {
       $this->object->reset();
        $this->assertEquals(2, $this->object->inc(1));
    }

    public function testInc2()
    {
       $this->object->reset();
        $this->assertEquals(2, $this->object->inc(1));
    }
}

ปัญหาที่สองคือเรื่องของ Performance จริงๆมันก็จะแตกต่างจากการเขียนคลาสปกติไม่เยอะหรอกครับแต่ในบางกรณีที่ผมเจอ คือมีการเขียนแบบนี้
Cfg::getInstance()->inc(1);
Cfg::getInstance()->inc(1);
Cfg::getInstance()->inc(1);
Cfg::getInstance()->inc(1);
Cfg::getInstance()->inc(1);

จะเห็นว่ามีการเรียกเมธอดภายในคลาส Cfg หลายครั้ง และทุกครั้งเรียกด้วย getInstance() วิธีนี้ทำให้ Performance ตกลงอย่างมากครับ เนื่องจากการเรียก getInstance() ในแต่ละครั้งจะต้องมีการปฏิบัติเพิ่มเติมตามหน้าที่ของฟังก์ชั่น ซึ่งการแก้ไขอาจเรียกแบบนี้ก็ได้
$cfg = Cfg::getInstance();
$cfg->inc(1);
​$cfg->inc(1);
​$cfg->inc(1);
​$cfg->inc(1);
​$cfg->inc(1);

ปัญหาอีกอันของ Singleton เกี่ยวกับ Performance คือ เนื่องจาก Singleton จะต้องเป็น Global ดังนั้นมันจะถูกโหลดเก็บไว้ในหน่วยความจำจนกว่าจะจบโปรแกรม ดังนั้นหากคลาสนี้เป็นคลาสขนาดใหญ่ เราจะสูญเสียหน่วยความจำส่วนหนึ่งไปให้กับคลาส