上篇介紹如何建立 android test project 並成功運行 test case 之後,本篇開始介紹重構的過程,必須先說明的是重構的手法因人而異
本篇重構的過程或方法不是唯一的選擇,舉例來說 code smell 中 第4點 過長的參數列(Long Parameter List),可以選擇的手法有3個
必須視情況選擇重構方法,但最後的結果都會讓整體更清晰更容易了解。
 
首先是Target class -> GameMap.java

/**
 * Map of game
 */
public class GameMap implements IGameMap
{
    private static final int MAP_UNKNOW = 0;
    private static final int MAP_BARRIER = 1;
    private static final int MAP_BLANK = -1;
    private static final int RAW_MAP_DATA_ONE = 1;
    private static final int BASIC_MAP_INDEX = 0;
    protected int mMapWidth;
    protected int mMapHeight;
    protected Bitmap mMapBitmap = null;
    protected Canvas mCanvas;
    private Context mContext;
    public boolean mIsMerging = true;
    public boolean mIsConvertDisable = false;
    public GameMap(Context context)
    {
        mContext = context;
    }
    @Override
    public void createMap(Map map)
    {
        ArrayList<Map> maps = new ArrayList<Map>();
        maps.add(map);
        createMaps(maps);
    }
    @Override
    public void createMaps(ArrayList<Map> maps)
    {
        if (maps.isEmpty() || mIsConvertDisable)
            return;
        if (maps.get(0) == null)
            return;
        //obtain raw map's width and height
        mMapWidth = maps.get(0).getWidth();
        mMapHeight = maps.get(0).getHeight();
        // initial empty map
        if (mMapBitmap == null) {
            if (mMapWidth <= 0 || mMapHeight <= 0)
                return;
            mMapBitmap = Bitmap.createBitmap(mMapWidth, mMapHeight,
                    Bitmap.Config.ARGB_4444);
            mCanvas = new Canvas(mMapBitmap);
        }
        mIsMerging = true;
        drawMapsWithColor(maps);
        mIsMerging = false;
    }
    private void drawMapsWithColor(ArrayList<Map> maps)
    {
        for (Map map : maps) {
            int[] coloredMap = TransforByteArrayToColoredMap(map);
            if (coloredMap != null) {
                mCanvas.drawBitmap(coloredMap, 0, map.getWidth(), 0, 0,
                        map.getWidth(), map.getHeight(), true, null);
            }
        }
    }
    // change byte raw data into a color raw data
    private int[] TransforByteArrayToColoredMap(Map map)
    {
        byte[] prePixels = map.getData();
        int length = prePixels.length;
        int[] colorData = null;
        try {
            colorData = new int[length];
        } catch(OutOfMemoryError E){
            mIsConvertDisable = true;
            return null;
        }
        int colorUnknow = mContext.getResources().getColor(
                R.color.map_unknow);
        int colorBarrier = mContext.getResources().getColor(
                R.color.map_barrier);
        int colorBlank = mContext.getResources().getColor(
                R.color.map_space);
        int colorOfVirtualWall = mContext.getResources().getColor(
                R.color.vw_paint);
        int colorOfTouchZone = mContext.getResources().getColor(
                R.color.tz_save);
        int mapIdx = map.getIndex();
        if (mapIdx == BASIC_MAP_INDEX) { // basic map
            for (int i = 0; i < length; ++i) {
                switch (prePixels[i]) {
                case MAP_UNKNOW:
                    colorData[newArrayPtr(i)] = colorUnknow;
                    break;
                case MAP_BARRIER:
                    colorData[newArrayPtr(i)] = colorBarrier;
                    break;
                case MAP_BLANK:
                    colorData[newArrayPtr(i)] = colorBlank;
                    break;
                }
            }
        } else if ((mapIdx >= Fly.VIRTUALWALL_MAPINDEX_LOWERBOUND)
                && (mapIdx <= Fly.VIRTUALWALL_MAPINDEX_UPPERBOUND)) {
            // map of virtual wall
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = colorOfVirtualWall;
            }
        } else if (mapIdx < Fly.COVERAGEMAP_MAPINDEX) {
            // map of clean area
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = colorOfTouchZone;
            }
        } else { // map of other raw data
            String color = getColorOfMap(map.getIndex());
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = Color.parseColor(color);
            }
        }
        return colorData;
    }
    private String getColorOfMap(int mapIndex)
    {
        String color = null;
        GlobalData globalData = (GlobalData) mContext.getApplicationContext();
        String model = globalData.Info().getModel();
        MapManager mapManager = new MapManager(mContext);
        List<MapLayer> supportedMapLayers = mapManager
                .getSupportedLayers(model);
        for (MapLayer map : supportedMapLayers) {
            if (mapIndex == Integer.valueOf(map.getMapIndex()))
                color = map.getColor();
        }
        return color;
    }
    // create new array pointer, startup from left-up side
    private int newArrayPtr(int oldPtr)
    {
        int newPtr;
        int remainderNum = oldPtr % mMapWidth;
        int quotientNum = (oldPtr - remainderNum) / mMapWidth;
        newPtr = mMapWidth * (mMapHeight - quotientNum - 1) + remainderNum;
        return newPtr;
    }
    @Override
    public Bitmap getBlendingBitmap()
    {
        return mMapBitmap;
    }
    @Override
    public boolean isMergingMaps()
    {
        return mIsMerging;
    }
    @Override
    public void disableConvert(boolean isDisable)
    {
        mIsConvertDisable = isDisable;
    }
    @Override
    public boolean isDisableConvert()
    {
        return mIsConvertDisable;
    }
}

 
從類別屬性開始,有幾個屬性的存取權限超過使用範圍,在物件導向中,封裝是主要的特徵之一,我們應該隱藏細節,不該暴露不必要的資訊
mIsMerging 和 mIsConvertDisable 的使用範圍都在 GameMap
(使用 eclipse 可以在屬性上按下 ctrl + alt + h,會列出該屬性被引用的所有位置)
相同的情況也出現在 protected 的屬性上,所以第一步先封裝不必要暴露給外界的資訊

