前言
本文簡單介紹一個(gè)重構(gòu)到策略模式的小例子。
GitHub地址:https://github.com/janipeng/GildedRose.git
里面有三個(gè)branch,分別是我做了三次重構(gòu)的過程,有一點(diǎn)點(diǎn)差別。重構(gòu)過程中的視頻如下。錄制的是forVideo這個(gè)branch。視頻地址如下:
http://v.youku.com/v_show/id_XMzQ2MTI0ODczNg==.html?spm=a2h3j.8428770.3416059.1
因?yàn)橛信笥颜f視頻中沒有講解過程,所有我又重新錄制了一個(gè)視頻。這個(gè)視頻是錄制的forVideoTwo 這個(gè)branch的,有一些簡單的講解和IntelliJ IDEA快捷鍵的說明,IDE快捷鍵的使用可以幫助我們提升寫代碼的體驗(yàn)和效率,也是我這里想要表達(dá)的,完成這個(gè)重構(gòu)練習(xí)的過程中,完全可以不需要使用鼠標(biāo),所有的操作都可以用鍵盤快捷鍵來操作。視頻地址如下:
http://v.youku.com/v_show/id_XMzQ2NDE4MDMxNg==.html?spm=a2h3j.8428770.3416059.1
需要重構(gòu)的代碼:
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 太多,而且很多嵌套。代碼的可讀性和可維護(hù)性非常差。
重構(gòu)
有必要再啰嗦一下重構(gòu)的定義:
名詞定義:對軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整,目的是在不改變軟件可觀察行為的前提下,提高其可靠性,降低其修改成本。
動(dòng)詞定義:使用一系列軟件重構(gòu)手法,在不改變軟件可觀察行為的前提下,調(diào)整其結(jié)構(gòu)。
重構(gòu)的幾個(gè)簡單規(guī)則:
去除代碼壞味道(Remove Code Smell)
代碼總是工作(Always Work)
可以隨時(shí)停止(Can Stop at Anytime)
可以隨時(shí)開始(Can Continue at Anytime)
可以隨時(shí)恢復(fù)(Can Revert at Anytime)
重構(gòu)的一個(gè)有趣的口訣:
舊的不變(Keep Old)
新的創(chuàng)建(Create New)
一步切換(Switch to New)
舊的再見(Bye to Old)
在這個(gè)重構(gòu)練習(xí)中,我會盡量遵守上面的規(guī)則。并且做到“小步提交,頻繁測試”。
重構(gòu)有一個(gè)非常重要的前提: 需要有足夠的自動(dòng)化測試。
在這次重構(gòu)練習(xí)之前,我先給需要重構(gòu)的代碼加上了單元測試。加單元測試的過程不在錄制視頻中。單元測試的代碼可以去GitHub上面看。
重構(gòu)過程分析
GildedRose 中的 updateQuality 方法,if else 條件分支和嵌套很多。代碼的可讀性非常差,想要通過看代碼理解代碼邏輯會非常困難。簡單分析一下,可以發(fā)現(xiàn),updateQuality 方法是更新item的quality和sellIn的,并且不同的item會有不同的更新規(guī)則。不同的item有不同的策略進(jìn)行更新,比較容易可以想到用策略模式。
根據(jù)上面提到的重構(gòu)的口訣,我們先不去修改舊的代碼,而是添加一個(gè)新的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);
}
}
在這個(gè)function里面先創(chuàng)建一個(gè)ItemStrategy的接口類。里面有一個(gè)update的function。然后根據(jù)item的name去創(chuàng)建不同的實(shí)現(xiàn)類。
接著,把updateQuality里面的代碼拷貝到每一個(gè)具體的實(shí)現(xiàn)類里面的update方法。然后把updateQuality2替換updateQuality。跑一下所有的單元測試。
接著,看每一個(gè)ItemStrategy的具體實(shí)現(xiàn)類。如下面的NormalItem。把所有跟NormalItem沒關(guān)系的代碼都刪掉。就變成下面的樣子:
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);
}
}
}
}
當(dāng)然,這個(gè)里面的代碼還是可以繼續(xù)在進(jìn)行一些重構(gòu)的,但不是這次重構(gòu)的重點(diǎn)。
更多更詳細(xì)的內(nèi)容可以去GitHub上面看,這里就不過多闡述了。
小結(jié)
本文介紹的一個(gè)比較簡單的重構(gòu)練習(xí)題,但是如果沒有想到用這樣的方式去重構(gòu)的話,其實(shí)也并不是很容易。在實(shí)際工作的生產(chǎn)代碼里面,假如有根據(jù)不同的類型有不同的處理邏輯,可以考慮用這個(gè)的方式去做重構(gòu)。
同時(shí),在反復(fù)多次完成這個(gè)重構(gòu)練習(xí)的過程中,對IntelliJ IDEA的使用會有很好的練習(xí)。
如果想成為畫家,可以先從練習(xí)畫雞蛋開始。就像這樣。
最后,這份代碼的需求是一個(gè)Coding Dojo的題目。大家有興趣可以看下圖中的需求去用TDD的形式做一個(gè)練習(xí)。后期,我也會嘗試寫一些Coding Dojo的文章分享出來。希望可以自己練習(xí)和給大家一定的幫助。