报告班长,图片截自网路
大家有听过「报告班长」吗?这部 1987 年的电影,当年推出後一炮而红,带领一阵中华民国军教片的风潮,由庾澄庆演唱的同名片尾曲也为这位歌手打响名号。但你有发现,有句歌词唱错了吗?
歌词里有句唱着:「出枪快、转枪慢」,但是根据中华民国单兵作战准则,标准用枪技巧,为了使单兵记得出枪动作不要太大惊动敌人,且准备好後立即进入备射姿势,其实应该要是「出枪慢、转枪面快」才对。
你想,要是在战场上,阿兵哥想起歌词,先快速出枪吸引敌人注意,再慢慢准备射击动作,那还怎麽活命啊?幸好大家都没有很认真在背歌词,不然我们要怎麽反共救国呢?
好,总之我们要来提枪上阵了。本文将实际找一个案例,在程序与测试都具备的情况下,试着透过一些重构手法,将原程序调整成可读性与易修改性都比较好的样貌,同时不破坏原逻辑。最後验证当新需求来临时,如此重构後的新设计,是否真能限缩修改范围,以更快速地因应。
Console Interaction 是笔者在网路上找到的一个 TDD 练习题,这里我们还没有要聊 TDD 的部份,所以我们假设已经有了可运行的程序,并且有测试保护。接下来我们会先了解题目内容,再进行下一步:
大家可能像笔者年纪够大,有玩过像 Mud 这种「线上文字游戏」,如果没有,那应该也使用过坊间一些使用 CLI 介面来互动的电脑软件。这种介面没有视窗介面那麽美观亲切,但在某些场合还蛮能快速解决问题的,譬如安装系统应用程序。
图片截自 Wikipedia
Console Interaction 的题目要求我们为一个「计算周长与面积」的应用程序编写 CLI 程序。此程序先问使用者要计算矩型(Rectangle)还是圆型(Circle),再依使用者输入,决定下一个问题,直到问完必须的问题,就会秀出该图型的周长与面积。
这个题目要完成不难,但要写得好测不容易,关键在开发者有没有发现陷阱,把「使用者介面」与「主逻辑」分开。什麽意思?其实题目虽名为 Console Interaction,但其实使用者介面应该要是可以随时抽换的。意即,你就算把介面换成 GUI,甚至是 Web,都不该影响核心的「状态」与「资料」。
看穿了这一点,我们发现我们其实可以只管「主逻辑」的程序与测试就好。上文有提到这篇文章着重在「重构」,於是我们假设有个小天使为我们完成了初版的程序如下:
public class Module {
private final String RECTANGLE_B_SELECTED = "RectangleBSelected";
private final String RECTANGLE_A_SELECTED = "RectangleASelected";
private final String RECTANGLE_SELECTED = "RectangleSelected";
private final String INITIAL = "Initial";
private String status = INITIAL;
private int a;
private int b;
public String print() {
if (this.status.equals(RECTANGLE_SELECTED)) {
return "Rectangle side A length?";
}
if (this.status.equals(RECTANGLE_A_SELECTED)) {
return "Rectangle side B length?";
}
if (this.status.equals(RECTANGLE_B_SELECTED)) {
return "Area=" + (a * b) + ", Circumference=" + (2 * (a + b));
}
return "Shape: (C)ircle or (R)ectangle?";
}
public void input(String answer) {
if (this.status.equals(INITIAL) && answer.equals("R")) {
this.status = RECTANGLE_SELECTED;
} else if (this.status.equals(RECTANGLE_SELECTED)) {
try {
Integer answerInt = Integer.valueOf(answer);
this.a = answerInt;
this.status = RECTANGLE_A_SELECTED;
} catch (NumberFormatException e) {
return;
}
} else if (this.status.equals(RECTANGLE_A_SELECTED)) {
try {
Integer answerInt = Integer.valueOf(answer);
this.b = answerInt;
this.status = RECTANGLE_B_SELECTED;
} catch (NumberFormatException e) {
return;
}
}
}
}
上面的程序,把主逻辑命名为 Module,并且定义 Module 的状态,每当 User Interface 呼叫 Module 的 print 或 input 指令, Module 就先判断自己现在处於什麽状态,并进行对应的行为。
程序完成了,测试当然也不能少,於是补上测试,确保功能正常。这里碍於篇幅,只列出大约 1/3 的测项,但应能足够看出测试的设计与意图:
class ConsoleInteractionTest {
@Test
void initial_and_print() {
Module module = new Module();
String printed = module.print();
Assertions.assertEquals("Shape: (C)ircle or (R)ectangle?", printed);
}
@Test
void initial_and_R() {
Module module = new Module();
module.input("R");
String printed = module.print();
Assertions.assertEquals("Rectangle side A length?", printed);
}
@Test
void initial_and_R_5() {
Module module = new Module();
module.input("R");
module.input("5");
String printed = module.print();
Assertions.assertEquals("Rectangle side B length?", printed);
}
// ... 後略
}
至此,我停下来了。读者应该有发现,我只完成一半工作,Circle 的逻辑还没做。为什麽不继续?因为这里已经有非常明显的坏味道了,如果硬着头皮继续,晚点重构成本会太高。因此,我决定在此暂停一下,闻闻坏味道,重构一下,然後再继续。
首先,If 太多了!每次 User Interface 与 Module 互动,Module 就要经过一连串的 If 判断,才能找到对应的行为。这里还没把 Circle 相关的状态们放进来,print 与 input 就要承担这麽多责任,很明显违反了「单一职责原则」,使得这两个方法同时变成了修改的热点,需要改善。
另外,在 input 的字串处理的逻辑也重复了,重复永远是我们要极力避免的坏味道,这点也需要改善。
其实说到底,这个 Module 的「资料」与「状态」被绑在一起了,但「状态」与「状态对应的处理逻辑」又被分开了。换句话说,在 Module 的方法被呼叫时,「现在该怎麽处理」应该要与「现在是什麽状态」应该要被独立抽到不同类别去才对,而 Module 只要负责「放一个状态在身上」就好,其他该做什麽事、什麽时候该跳状态,让状态他们自己去协调就好。
聪明如你,可能已经想到了,这不就是「状态模式」的场景吗?没错,你果真有读书!既然如此我们就来试着重构成状态模式吧!决定了重构的方向,又有测试保护,那就直接开工吧!因为重构步骤繁多(我们没有要「重写」),用文字不容易表达,这里用一段影片,来示范重构细节:
小步骤重构,参考影片
现在我们再来试着加入刚刚未完成的 Circle 逻辑,看看已经重构成「状态模式」的这段程序,在需要加入新状态时好不好加。都已经使用了状态模式,理应符合「开放封闭原则」,这时应该只需新增类,不需修改旧类别才对。试了就知道:
重构完後,接着完成剩余功能
今天我们藉由一个程序题,走了一遍分析、写程序、写测试、发现有 Code Smell、停下来再分析、接着再重构的流程。在日常工作中,不一定会经常遇到这麽复杂的场景,但这只是个练习,主要是希望读者们参考一下这样的工作方式,未来在工作上可以也试着照这样的流程,一小步一小步慢慢前进,而使每一步都走得稳健而安全,才能做自己老是在「安全的环境」中进行每天的工作。最後,惟有看出设计模式的必要性,才改写成设计模式。
谜之声:「设计模式是重构出来的,不是规划出来的。」
ithelp2021
>>: DAY 5 Big Data 5Vs – Volume(容量) - RedShift
前言 Hi, 我是鱼板伯爵今天要教大家如何初始化 Environment,在未来的开发的过程中可以快...
infrastructure 也可以 for each 之三 课程内容与代码会放在 Github 上...
大家好~ 今天来把我们 Server 环境处理好吧~ SSH 先将我们昨天下载的金钥修改一下权限。 ...
Day2 环境架设 前言 一般来说,蛮多人会用Jupyter notebook来进行Python的入...
简述音乐情绪模型 看完了昨天的介绍之後,我们知道音乐跟情绪是有相关且可以被分类的,而把这其中关系模型...