分類
Refactoring

Refactoring example : GameMap

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

分類
Refactoring

Create test project for android project

保持重構後程式運行的正確性是重構前的第一考量

因此在重構前一定要先建立對應的測試系統,這裡使用 junit 來建立 android project 的 test project
 

1. 為了避免測試碼和程式碼的混淆,另外建立 android test project 來存放 test code

Eclipse -> File -> New -> Other -> Android Test Project
 

2. 在 android test project 根據要測試的 class 建立相應的 package 和 test class

target class -> GameMap.java in com.foxx.map package
Create test class -> GameMapTest.java in com.foxx.map.test package
 

3. 開始在 test class 撰寫 test case

Target class(GameMap.java)如下,我的任務是了解這個class,並將它重構

/**
 * 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;
        }
        // the color should be discussed
        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;
    }
    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;
    }
}

 
GameMap有不少code smell,之後我會一一列舉出來並嘗試修正,但首先必須先建立test class(GameMapTest.java)如下, 選擇 junit version 3,
GameMapTest繼承AndroidTestCase是因為GameMap的建構式只需要傳入Context,並不需要Activity。
反之,如果今天要測試的為Activity就必須繼承ActivityInstrumentationTestCase2。

public class GameMapTest extends AndroidTestCase
{
    private GameMap mGameMap;
    private Map mMap;
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        mMap = new Map(0, 2, 2, new byte[]{0,0,1,1});
        mGameMap = new GameMap(getContext());
        testCreateMap();
    }
    public void testCreateMap(){
        mGameMap.createMap(mMap);
        assertNotNull(mGameMap);
    }
    public void testGetBlendingBitmap(){
        Bitmap bitmap = mGameMap.getBlendingBitmap();
        assertNotNull(bitmap);
    }
    public void testMerginMap(){
        boolean isMapMergin = mGameMap.isMergingMaps();
        assertEquals(false, isMapMergin);
    }
    public void testGetDisableConvert()
    {
        boolean isDisableConvert = mGameMap.isDisableConvert();
        assertEquals(false, isDisableConvert);
    }
    public void testDisableConvert(){
        mGameMap.disableConvert(true);
        boolean isDisableConvert = mGameMap.isDisableConvert();
        assertEquals(isDisableConvert, true);
    }
}

繼承 AndroidTestCase 是為了取得 context 物件並傳入 GameMap 建構式,
Test Case 基本上保持一個方法測試一個功能,而 Map mMap 則是測試資料(test fixture),也是為了要驗證重構後結果正確,setUp 方法會在所有測試方法啟動前先執行,
通常會在這裡建立測試資料,寫完 test code 就先跑跑看吧,接上實機執行 test case
 
如果想驗證測試機制的確可以運行,也可以故意寫錯一些條件,讓junit告訴你,至於 test coverage 要多少,在Refactoring有提到可以運行少部份的測試,主要是驗證重構的結果即可
下篇開始重構 GameMap.java !!!

分類
Android Uncategorized

Set unban ip in fail2ban

How to set unban ip in fail2ban??

 
1. Edit /etc/fail2ban/jail.local and find ignoreip

[DEFAULT]
# "ignoreip" can be an IP address, a CIDR mask or a DNS host
ignoreip = 127.0.0.1/8 xxx.xxx.xxx.xxx

 
2. Input your unban ip as xxx.xxx.xxx.xxx
 
Note:
1. Remember use space to identify different ip
2. fail2ban commands
 

分類
Android Uncategorized

jenkins build error : error while writing Main: Main.class (Permission denied)

Description:

Build java project in jenkins and show error message

[javac] /path_of_class.java:1: error while writing Main: /path_of_class.class (Permission denied)
[javac] public class Main
[javac]        ^
[javac] 1 error

 

Root caused:

permission of directory is not enough
 

Solution:

1.
Open Terminal and move to parent of director which at java project
2.
sudo chmod 777 director_name
 

分類
小技巧

如何簡單去除多餘的小數位 : 可指定位數 : double

Description:

假設你有個Double dou = 3.1415; 現在只想留下3.14(去除掉倒數2位), 第一個作法是把dou轉為String, 再藉由String的方法來完成, 如下

public class RemoveDecimalExample
{
    public Double removeDecimal(Double source)
    {
        String temp = source.toString();
        int dotIndex = temp.indexOf(".");
        return  Double.parseDouble(temp.substring(0, dotIndex+3));
    }
}

有點麻煩, 還要多處理2個變數,
另一種方法可藉由String.format()來轉換

public Double removeDecimalByFormat(int number, Double source)
{
    return Double.parseDouble(String.format("%."+number+"f", source));
}

參數 number 可指定想轉換的小數點位數, 若來源超過指定值會直接刪除超過的部份
 

分類
Android Uncategorized

如何簡單的從TextView, EditView取得數值

通常輸入數值都會使用EditText等等的文字框當作對象, 如何才能很快速的從這些view取得數值呢??
使用 getText().toString() 加上各個數值類別(Integer, Double, Float, Long, Byte, Short)的ParseXXX() 最快 如下

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class GetValueFromTextView extends Activity
{
    private static final int PERCENT = 100;
    private static final int MONTH_COUNT = 12;
    private EditText mInput_lend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initComponents();
    }
    private void initComponents()
    {
        mInput_lend = (EditText) findViewById(R.id.input_lend);
        mInputNumber = (EditText) findViewById(R.id.input_number);
        mInputRate = (EditText) findViewById(R.id.input_rate);
        mSubmit = (Button) findViewById(R.id.submit);
        mSubmit.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v)
            {
                mAmount.setText("" + countAmount());
            }
        });
        mAmount = (TextView) findViewById(R.id.amount);
    }
    private double countAmount()
    {
        int lend = Integer.parseInt(mInput_lend.getText().toString());
        double rateOfMonth = Double.parseDouble(mInputRate.getText().toString()) / MONTH_COUNT
                / PERCENT;
        int number = Integer.parseInt(mInputNumber.getText().toString());
        return lend * (Math.pow(rateOfMonth + 1, number));
    }
}

重點在 countAmount 方法中, 以下列方式取得string並轉換成數字

int lend = Integer.parseInt(mInput_lend.getText().toString());
double rateOfMonth = Double.parseDouble(mInputRate.getText().toString()) / MONTH_COUNT / PERCENT;
int number = Integer.parseInt(mInputNumber.getText().toString());

但要小心轉型溢位的問題
main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/main_tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/title"/>
    <TextView
        android:id="@+id/main_tv_lendHint"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/lend"/>
    <EditText
        android:id="@+id/input_lend"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:inputType="number"/>
    <TextView
        android:id="@+id/main_tv_rateHint"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/rate"/>
    <EditText
        android:id="@+id/input_rate"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:inputType="numberDecimal"/>
    <TextView
        android:id="@+id/main_tv_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/rate"/>
    <EditText
        android:id="@+id/input_number"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:inputType="number"/>
    <Button
        android:id="@+id/submit"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/btn"/>
    <TextView
        android:id="@+id/amount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

以及 strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Count Round-amount savings</string>
    <string name="title">Round-amount savings:</string>
    <string name="lend">capital(NT$):</string>
    <string name="rate">rate of year(%):</string>
    <string name="number">number of periods:</string>
    <string name="btn">count</string>
</resources>

 

分類
Android Uncategorized

取得 wifi scan ap result : ScanResult

在 android 手機中 -> 設定 -> Wi-Fi , 每隔一段時間會自動搜尋附近的wifi ap的訊息並顯示在列表上, 本篇介紹如何同步取得這些wifi ap的訊息
這些wifi ap的訊息是藉由ScanResult來顯示, ScanResult 類別相當簡單, 請參考官網
而每次搜尋wifi ap的動作都是以Broadcast的方式來達成, 所以我們只要建立一個專門來接收該動作的Broadcast Receiver 就能自動同步取得其搜尋結果(ScanResult)

package com.foxx.wifiapreceiver;
import java.util.List;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.util.Log;
public class WifiApReceiver extends BroadcastReceiver
{
    private static final String TAG = "WifiApResult";
    private Activity mActivity;
    public WifiApReceiver(Activity activity) {
        mActivity = activity;
        mActivity.registerReceiver(this, new IntentFilter(
                WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
    }
    @Override
    public void onReceive(Context context, Intent intent)
    {
        getScanResults();
    }
    private void getScanResults()
    {
        List<ScanResult> scanResults = getWifiManager().getScanResults();
        Iterator<ScanResult> iter = scanResults.iterator();
        while (iter.hasNext()) {
            showAllFieldOfScanResult(iter.next());
        }
    }
    private WifiManager getWifiManager()
    {
        return (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE);
    }
    private void showAllFieldOfScanResult(ScanResult scanResult)
    {
        Log.d(TAG, "--------------------------------------");
        Log.d(TAG, "scanResult.SSID:" + scanResult.SSID);
        Log.d(TAG, "scanResult.BSSID:" + scanResult.BSSID);
        Log.d(TAG, "scanResult.capabilities:" + scanResult.capabilities);
        Log.d(TAG, "scanResult.frequency:" + scanResult.frequency);
        Log.d(TAG, "scanResult.level:" + scanResult.level);
        Log.d(TAG, "scanResult.BSSID:" + scanResult.describeContents());
    }
    public void unregisterReceiver()
    {
        try {
            mActivity.unregisterReceiver(this);
        } catch (Exception e) {
            Log.d(TAG, "execption in unregisterReceiver");
        }
    }
}

1. 在constructor註冊了receiver
2. OnReceiver 為 callback function , 只要有搜尋wifi ap的動作, OnReceiver就會被呼叫
 
在AndroidManifest.xml加入2個必要的permission

<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>

 
使用的方式相當簡單

package com.foxx.wifiapreceiver;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity
{
    private WifiApReceiver mWifiReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initWifiReceiver();
    }
    private void initWifiReceiver(){
        mWifiReceiver = new WifiApReceiver(this);
    }
    @Override
    protected void onStop()
    {
        mWifiReceiver.unregisterReceiver();
        super.onStop();
    }
}

只要注意在onStop要unregister receiver即可
 
 
 

分類
Design Pattern

使用 Factory Method Pattern 替換多建構式

Factory Method Pattern :

提供產生物件的方法, 並讓其子類別決定產生何種類型的實體, Factory Method讓物件的產生延遲到子類別中.

 
Simple Factory Pattern 相同的例子, 同一種Dialog但在不同情況下必須有不同組合模式
Factory Method Pattern 和 Simple Factory Pattern 的不同在於 Simple Factory Pattern 將產生物件的動作全部放在單個類別中(Factory),
對於建構過程簡單的物件而言已經足夠,但缺點是若需要新增其他類別就必須修改Factory中的方法, 不符合 ocp,
解決方法可以把 Factory Method Pattern 當作 Simple Factory Pattern進階版, 建立 Factory 的子類別並將產生物件的動作寫在這些子類別中
這是原本的Dialog類別

package com.foxx.simplefactory;
import android.app.Dialog;
import android.content.Context;
import android.widget.Button;
import android.widget.TextView;
public class CustomDialog extends Dialog
{
    private String mDescription;
    private TextView mUpTextView;
    private TextView mDownTextView;
    private Button mUpButton;
    private Button mDownButton;
    /**
     * Used in one button
     */
    public CustomDialog(Context context, Button upButton) {
        super(context);
    }
    /**
     * Used in two button
     */
    public CustomDialog(Context context, Button upButton, Button downButton) {
        super(context);
    }
    /**
     * Used in one button and one description
     */
    public CustomDialog(Context context, Button upButton, String description) {
        super(context);
    }
}

 
首先把原本的Factory抽象化

