单身模式与单元测试和拆解

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (1)
  • 关注 (0)
  • 查看 (42)

我们有一个CMS框架。这一点很重要,因为框架管理有关请求的所有内容 - 从类加载到数据库连接管理再到错误处理。

为了做到这一点,使用了许多系统单例 - 系统状态单例或工厂。

大多数单例都是使用带静态变量的抽象类实现的ATM。在问题的最后,我们发布代码,Singleton1用于表示类的pattern()。请注意,讨论中的单例都没有被持久化,序列化或者需要一个实际的对象实例 - 我们的问题不是功能,它们工作得很好。

我们正在设置自动测试:单元,功能,验收。我们正在使用Codeception(它使用PHPUnit),但请抽象测试平台,让我们使用它作为示例。我们询问一般的通用自动化测试。

我们面临的问题是支持体面的拆解来测试一些系统功能。例如,很明显我们类加载是不可逆的,因为PHP不支持卸载类,所以只有进程隔离才能为类加载提供合适的拆卸(这是真的吗?)。然而,除此之外,其他单身人士可以/应该进行测试。

基于抽象类的单例模式似乎更难以拆卸,但是PHPUnit在测试之间“回滚”类静态属性是一种不错的努力,所以它有点工作。静态属性在测试之间保存和恢复。

单实例单例(Singleton2在下面的代码示例中)模式看起来更合适,因为它允许显式拆卸(只是取消设置单实例引用)。从理论上讲,它还允许多个“上下文”单例 - 因此我们可以预先初始化一些单体,以便在各种测试环境中重复使用。

我们发现在两种实现中都会存在一致性危害并且我们认为无法进行包装(除了进程隔离):例如,以单件工厂为例。虽然它的状态可以“显然”被保存/恢复,但这非常浅:工厂操作的对象实例可以对全局状态进行操作,这些操作一般难以恢复(文件锁作为一个例子可以想到)。

问题: 什么是用于使您的生活更容易进行自动化测试的单例和拆解模式?各种此类模式的共同优点和缺陷是什么?

鉴于描述(虽然在描述我们的模式方面可能有点简短)和您的经历,您是否注意到与我们错过或假设错误的主题有关的明显事项?

/*  Pattern 1: abstract singleton; teardown is done 
        by the testing framework by resetting static properties */
abstract class Singleton1 {
    protected static $singletonProperty1;    // Protected - allow singleton emancipation
    protected static $singletonProperty2;
    public static function init(){
        self::$singletonProperty1='Hello';
        self::$singletonProperty2='World';
    }
    public static function use(){
        echo implode(' ',[self::$singletonProperty1,self::$singletonProperty2]);
    }
}

Singleton1::init();
Singleton1::use();

/*  Pattern 2: single-instance singleton w/ explicit teardown */
class Singleton2 {
    private static $instance;

    protected $singletonProperty1;    // Protected - allow singleton emancipation
    protected $singletonProperty2;

    protected function __construct(){
        $this->singletonProperty1='Hello';
        $this->singletonProperty2='World';
    }
    public function use(){
        echo implode(' ',[$this->singletonProperty1,$this->singletonProperty2]);
    }

    public static function getInstance(){
        if(!self::$instance){
            $class=get_called_class();    // Backwards PHP compatibility of "static"
            self::$instance=new $class();
        }
        return self::$instance;
    }
    public static function tearDown(){
        self::$instance=null;    // Backwards PHP compatibility: do NOT use unset on class/object properties, inconsistent behavior
    }
}

Singleton2::getInstance()->use();

提问于
用户回答回答于

首先,你不应该基于继承来构建单例。这与单身人士的整个前提相反。

另外,因为它们依赖于静态属性,所以最终会混合你的孩子或单独执行单例代码(这就是你现在所做的)。哪个没有给你任何真正的保证,这个类将符合标准模式。

我发现构建它们的最好方法是使用特征和接口。

我有一个composer包(和Git repo)

https://github.com/ArtisticPhoenix/Pattern

{
    "require" : {
        "evo/patterns" : "~1.0"
    }
}

这是基本单例的代码:

SingletonTrait

<?php
/**
 *
 * (c) artisticphoenix
 *
 * For license information please view the LICENSE file included with this source code.
 *
 * Singletion pattern.  Classes using this trait should
 * <ul>
 *    <li>Implement \evo\pattern\singleton\SingletonInterface</li>
 *    <li>Be final</li>
 *    <li>Can overwrite init() method</li>
 * </ul>
 *
 * @author HughDurham {ArtisticPhoenix}
 * @package Evo
 * @subpackage pattern
 *
 */
trait SingletonTrait
{

    /**
     *
     * @var self
     */
    private static $instance;

    /**
     * no access
     */
    final private function __construct()
    {
    }

    /**
     * no access
     */
    final private function __clone()
    {
    }

    /**
     * no access
     */
    final private function __wakeup()
    {
    }

    /**
     *
     * Arguments passed to getInstance are passed to init(),
     * this only happens on instantiation
     *
     * @return self
     */
    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new self;
            self::$instance->init();
        }
        return self::$instance;
    }

    /**
     *
     * @return boolean
     */
    public static function isInstantiated()
    {
        return self::$instance ? true : false;
    }

    /**
     * called when the first instance is created (after construct)
     *
     * Overwrite this method with your startup code
     *
     */
    protected function init()
    {
    }
}

SingletonInterface

<?php
/**
 *
 * (c) artisticphoenix
 *
 * For license information please view the LICENSE file included with this source code.
 *
 * Singleton pattern
 *
 * @author HughDurham {ArtisticPhoenix}
 * @package Evo
 * @subpackage pattern
 */
interface SingletonInterface
{

    /**
     * @return self
     */
    public static function getInstance();

    /**
     * has an instance of the singleton been created
     *
     * @return bool
     */
    public static function isInstantiated();
}

Concrete(最小例子)

class foo{
     public function hello(){ echo "World\n"; }
}

final class bar extends foo implements SingletonInterface{
  use SingletonTrait;
   protected function init(){
       echo __METHOD__."\n";
   }

}

bar::getInstance()->hello();
bar::getInstance()->hello();
bar::getInstance()->hello();

输出

bar::init
World
World
World

砂箱

不确定这将如何影响您的测试方法,但您可以只使顶层成为一个实际的单例。然后你可以正常测试较低级别的类。

界面给你的合同说明这个类将提供这些方法等。基本上你可以通过使用接口键入提示任何单例。然后,该特征只是填写了界面和一些基本助手的要求。

PS。还有一个MultitonTrait,它允许你拥有多个实例。这可能看起来有点反对。但想想它是多个单身人士的容器(如Singleton工厂)。它对于像数据库连接这样的东西非常有用,你可以在每个数据库中拥有一个DB单例。基本上self::$instance变成一个在调用时传递别名的数组getInstance。因此,对于数据库,您可以根据数据库名称对它们进行别名,然后在连接时使用它来为该数据库(用户,传递等等)init()获取配置,例如它就像这样:Db::getInstance('myDB')

希望它能帮到你。我知道这不是一个“答案”的答案,但要发表评论还有很多。非常适合只需要一个例子的事情。自动加载器,调试器,关闭处理程序,请求包装器,数据库包装器等。但它们确实有一些必须牢记的缺点。

扫码关注云+社区

领取腾讯云代金券