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

Łuk*_*oda 6 php design-patterns

我有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);
}
Run Code Online (Sandbox Code Playgroud)

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

看起来像一个非常干净的方法,但同时,当我想使用UserTypeManager我需要模拟的类创建测试时UserTypeManager,那么这个模拟将需要返回其他模拟(存储库和管理器).

这当然是可行的,但它让我思考是否可以避免这种情况.我能想到的唯一另一件事就是允许在测试过程中避免上述复杂性,这样就像这样:

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

/**
 * My class managing multiple types of user.
 */
class ManageMultipleTypesOfUsers implements UserTypeManagerInterface {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

所以我只想将所有存储库和管理器添加到实现UserTypeManagerInterface接口的所有类中.所以对象会直接使用给他们的东西.

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

这里有任何中间地带吗?

bwo*_*ebi 1

我无法对此给出明确的答案,因为这是一种权衡。

据我所知 User 是一个纯值对象?这是一个好的开始。这意味着操作起来很简单,没有副作用。

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

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

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

  • 如果您只有一个[较小的]中心方法,那么接口加类可能会过度杀伤力,并且往往会在您的代码库中散布许多小文件。通常传递一个闭包也能达到同样的效果。
  • 除了简单的闭包/可调用就足够的时候,公共 API 应该始终是接口。
  • 当实现仅添加断言时,通常最好使用一个类向访问控制管理器 [从数据库读取/缓存权限] 分派请求。
  • 过度设计与不可测试: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); };
Run Code Online (Sandbox Code Playgroud)

或者从根本上来说(如果普通用户是管理员用户添加的子集)[只要函数本身没有副作用并且其调用者不会太难测试]:

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

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

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

是的,函数式方法可能不那么可测试(即,如果直接调用该函数(不作为可调用传递),您将无法模拟该函数),但它可能会给您带来可读性的巨大提升。保存琐碎间接的级别可能会使您的代码更易于分析。我强调的是可能,因为删除太多间接可能会达到相反的效果。找到适用于您的应用程序的中间立场。

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

Ps:在你的第二个提案中,唯一让我觉得有点RepositoryInterface $repository, ManagerInterface $manager味道的是 addUserType 方法签名。您确定它不是构造函数的一部分吗?