"静态方法是可测试性的死亡" - 替代构造函数的替代方案?

dec*_*eze 11 php oop static-methods unit-testing design-patterns

据说"静态方法是可测试性的死亡".如果是这样,那么下面的可行替代模式是什么?

class User {

    private $phone,
            $status = 'default',
            $created,
            $modified;

    public function __construct($phone) {
        $this->phone    = $phone;
        $this->created  = new DateTime;
        $this->modified = new DateTime;
    }

    public static function getByPhone(PDO $pdo, $phone) {
        $stmt = $pdo->prepare('SELECT * FROM `users` WHERE `phone` = :phone');
        $stmt->execute(compact('phone'));
        if (!$stmt->rowCount()) {
            return false;
        }

        $record         = $stmt->fetch(PDO::FETCH_ASSOC);
        $user           = new self($record['phone']);
        $user->status   = $record['status'];
        $user->created  = new DateTime($record['created']);
        $user->modified = new DateTime($record['modified']);
        return $user;
    }

    public function save(PDO $pdo) {
        $stmt = $pdo->prepare(
            'INSERT INTO `users` (`phone`, `status`, `created`, `modified`)
                  VALUES         (:phone,  :status,  :created,  :modified)
             ON DUPLICATE KEY UPDATE `status`   = :status,
                                     `modified` = :modified');

        $data = array(
            'phone'    => $this->phone,
            'status'   => $this->status,
            'created'  => $this->created->format('Y-m-d H:i:s'),
            'modified' => date('Y-m-d H:i:s')
        );

        return $stmt->execute($data);
    }

    ...

}
Run Code Online (Sandbox Code Playgroud)

这只是一个减少的例子.该类有一些方法和属性,并且在写入数据库时​​有更多的验证等.这个类背后的指导设计原则是它将用户建模为对象.某些对象的属性在创建后无法修改,例如电话号码(充当主要ID),创建用户的日期等等.其他属性只能根据严格的业务规则进行更改,这些规则都严格验证了setter和getter.

该对象本身并不代表数据库记录,数据库仅被视为永久存储的一种可能形式.因此,数据库连接器不存储在对象中,而是需要在每次对象需要与数据库交互时注入.

创建新用户时,如下所示:

$user = new User('+123456789');
Run Code Online (Sandbox Code Playgroud)

从永久存储中还原现有用户时,如下所示:

$pdo  = new PDO('...');
$user = User::getByPhone($pdo, '+123456789');
Run Code Online (Sandbox Code Playgroud)

如果我认真对待"死亡可测性"线,这应该是坏事.我完全能够测试这个对象,因为它完全依赖注入并且static方法没有状态.我怎么能以不同的static方式做到这一点并避免使用方法?或者更确切地说,static在这种情况下究竟反对什么?是什么让这种static方法特别难以测试?

dec*_*eze 4

这主要是我和 @zerkms 之间的聊天(我的观点)的总结:

其实争论的焦点是这样的:

public function doSomething($id) {
    $user = User::getByPhone($this->pdo, $id);

    // do something with user

    return $someData;
}
Run Code Online (Sandbox Code Playgroud)

这使得测试变得困难,doSomething因为它对User类进行了硬编码,而该类可能有也可能没有很多依赖项。但这实际上与使用非静态方法实例化对象相同:

public function doSomething($id) {
    $user = new User;
    $user->initializeFromDb($this->pdo, $id);

    // do something with user

    return $someData;
}
Run Code Online (Sandbox Code Playgroud)

我们没有使用静态方法,但它仍然是不可模拟的。事实上,情况变得更糟了。
答案是使用工厂:

public function doSomething($id) {
    $user = $this->UserFactory->byPhone($id);

    // do something with user

    return $someData;
}
Run Code Online (Sandbox Code Playgroud)

现在工厂可以进行依赖注入和模拟,并且User类不再是硬编码的。您可能会或可能不会认为这太过分了,但它确实提高了可模拟性。

但这并没有改变这个工厂可以很好地使用静态方法实例化实际用户对象的事实:

public function byPhone($id) {
    return User::getByPhone($this->db, $id);
}
Run Code Online (Sandbox Code Playgroud)

此处使用静态方法或常规构造函数没有区别。

$user = new User($db, $id);
$user = User::getByPhone($db, $id);
Run Code Online (Sandbox Code Playgroud)

两个表达式都返回一个实例,User并且都“硬编码”该类User。无论如何,这只是需要在某个时刻发生。

对于我的用例,static构造函数方法对对象最有意义。正如所证明的,static方法不是问题。争论的焦点在于如何称呼它们,而不是它们是否存在。我还没有看到关于不使用静态构造函数的令人信服的论据,因为它们可以包装在工厂中,这可以减轻可模拟性的任何问题,就像常规对象实例化一样。