在上篇介紹如何建立 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 函式清晰度提高了不少,被提煉出來的方法也增加了重用性。