package com.foxx.factorymethod;
import com.foxx.simplefactory.CustomDialog;
public abstract class CustomDialogFactory
{
    protected CustomDialog mCustomDialog;
    public CustomDialog createDialog(){
        return mCustomDialog;
    }
}

 
建立對應第一個建構式的SubFactory

package com.foxx.factorymethod;
import android.content.Context;
import android.widget.Button;
import com.foxx.simplefactory.CustomDialog;
public class OneButtonDialogFactory extends CustomDialogFactory
{
    public OneButtonDialogFactory(Context context, Button upButton) {
        mCustomDialog = new CustomDialog(context, upButton);
    }
}

 
建立對應第二個建構式的SubFactory

package com.foxx.factorymethod;
import android.content.Context;
import android.widget.Button;
import com.foxx.simplefactory.CustomDialog;
public class TwoButtonDialogFactory extends CustomDialogFactory
{
    public TwoButtonDialogFactory(Context context, Button upButton, Button downButton) {
        mCustomDialog = new CustomDialog(context, upButton, downButton);
    }
}

 
建立對應第三個建構式的SubFactory

package com.foxx.factorymethod;
import android.content.Context;
import android.widget.Button;
import android.widget.TextView;
import com.foxx.simplefactory.CustomDialog;
public class OneButtonOneDescriptionDialogFactory extends CustomDialogFactory
{
    public OneButtonOneDescriptionDialogFactory(Context context, Button upButton, String description) {
        mCustomDialog = new CustomDialog(context, upButton, description);
    }
}

 
最後是MainActivity