/**
 * Map of game
 */
public class GameMap implements IConvertMap
{
    ...
    private int mMapWidth;
    private int mMapHeight;
    private Bitmap mMapBitmap;
    private Canvas mCanvas;
    private Context mContext;
    private boolean mIsMerging = true;
    private boolean mIsConvertDisable = false;
    ...
}

 
接著看到 TransforByteArrayToColoredMap函式,這個函式長度實在太長,保持小函式具有許多優點,重用性高,修改不容易出問題,而函數名稱開頭應該改為小寫

    private int[] TransforByteArrayToColoredMap(Map map)
    {
        byte[] prePixels = map.getData();
        int length = prePixels.length;
        int[] colorData = null;
        try {
            colorData = new int[length];
        } catch(OutOfMemoryError E){
            mIsConvertDisable = true;
            return null;
        }
        int colorUnknow = mContext.getResources().getColor(
                R.color.map_unknow);
        int colorBarrier = mContext.getResources().getColor(
                R.color.map_barrier);
        int colorBlank = mContext.getResources().getColor(
                R.color.map_space);
        int colorOfVirtualWall = mContext.getResources().getColor(
                R.color.vw_paint);
        int colorOfTouchZone = mContext.getResources().getColor(
                R.color.tz_save);
        int mapIdx = map.getIndex();
        if (mapIdx == BASIC_MAP_INDEX) { // basic map
            for (int i = 0; i < length; ++i) {
                switch (prePixels[i]) {
                case MAP_UNKNOW:
                    colorData[newArrayPtr(i)] = colorUnknow;
                    break;
                case MAP_BARRIER:
                    colorData[newArrayPtr(i)] = colorBarrier;
                    break;
                case MAP_BLANK:
                    colorData[newArrayPtr(i)] = colorBlank;
                    break;
                }
            }
        } else if ((mapIdx >= Fly.VIRTUALWALL_LOWERBOUND)
                && (mapIdx <= Fly.VIRTUALWALL_UPPERBOUND)) {
            // map of virtual wall
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = colorOfVirtualWall;
            }
        } else if (mapIdx < Fly.COVERAGEMAP_MAPINDEX) {
            // map of clean area
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = colorOfTouchZone;
            }
        } else { // map of other raw data
            String color = getColorOfMap(map.getIndex());
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = Color.parseColor(color);
            }
        }
        return colorData;
    }

 
