TDD社区中有一个提示,说
我们不应该对不拥有的类型使用模拟对象 。 我认为这是一个很好的建议,我会尝试遵循。 当然,有些人说我们根本不应该使用模拟对象。 无论您持有什么意见,“不要模仿不属于您的东西”的建议也包含隐藏的含义。 人们经常看到他的“嘲笑”一词,然后大怒。
这种隐藏的含义是,您应该在我们的应用程序和我们使用的第三方代码之间创建接口,客户端,网桥,适配器。 是否在测试中创建这些接口的模拟对象并不重要。 重要的是,我们创建和使用可以更好地将我们的代码与第三方代码分开的接口。 在PHP世界中,一个典型的例子是在使用
Guzzle HTTP客户端的应用程序中创建和使用HTTP客户
端 ,而不是直接使用Guzzle。
怎么了 好吧,对于初学者来说,Guzzle具有比您的应用程序所需的API更为强大的API(在大多数情况下)。 创建自己的HTTP客户端(仅提供必要的Guzzle API集),将使应用程序开发人员只能使用此客户端进行操作。 如果将来Guzzle API发生更改,我们将需要在一个地方进行更改,而不是在整个应用程序中更正其调用,以希望一切都不会中断。 有两个很好的理由,我什至没有提到模拟对象!
我认为这很难实现。 第三方代码通常位于我们应用程序的单独文件夹中,通常为
vendor/
或
library/
。 它也驻留在不同的名称空间中,并且与我们的应用程序中使用的命名约定不同。 第三方代码很容易识别,并且只需一点纪律,我们就可以减少应用程序代码对第三方部分的依赖。
如果我们对遗留代码应用相同的规则怎么办?
如果我们同时查看我们的旧代码和第三方怎么办? 当我们仅修复错误并对其进行少量调整时,如果仅在支持模式下使用过时的代码,则可能很难做到,甚至会适得其反。 但是,如果我们编写(重新)使用过时的新代码,我认为值得与第三方代码一样考虑。 至少从新代码的角度来看。
如果可能,过时的新代码应位于不同的文件夹和名称空间中。 自上次我看到该系统
没有启动以来已经有很长时间了,所以这是完全可行的。 但是,如果不是为新代码创建接口并使用它们,该怎么办而不是在新代码中盲目使用旧代码?
过时的代码通常到处都是“神圣的”对象,它们执行了太多的工作。 它们使用全局状态,具有公共属性或魔术方法,可以像访问公共属性一样访问私有属性,具有静态方法,可以
很方便地在任何地方和任何地方调用。 因此,这种非常便利的条件使我们进入了目前的状况。
过时的代码的另一个甚至可能更严重的问题是,我们准备更改,修复,破解它,因为我们不将其视为第三方代码。 当我们看到错误或想要向第三方代码添加新功能时该怎么办? 我们描述问题和/或创建请求请求。 我们
不做的是不要转到
vendor/
文件夹,也不要在此处编辑代码。 我们为什么要用遗留代码来做到这一点? 然后我们用手指交叉,希望一切都没有中断。
让我们尝试编写仅包含旧的“ divine”对象的API所需子集的接口,而不是在新代码中盲目地使用过时的代码。 假设我们在旧版代码中有一个
User
对象,该对象了解一切。 他知道如何更改电子邮件和密码,如何将论坛用户升级为主持人,如何更新公共用户个人资料,设置通知首选项,将自己保存在数据库中等等。
src /旧版/ User.php <?php namespace Legacy; class User { public $email; public $password; public $role; public $name; public function promote($newRole) { $this->role = $newRole; } public function save() { db_layer::save($this); } }
这是一个粗略的示例,但反映了问题:每个属性都是公共的,可以轻松更改为任何值,我们需要记住在对
save
进行任何更改后显式调用
save
方法,等等。
让我们限制自己,禁止访问这些公共属性,并尝试猜测过时的系统如何工作,同时增加用户权限:
src / LegacyBridge / Promoter.php <?php namespace LegacyBridge; interface Promoter { public function promoteTo(Role $role); }
src / LegacyBridge / LegacyUserPromoter.php <?php namespace LegacyBridge; class LegacyUserPromoter implements Promoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function promoteTo(Role $newRole) { $newRole = (string) $newRole;
现在,当我们想增加新代码中的
User
权限时,我们使用
LegacyBridge\Promoter
界面,该界面处理了在过时的系统中增加用户的所有细微
LegacyBridge\Promoter
。
更改传统语言
过时的代码接口使我们有机会改进系统的设计,并且可以使我们免于很久以前出现的命名错误。 将用户角色从主持人更改为参与者的过程不是“提升”(增加),而是“降级”(减少)。 即使遗留代码看起来相同,也没有人阻止我们为这些不同的事物创建两个接口。
src / LegacyBridge / Promoter.php <?php namespace LegacyBridge; interface Promoter { public function promoteTo(Role $role); }
src / LegacyBridge / LegacyUserPromoter.php <?php namespace LegacyBridge; class LegacyUserPromoter implements Promoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function promoteTo(Role $newRole) { if ($newRole->isMember()) { throw new \Exception("Can't promote to a member."); } $legacyMemberRole = 2; $this->legacyUser->promote($legacyMemberRole); $this->legacyUser->save(); } }
src / LegacyBridge / Demoter.php <?php namespace LegacyBridge; interface Demoter { public function demoteTo(Role $role); }
src / LegacyBridge / LegacyUserDemoter.php <?php namespace LegacyBridge; class LegacyUserDemoter implements Demoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function demoteTo(Role $newRole) { if ($newRole->isModerator()) { throw new \Exception("Can't demote to a moderator."); } $legacyModeratorRole = 1; $this->legacyUser->promote($legacyModeratorRole); $this->legacyUser->save(); } }
不是很大的变化,但是代码的目的变得更加清晰。
现在,下次您需要从过时的代码中调用某些方法时,请尝试为它们创建一个接口。 这可能是不可能的,可能会太昂贵。 我知道这个“神圣”对象的静态方法实际上非常简单易用,借助它的帮助,您可以更快地完成工作,但至少可以考虑使用此选项。 您只是可以略微改善正在创建的新系统的设计。