重构遗留代码(9):分析 Concerns

旧代码,丑陋的代码,复杂的代码,意大利面条似的代码,鬼话废话……就是四个字:遗留代码。这是一个系列文章,将有助于你处理并解决它。

在这篇教程中,我们将继续关注业务逻辑。我们将评估RunnerFunctions.php是否属于某个类,如果是这样,属于哪个类?我们将考虑关注和方法所属。最后,我们将学点关于模拟的概念。那么,你还在等什么?继续阅读吧。

 

RunnerFunctions – 从面向过程到面向对象

即使我们大部分代码是面向对象的形式,很好地组织类,有些功能只是定义在文件中。我们需要采取些措施以给予RunnerFunctions.php的函数更加面向对象的样子。

我的第一本能只是将它们放在一个类中。这没什么天才的,但它是使得我们开始修改代码。让我们看看这个想法能否真正实现。

如果我们这么做,我们需要修改测试程序和GameRunner.php以使用新的类。我们暂且将类称为通用的,当需要时重命名它将是容易的。我们甚至不知道这个类是将独立存在还是将融入Game类。所以还不要担心命名。

在我们的GoldenMasterTest.php文件中,我们必须修改运行代码的方式。函数是generateOutput(),它的第三行需要被修改为创建一个新对象并调用run()。但这样测试失败了。

现在我们需要进一步修改新的类。

我们只需要修改run()方法的while表达式条件。新代码从当前类通过为didSomebodyWin()和isCurrentAnswerCorrect()准备的$this->来调用它们。

这使金牌大师程序通过了,但它破坏了Runner测试。

问题在assertAnswersAreCorrectFor()中,但首先创建一个Runner对象很容易解决。

同样的问题也需要在三个其他的函数中部署。

虽然这使代码通过了,但它引入了一些代码重复。因为我们现在所有的测试程序都通过了,可以提取Runner创建到setUp()方法中了。

干得漂亮。所有这些新的创建和重构让我思考。我们命名变量runner。也许我们的类也可以以相同名字命名。让我们重构它。应该是容易的。

如果在上面的框你没有勾选“Search for text occurrences”,不要忘了手动修改你包括的范围,因为重构也会重命名文件。

现在我们有了一个文件命名为GameRunner.php,另一个命名为Runner.php,还有第三个命名为Game.php。我不知道对你怎么样,但这似乎让我很困惑。如果这是在我生命中第一次见到这三个文件,我会不知道哪个是做什么的。我们需要至少移除它们中的一个。

在我们重构的早期阶段,创建RunnerFunctions.php的原因是构建一种为测试包含所有方法和文件的方式。我们需要进入任何代码,但不是运行所有代码,除非在金牌大师程序中的已准备的环境中。我们仍可以做相同的事,只是不从GameRunner.php运行代码。在我们继续之前,需要更新包含和在代码里创建新的类。

这将做到这点。我们需要明确包含Display.php,以便当Runner试图创建新的CLIDisplay的时候,它会知道去实现什么。

 

分析关注

我认为面向对象编程最重要的特性之一是分析关注。我常常问我自己的问题是,“这个类做了它的名字所表达的意思吗?”,“这个方法关注的是这个对象吗?”,“我的对象应该关心具体值吗?”。

令人惊异的是,这些类型的问题在澄清业务域和软件架构上有着巨大的作用。在Syneto,我们在小组中问答这些类型的问题。许多时候当一个开发者有了困境,他或者她只要站起来,从团队中请求2分钟的关注来了解我们对于一个课题的意见。那些对代码架构熟悉的人会从软件的角度回答,而另一些对业务领域更熟悉的人可能揭示业务方面一些重要的见解。

让我们试着考虑下我们的例子中的关注。我们可以继续专注于Runner类。比起Game类,这个类非常有可能消除或者改造。

首先,Runner应该关心isCurrentAnswerCorrect()怎么运作的吗?Runner应该有任何关于问题和答案的知识吗?

看起来似乎这个方法从Game类消除更好点。我强烈认为关于问答的Game类应该关心答案正确与否。我真的相信Game类必须关注为当前问题提供答案的结果。

是时候行动了。我们将做移动方法重构。正如从我之前的教程中我们已经看到的,我将只给你看最终结果。

有必要注意到不只是方法没了,而且用来定义答案限制的常量也没了。

但didSomebodyWin()怎么样?Runner应该决定何时某人胜出吗?如果我们看看方法体,我们可以看到一个问题如黑夜中的手电筒一样突出。

无论这个方法做了什么,它只作用在Game类对象上。它验证了从Game返回的当前答案。之后它返回了无论一个game对象在它的wasCorrectlyAnswered()或wrongAnswer()方法中返回的值。这个方法有效地没执行任何操作。它所关注的全部是Game类。这是一个经典的代码示例,称为依恋情。一个类做了另一个类应该做的事。是时候移动它了。

像往常一样,我们首先移动测试程序。测试驱动开发?任何人?

这让我们没有更多的测试可以运行,所以这个文件现在可以移走了。删除是编程中我最喜欢的部分。

当我们运行测试程序时,我们得到一个很漂亮的错误:

也是时候修改代码了。把方法复制并粘帖进Game类将神奇般得使所有测试通过。包括了旧的方法和移入GameTest类的方法。但将方法放在合适的位置的同时带来了两个问题:Runner也需要修改,我们传入的假Game对象因为它是Game类的一部分而并不需要做任何事。

修正Runner是很简单的。我们只需将$this->didSomebodyWin(…)修改为$aGame->didSomebodyWin(…)。在我们的下一步之后,我们将再次回到这里并修改它,测试重构。

是时候做些模拟了!取代使用定义在我们测试程序最后的假的类,我们将使用Mockery。它允许我们简单得覆写Game中的方法,希望它被调用并且返回我们想要的值。当然,我们可以通过假的类继承Game类并重写我们自己的方法做到这点。但工具存在了,为什么做这样的工作?

在我们第二个方法重写之后,我们可以摆脱假的Game类和任何初始化它的方法了。问题解决了!

 

最后的思考

即使我们能想到的只有Runner,但今天我们取得了很大的进步。我们学习了职责,我们定义了属于另一个类的方法和变量。我们在更高层次上思考并且我们走向了一个更好的解决方案。在Syneto团队,有一种强烈的信仰,即有办法把代码写好,并且除非使代码至少更清晰一点否则从不提交修改。

这是在经过一段时间之后,能够带来好得多的代码库,以更少的相关性,更多地测试并最终更少的错误的技术。

感谢,占用你的时间了。

收藏 评论

关于作者:EluQ

南京土著,程序员。(新浪微博:@EluQ) 个人主页 · 我的文章 · 11

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部