第14~23行有一堆暫時變數,這些暫時變數沒有任何的好處,使用 replace temp with query 來消除這些變數,代價是新增getColorIndexFromRes函式

    private int[] transforByteArrayToColoredMap(Map map)
    {
        byte[] prePixels = map.getData();
        int length = prePixels.length;
        int[] colorData = null;
        try {
            colorData = new int[length];
        } catch (OutOfMemoryError E) {
            mIsConvertDisable = true;
            return null;
        }
        int mapIdx = map.getIndex();
        if (mapIdx == BASIC_MAP_INDEX) { // basic map
            for (int i = 0; i < length; ++i) {
                switch (prePixels[i]) {
                    case MAP_UNKNOW:
                        colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_unknow);
                        break;
                    case MAP_BARRIER:
                        colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_barrier);
                        break;
                    case MAP_BLANK:
                        colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_space);
                        break;
                }
            }
        } else if ((mapIdx >= Fly.VIRTUALWALL_LOWERBOUND)
                && (mapIdx <= Fly.VIRTUALWALL_UPPERBOUND)) {
            // map of virtual wall
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.vw_paint);
            }
        } else if (mapIdx < Fly.COVERAGEMAP_MAPINDEX) {
            // map of clean area
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.tz_save);
            }
        } else { // map of other raw data
            String color = getColorOfMap(map.getIndex());
            for (int i = 0; i < length; ++i) {
                if (prePixels[i] == RAW_MAP_DATA_ONE)
                    colorData[newArrayPtr(i)] = Color.parseColor(color);
            }
        }
        return colorData;
    }
    private int getColorIndexFromRes(int res)
    {
        return mContext.getResources().getColor(res);
    }

 
接著仔細看看第15,29,36,42行的內容,看起來就像根據不同的條件是填滿colorData內容,把條件式的內容提取出來成函數並賦予適當的名稱(fillColorDataForXXX)

    private int[] transforByteArrayToColoredMap(Map map)
    {
        byte[] prePixels = map.getData();
        int length = prePixels.length;
        int[] colorData = null;
        try {
            colorData = new int[length];
        } catch (OutOfMemoryError E) {
            mIsConvertDisable = true;
            return null;
        }
        int mapIdx = map.getIndex();
        if (mapIdx == BASIC_MAP_INDEX) {
            fillColorDataForBasicMap(prePixels, length, colorData);
        } else if ((mapIdx >= Fly.VIRTUALWALL_LOWERBOUND)
                && (mapIdx <= Fly.VIRTUALWALL_UPPERBOUND)) {
            fillColorDataForVirtualWall(prePixels, length, colorData);
        } else if (mapIdx < Fly.COVERAGEMAP_MAPINDEX) {
            fillColorDataForTouchZone(prePixels, length, colorData);
        } else {
            fillColorDataForOtherMap(map, prePixels, length, colorData);
        }
        return colorData;
    }
    private void fillColorDataForOtherMap(Map map, byte[] prePixels, int length, int[] colorData)
    {
        String color = getColorOfMap(map.getIndex());
        for (int i = 0; i < length; ++i) {
            if (prePixels[i] == RAW_MAP_DATA_ONE)
                colorData[newArrayPtr(i)] = Color.parseColor(color);
        }
    }
    private void fillColorDataForTouchZone(byte[] prePixels, int length, int[] colorData)
    {
        for (int i = 0; i < length; ++i) {
            if (prePixels[i] == RAW_MAP_DATA_ONE)
                colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.tz_save);
        }
    }
    private void fillColorDataForVirtualWall(byte[] prePixels, int length, int[] colorData)
    {
        for (int i = 0; i < length; ++i) {
            if (prePixels[i] == RAW_MAP_DATA_ONE)
                colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.vw_paint);
        }
    }
    private void fillColorDataForBasicMap(byte[] prePixels, int length, int[] colorData)
    {
        for (int i = 0; i < length; ++i) {
            switch (prePixels[i]) {
                case MAP_UNKNOW:
                    colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_unknow);
                    break;
                case MAP_BARRIER:
                    colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_barrier);
                    break;
                case MAP_BLANK:
                    colorData[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_space);
                    break;
            }
        }
    }
    private int getColorIndexFromRes(int res)
    {
        return mContext.getResources().getColor(res);
    }

 
