PHP:管理实体类型的设计模式


PHP: Design pattern for managing types of entity

我有User实体。我希望这个实体有多个"类型",使用不同的管理器和存储库。所有类型的User实体只共享UserInterface。现在,我在寻找一个组织一切的好方法。我想到的第一件事就是创建这样的东西:

interface UserTypeManagerInterface
{
  public function addUserType($name, RepositoryInterface $repository, ManagerInterface $manager);
  public function hasType($name);
  public function getRepository($type);
  public function getManager($type);
}

然后在我想要同时管理多种类型的User的地方,我将注入这个,在我想要管理特定类型的用户的地方,我只能为其类型注入特定的存储库和管理器对象。

似乎是一个相当干净的方法,但同时,当我想使用UserTypeManager创建一个类的测试时,我需要模拟UserTypeManager,然后这个模拟需要返回其他模拟(存储库和管理器)。

这当然是可行的,但它让我思考是否可以避免。我能想到的唯一一种可以在测试期间避免上述复杂性的方法是:

interface UserTypeManagerInterface {
    public function addUserType($name, RepositoryInterface $repository, ManagerInterface $manager);
}
/**
 * My class managing multiple types of user.
 */
class ManageMultipleTypesOfUsers implements UserTypeManagerInterface {
    // ...
}

所以我会把所有的存储库和管理器添加到所有实现UserTypeManagerInterface接口的类中。因此,对象将直接使用给定给它们的内容。

这样测试会更简洁,因为我只需要模拟一个管理器和一个存储库来测试类ManageMultipleTypesOfUsers,但这感觉太像过度工程了。;)

还有中间地带吗?

我不能给出一个明确的答案,因为这是一个权衡。

就我所见,用户是一个纯值对象?这是一个好的开始。这意味着操作它很简单,没有副作用。

现在,考虑这些将影响您的设计的变量:

  • 接口只是一个方法吗?或者它的所有方法都是对一个方法的简单包装?)
  • 接口是否意味着让库的用户定义自定义实现?
  • 是否实现只是添加断言(例如接口需要一个方法delete(User $user) -只允许管理员,其他实现只是抛出,基本的权限检查)或使用截然不同的代码?
  • 我们有多少过度设计和不可测试的混乱?

那么,这些变量是如何影响我们的决策的?

  • 如果你曾经只有一个[较小的]中心方法,那么接口加类可能是多余的,并且倾向于用许多小文件来破坏你的代码库。通常传递Closure也能达到同样的效果。
  • 除了简单的闭包/可调用之外,公共API应该始终是接口。
  • 当实现只是添加断言时,您通常最好使用一个类向访问控制管理器发送请求[从数据库读取/缓存权限]。
  • 过度工程vs不可测试:50个文件,每3行不同的代码最好合并成一个300行文件。

因为我不知道需求是什么,所以我不能给你一个理想的解决方案。您提出的(第二种)解决方案适用于"过度设计"的情况。

更温和的解决方案是函数式方法:

function addAdminUser($name, RepositoryInterface $repository, ManagerInterface $manager) { /* ... */ }
function addNormalUser($name, RepositoryInterface $repository, ManagerInterface $manager) { /* ... */ }
// you even can pass this around as a callable ($cb = "addAdminUser"), or (pre-binding):
$cb = function($name) use ($repo, $mgr) { addAdminUser($name, $repo, $mgr); };

或者根本地(如果普通用户是admin用户的子集)[只要函数本身没有副作用并且它的调用者不会太难测试]:

function addUser($name, $type, RepositoryInterface $repository, ManagerInterface $manager) {
    /* ... common logic ... */
    if ($type == IS_ADMIN_USER) { /* ... */ }
}

如果这太激进,也可以注入回调到addUser:

function addUser($name, $cb, RepositoryInterface $repository, ManagerInterface $manager) {
    $user = new User;
    /* ... common logic ... */
    $cb($user, $repository, $manager);
}

是的,函数方法可能不是可测试的(也就是说,如果你直接调用它(不传递callable),你将无法模拟函数),但它可能会给你带来巨大的可读性。保存琐碎的间接级别可以使代码更容易分析。我强调的是may,因为去掉太多的间接性可能会达到相反的效果。找到适合您的应用程序的中间地带。

TL;DR: PHP为您提供了更实用的方法,但可测试性较差。有时这是一种很好的权衡,有时则不然。这取决于你的具体代码,中间地带在哪里。

注。:在你的第二个建议中,真正闻起来有点对我来说,唯一的事情是在addUserType方法签名中有RepositoryInterface $repository, ManagerInterface $manager。你确定它不是构造函数的一部分吗?