前言
本文简单介绍一个重构到策略模式的小例子。
GitHub地址:https://github.com/janipeng/GildedRose.git
里面有三个branch,分别是我做了三次重构的过程,有一点点差别。重构过程中的视频如下。录制的是forVideo这个branch。视频地址如下:
http://v.youku.com/v_show/id_XMzQ2MTI0ODczNg==.html?spm=a2h3j.8428770.3416059.1
因为有朋友说视频中没有讲解过程,所有我又重新录制了一个视频。这个视频是录制的forVideoTwo 这个branch的,有一些简单的讲解和IntelliJ IDEA快捷键的说明,IDE快捷键的使用可以帮助我们提升写代码的体验和效率,也是我这里想要表达的,完成这个重构练习的过程中,完全可以不需要使用鼠标,所有的操作都可以用键盘快捷键来操作。视频地址如下:
http://v.youku.com/v_show/id_XMzQ2NDE4MDMxNg==.html?spm=a2h3j.8428770.3416059.1
需要重构的代码:
package com.jani;
import java.util.ArrayList;
import java.util.List;
public class GildedRose {
static List<Item> items = null;
/**
* @param args
*/
public static void main(String[] args) {
System.out.println("OMGHAI!");
items = new ArrayList<Item>();
items.add(new Item("+5 Dexterity Vest", 10, 20));
items.add(new Item("Aged Brie", 2, 0));
items.add(new Item("Elixir of the Mongoose", 5, 7));
items.add(new Item("Sulfuras, Hand of Ragnaros", 0, 80));
items.add(new Item("Backstage passes to a TAFKAL80ETC concert", 15, 20));
items.add(new Item("Conjured Mana Cake", 3, 6));
updateQuality();
}
public static void updateQuality()
{
for (int i = 0; i < items.size(); i++)
{
if ((!"Aged Brie".equals(items.get(i).getName())) && !"Backstage passes to a TAFKAL80ETC concert".equals(items.get(i).getName()))
{
if (items.get(i).getQuality() > 0)
{
if (!"Sulfuras, Hand of Ragnaros".equals(items.get(i).getName()))
{
items.get(i).setQuality(items.get(i).getQuality() - 1);
}
}
}
else
{
if (items.get(i).getQuality() < 50)
{
items.get(i).setQuality(items.get(i).getQuality() + 1);
if ("Backstage passes to a TAFKAL80ETC concert".equals(items.get(i).getName()))
{
if (items.get(i).getSellIn() < 11)
{
if (items.get(i).getQuality() < 50)
{
items.get(i).setQuality(items.get(i).getQuality() + 1);
}
}
if (items.get(i).getSellIn() < 6)
{
if (items.get(i).getQuality() < 50)
{
items.get(i).setQuality(items.get(i).getQuality() + 1);
}
}
}
}
}
if (!"Sulfuras, Hand of Ragnaros".equals(items.get(i).getName()))
{
items.get(i).setSellIn(items.get(i).getSellIn() - 1);
}
if (items.get(i).getSellIn() < 0)
{
if (!"Aged Brie".equals(items.get(i).getName()))
{
if (!"Backstage passes to a TAFKAL80ETC concert".equals(items.get(i).getName()))
{
if (items.get(i).getQuality() > 0)
{
if (!"Sulfuras, Hand of Ragnaros".equals(items.get(i).getName()))
{
items.get(i).setQuality(items.get(i).getQuality() - 1);
}
}
}
else
{
items.get(i).setQuality(items.get(i).getQuality() - items.get(i).getQuality());
}
}
else
{
if (items.get(i).getQuality() < 50)
{
items.get(i).setQuality(items.get(i).getQuality() + 1);
}
}
}
}
}
}
这段代码很明显的坏味道就是if else 太多,而且很多嵌套。代码的可读性和可维护性非常差。
重构
有必要再啰嗦一下重构的定义:
名词定义:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可靠性,降低其修改成本。
动词定义:使用一系列软件重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的几个简单规则:
去除代码坏味道(Remove Code Smell)
代码总是工作(Always Work)
可以随时停止(Can Stop at Anytime)
可以随时开始(Can Continue at Anytime)
可以随时恢复(Can Revert at Anytime)
重构的一个有趣的口诀:
旧的不变(Keep Old)
新的创建(Create New)
一步切换(Switch to New)
旧的再见(Bye to Old)
在这个重构练习中,我会尽量遵守上面的规则。并且做到“小步提交,频繁测试”。
重构有一个非常重要的前提: 需要有足够的自动化测试。
在这次重构练习之前,我先给需要重构的代码加上了单元测试。加单元测试的过程不在录制视频中。单元测试的代码可以去GitHub上面看。
重构过程分析
GildedRose 中的 updateQuality 方法,if else 条件分支和嵌套很多。代码的可读性非常差,想要通过看代码理解代码逻辑会非常困难。简单分析一下,可以发现,updateQuality 方法是更新item的quality和sellIn的,并且不同的item会有不同的更新规则。不同的item有不同的策略进行更新,比较容易可以想到用策略模式。
根据上面提到的重构的口诀,我们先不去修改旧的代码,而是添加一个新的function。
public static void updateQuality2() {
ItemStrategy itemStrategy;
for (Item item : items) {
switch (item.getName()) {
case "Aged Brie":
itemStrategy = new AgedBrie();
break;
case "Backstage passes to a TAFKAL80ETC concert":
itemStrategy = new BackstagePasses();
break;
case "Sulfuras, Hand of Ragnaros":
itemStrategy = new Sulfuras();
break;
default:
itemStrategy = new NormalItem();
}
itemStrategy.update(item);
}
}
在这个function里面先创建一个ItemStrategy的接口类。里面有一个update的function。然后根据item的name去创建不同的实现类。
接着,把updateQuality里面的代码拷贝到每一个具体的实现类里面的update方法。然后把updateQuality2替换updateQuality。跑一下所有的单元测试。
接着,看每一个ItemStrategy的具体实现类。如下面的NormalItem。把所有跟NormalItem没关系的代码都删掉。就变成下面的样子:
public class NormalItem implements ItemStrategy {
@Override
public void update(Item item) {
if (item.getQuality() > 0) {
item.setQuality(item.getQuality() - 1);
}
item.setSellIn(item.getSellIn() - 1);
if (item.getSellIn() < 0) {
if (item.getQuality() > 0) {
item.setQuality(item.getQuality() - 1);
}
}
}
}
当然,这个里面的代码还是可以继续在进行一些重构的,但不是这次重构的重点。
更多更详细的内容可以去GitHub上面看,这里就不过多阐述了。
小结
本文介绍的一个比较简单的重构练习题,但是如果没有想到用这样的方式去重构的话,其实也并不是很容易。在实际工作的生产代码里面,假如有根据不同的类型有不同的处理逻辑,可以考虑用这个的方式去做重构。
同时,在反复多次完成这个重构练习的过程中,对IntelliJ IDEA的使用会有很好的练习。
如果想成为画家,可以先从练习画鸡蛋开始。就像这样。
最后,这份代码的需求是一个Coding Dojo的题目。大家有兴趣可以看下图中的需求去用TDD的形式做一个练习。后期,我也会尝试写一些Coding Dojo的文章分享出来。希望可以自己练习和给大家一定的帮助。