第16,19,21,23行的參數出現了資料泥團(data clumps)的怪味,事實上 prePixels, length, colorData 的來源都是 map
但因為 map 並沒有提供相關的取值函式讓我們可以直接調用。
可以使用 Introduce Parameter Object,建立一個新的類別將Map包裝起來,並在其中寫一些委託函式,讓外界方便呼叫,
其實這個新的類別也有Facade pattern 的形式存在,就簡單的命名為MapFacade

    class MapFacade {
        private Map mMap;
        private int[] mColorData;
        public MapFacade(Map map){
            mMap = map;
            try {
                mColorData = new int[mMap.getData().length];
            } catch (OutOfMemoryError e) {
                mIsConvertDisable = true;
                mColorData = null;
            }
        }
        public byte[] getDataOfMap(){
            return mMap.getData();
        }
        public int getLengthOfMapData(){
            return mMap.getData().length;
        }
        public int[] getColorData(){
            return mColorData;
        }
        public int getIndexOfMap(){
            return mMap.getIndex();
        }
    }
    private int[] transforByteArrayToColoredMap(Map map)
    {
        MapFacade mapFacade = new MapFacade(map);
        if (mapFacade.getColorData() == null) {
            return null;
        }
        int mapIdx = mapFacade.getIndexOfMap();
        if (mapIdx == BASIC_MAP_INDEX) {
            fillColorDataForBasicMap(mapFacade);
        } else if ((mapIdx >= Fly.VIRTUALWALL_LOWERBOUND)
                && (mapIdx <= Fly.VIRTUALWALL_UPPERBOUND)) {
            fillColorDataForVirtualWall(mapFacade);
        } else if (mapIdx < Fly.COVERAGEMAP_MAPINDEX) {
            fillColorDataForTouchZone(mapFacade);
        } else {
            fillColorDataForOtherMap(mapFacade);
        }
        return mapFacade.getColorData();
    }
    private void fillColorDataForOtherMap(MapFacade mapFacade)
    {
        for (int i = 0; i < mapFacade.getLengthOfMapData(); ++i) {
            if (mapFacade.getDataOfMap()[i] == RAW_MAP_DATA_ONE)
                mapFacade.getColorData()[newArrayPtr(i)] = Color.parseColor(getColorOfMap(mapFacade.mMap.getIndex()));
        }
    }
    private void fillColorDataForTouchZone(MapFacade mapFacade)
    {
        for (int i = 0; i < mapFacade.getLengthOfMapData(); ++i) {
            if (mapFacade.getDataOfMap()[i] == RAW_MAP_DATA_ONE)
                mapFacade.getColorData()[newArrayPtr(i)] = getColorIndexFromRes(R.color.tz_save);
        }
    }
    private void fillColorDataForVirtualWall(MapFacade mapFacade)
    {
        for (int i = 0; i < mapFacade.getLengthOfMapData(); ++i) {
            if (mapFacade.getDataOfMap()[i] == RAW_MAP_DATA_ONE)
                mapFacade.getColorData()[newArrayPtr(i)] = getColorIndexFromRes(R.color.vw_paint);
        }
    }
    private void fillColorDataForBasicMap(MapFacade mapFacade)
    {
        for (int i = 0; i < mapFacade.getLengthOfMapData(); ++i) {
            switch (mapFacade.getDataOfMap()[i]) {
                case MAP_UNKNOW:
                    mapFacade.getColorData()[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_unknow);
                    break;
                case MAP_BARRIER:
                    mapFacade.getColorData()[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_barrier);
                    break;
                case MAP_BLANK:
                    mapFacade.getColorData()[newArrayPtr(i)] = getColorIndexFromRes(R.color.map_space);
                    break;
            }
        }
    }
    private int getColorIndexFromRes(int res)
    {
        return mContext.getResources().getColor(res);
    }

 
MapFacade 包含了 Map 物件並提供 getDataOfMap, getColorData, getLengthOfMapData 方便外部存取,
也改善了 fillColorDataForXXX 系列的參數,這次步驟因為變化較大,執行 test case 看看結果。
 
看起來沒有破壞原先的程式行為,重構之後原本的 transforByteArrayToColoredMap 函式清晰度提高了不少,被提煉出來的方法也增加了重用性。