package com.foxx.factorymethod;
import com.foxx.simplefactory.CustomDialog;
public class MainActivity extends Activity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        CustomDialogFactory oneButtonFactory = new OneButtonDialogFactory(context, upButton);
        CustomDialog oneDialog = oneButtonFactory.createDialog();
        CustomDialogFactory twoButtonFactory = new TwoButtonDialogFactory(context, upButton, downButton);
        CustomDialog twoDialog = twoButtonFactory.createDialog();
        CustomDialogFactory oneButtonOneDescriptionFactory = new OneButtonOneDescriptionDialogFactory(context, upButton, description);
        CustomDialog oneButtonOneDescriptionDialog = oneButtonOneDescriptionFactory.createDialog();
    }
}

若之後還有第4種,第5種Dialog 可以另外新增SubFactory繼承CustomDialogFactory, 而不需要修改已經寫好的部份
任何的Dialog若有變動需要修改的話, 可到各對應的SubFactory修改即可, 不會改動到其他SubFactory
 

Class Diagram

 
factorymethod
 
 
 
 
 
 
 

分類
Jenkins

git plugin error on Jenkins : Couldn't find any revision to build. Verify the repository and branch configuration for this job.

Description :
建立完 git repo 並在jenkins新增相對應的job, 執行”馬上建置”, 在jenkins console output顯示的訊息如下

...
ERROR: Couldn't find any revision to build. Verify the repository and branch configuration for this job.
Finished: FAILURE

該錯誤目前只出現在建立完 git repo, 但未commit的情況下, 事實上開啟Terminal, 移動到git repo, 輸入git log會出現以下訊息

fatal: bad default revision 'HEAD'

只要有git commit過後, 即可解決
 
Solution :
Step 1.
新增一個檔案, 檔案內容形式不拘 e.g. ReadMe
 
Step 2.
git add ReadMe
 
Step 3.
git commit
 

分類
Script

How to auto login as root by expect script

We use “expect” to auto login as root, reference here(expect)

 
1.  Install “expect” on your machine

sudo apt-get install expect

 
2. write expect script(auto_login_root.sh)

#!/usr/bin/expect
set password "here_is_your_root_password"
spawn sudo -i
send "$password\r"
interact

Note :  command reference
 
3. execute expect script

expect auto_login_root.sh