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