分類
Refactoring

簡化巢狀條件式的技巧(How to fix nested condition)

巢狀條件式通常為為複雜度高的區域。
以下紀錄簡化巢狀條件式的技巧


1.巢狀 if 轉換成 if then else + and。
先從最簡單的開始,e.g.

    private String nestedCondition1()
    {
        String result = "";
        if (isProcess) {
            if (isDone) {
                return result = result.concat("process is truedone is true");
            }
        }
        return result;
    }

目標是第4和第5行巢狀if,我們使用 if + and,化解巢狀if。

    private String fixByIfThenElse1()
    {
        String result = "";
        if (isProcess && isDone) {
            return result = result.concat("process is truedone is true");
        }
        return result;
    }

2.變化:原式 if 和 if 之間有statement。e.g.第5行新增 result = result.concat(“process is true”);

    private String nestedCondition2()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                return result = result.concat("done is true");
            }
        }
        return result;
    }

對於第5行有個轉換的小技巧。在2個 if 之間的 statement 可以轉換為 if(condition1 && ! condition2){statement};
需要注意的是 condition2 必須反轉。e.g.

if(condition1){
    statement1;
    if(condition2){
        statement2;
    }
}
---------trans-----------
if(condition1 && !condition2){
    statement1;
}else if(condition1 && condition2){
    statement1;
    statement2;
}

轉換 if-then-else + and

    private String fixByIfThenElse2()
    {
        String result = "";
        if(isProcess && !isDone){
            result = result.concat("process is true");
        }else if(isProcess && isDone){
            result = result.concat("process is true");
            result = result.concat("done is true");
        }
        return result;
    }

 3.變化: 原式最外層 if 產生相對應的 else。新增 9~11行

    private String nestedCondition3()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                return result = result.concat("done is true");
            }
        } else {
            result = result.concat("process is not true");
        }
        return result;
    }

轉換 if-then-else + and,新增8~10行

    private String fixByIfThenElse3()
    {
        String result = "";
        if(isProcess && !isDone){
            result = result.concat("process is true");
        }else if(isProcess && isDone){
            result = result.concat("process is true");
            result = result.concat("done is true");
        }else if(!isProcess){
            result = result.concat("process is not true");
        }
        return result;
    }

 4.變化: 原式內層 if 產生相對應 else 。新增 8~10行

    private String nestedCondition4()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                return result = result.concat("done is true");
            } else {
                return result = result.concat("done is not true");
            }
        } else {
            result = result.concat("process is not true");
        }
        return result;
    }

轉換 if-then-else + and

    private String fixAndNestedConditionSimple()
    {
        String result = "";
        if (isProcess && isDone) {
            result = result.concat("process is true");
            return result = result.concat("done is true");
        } else if (isProcess && !isDone) {
            result = result.concat("process is true");
            return result = result.concat("done is not true");
        } else if (!isProcess) {
            result = result.concat("process is not true");
        }
        return result;
    }

5.變化:外層 if 的 else 加入其他動作,12~17行

    private String nestedCondition5()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                result = result.concat("done is true");
            } else {
                result = result.concat("done is not true");
            }
        } else {
            result = result.concat("process is not true");
            if (isPrint) {
                result = result.concat("print is true");
            } else {
                result = result.concat("print is not true");
            }
        }
        return result;
    }

轉換對應的 if-then-else + and  為

private String fixByNestedCondition5()
    {
        String result = "";
        if (isProcess && isDone) {
            return result = result.concat("process is truedone is true");
        } else if (isProcess && !isDone) {
            return result = result.concat("process is truedone is not true");
        } else if (!isProcess && isPrint) {
            result = result.concat("process is not true");
            return result = result.concat("print is true");
        } else if (!isProcess && !isPrint) {
            result = result.concat("process is not true");
            return result = result.concat("print is not true");
        }
        return result;
    }

總結:
照這種排列邏輯來看基本上所有的巢狀 if 都可以轉換 if then else + and的結構,差別只在於複雜度與可讀性的不同。


 
2.以衛述句取代巢狀條件式。
這個方法是從”重構:改善既有程式的設計”一書來的。
衛述句是指條件式中若有特別的判斷邏輯,必須明確的標示出來並立刻從函式中返回。e.g.

    private String nestedCondition1()
    {
        String result = "";
        if (isProcess) {
            if (isDone) {
                return result = result.concat("process is truedone is true");
            }
        }
        return result;
    }

以衛述句改善為

    private String fixByGuardClause1()
    {
        String result = "";
        if (!isProcess) {
            return result;
        }
        if (isDone) {
            return result = result.concat("process is truedone is true");
        }
        return result;
    }

2.變化:原式 if 和 if 之間有statement。e.g.第5行新增 result = result.concat(“process is true”);

    private String fixByGuardClauseOrigin()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
        }else{
            if (isDone) {
                result = result.concat("done is true");
            }else{
                result = result.concat("done is not true");
            }
        }
        return result;
    }

以衛述句改善有2種形式,分別為是否在判斷邏輯加入 and 運算。
加入and運算後可以看到其第1種方式較為簡潔(不需要摻雜result = result.concat(“process is true”)),可讀性較高。
1.在判斷邏輯加入 and 。

    private String fixByGuardClauseWithAnd2()
    {
        String result = "";
        if(isProcess && !isDone){
            return result = result.concat("process is true");
        }
        if(isProcess && isDone){
            result = result.concat("process is true");
            return result= result.concat("done is true");
        }
        return result;
    }

2.不加入 and。

    private String fixByGuardClause2()
    {
        String result = "";
        if(!isProcess){
            return result;
        }
        result = result.concat("process is true");
        if(isDone){
            return result = result.concat("done is true");
        }
        return result;
    }

3.變化型: 原式最外層 if 產生相對應的 else。新增 9~11行

    private String nestedCondition3()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                return result = result.concat("done is true");
            }
        } else {
            result = result.concat("process is not true");
        }
        return result;
    }

以衛述句改善為

    private String fixByGuardClause3WithAnd()
    {
        String result = "";
        if(!isProcess){
            return result.concat("process is not true");
        }
        if(isProcess && !isDone){
            return result.concat("process is true");
        }
        if(isProcess && isDone){
            result = result.concat("process is true");
            return result.concat("done is true");
        }
        return result;
    }

4.變化: 原式內層 if 產生相對應 else 。新增 8~10行 

    private String nestedCondition4()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                return result = result.concat("done is true");
            } else {
                return result = result.concat("done is not true");
            }
        } else {
            result = result.concat("process is not true");
        }
        return result;
    }

以衛述句改善為

    private String fixByGuardClause4WithAnd()
    {
        String result = "";
        if(isProcess && !isDone){
            result = result.concat("process is true");
            return result = result.concat("done is not true");
        }
        if(isProcess && isDone){
            result = result.concat("process is true");
            return result = result.concat("done is true");
        }
        if(!isProcess){
            return result = result.concat("process is not true");
        }
        return result;
    }

5.變化:外層 if 的 else 加入其他動作,新增12~17行

    private String nestedCondition5()
    {
        String result = "";
        if (isProcess) {
            result = result.concat("process is true");
            if (isDone) {
                result = result.concat("done is true");
            } else {
                result = result.concat("done is not true");
            }
        } else {
            result = result.concat("process is not true");
            if (isPrint) {
                result = result.concat("print is true");
            } else {
                result = result.concat("print is not true");
            }
        }
        return result;
    }

以衛述句改善為

    private String fixByGuardClause5WithAnd()
    {
        String result = "";
        if (!isProcess && !isPrint) {
            result = result.concat("process is not true");
            result = result.concat("print is not true");
            return result;
        }
        if (!isProcess && isPrint) {
            result = result.concat("process is not true");
            result = result.concat("print is true");
            return result;
        }
        if (isProcess && isDone) {
            result = result.concat("process is true");
            result = result.concat("done is true");
            return result;
        }
        if (isProcess && !isDone) {
            result = result.concat("process is true");
            result = result.concat("done is not true");
            return result;
        }
        return result;
    }

無論使用衛述句或是if-then-else都可將難以理解的巢狀結構分解成簡單的表達式。
 
 
 
 
 

分類
Refactoring

重構專案紀錄(ConfigClient)

ConfigClient 為 Alljoyn 的一個小專案,由於規模不大適合拿來作重構介紹。
重構過程會放到 github 方便觀察其內容。
首先 ConfigClient 有三個 class,各為 MainActivity,ConfigApplication , ConfigActivity。
我們先從 MainActivity 開始。
在開始重構之前先為 MainActivity 建立 UnitTest Project , 確保重構沒有破壞原本的功能。
建議使用 wizard 來建立 Android Test Project(ATP) , 預設的 ATP 會和被測專案分開建立避免弄亂專案結構。
完成 ATP 的建立後,必須新增 UnitTest class e.g.

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private MainActivity mMainActivity;
    private BroadcastReceiver mBroadcastReceiver;
    private Button mAJConnectBtn;
    private TextView mCurrentNetworkView;
    private ArrayAdapter<Device> mDeviceAdapter;
    public MainActivityTest() {
        super(MainActivity.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mMainActivity = getActivity();
        initLayoutComponents();
    }
    private void initLayoutComponents()
    {
        mAJConnectBtn = (Button) mMainActivity.findViewById(R.id.AllJoynConnect);
        mCurrentNetworkView = (TextView) mMainActivity.findViewById(R.id.current_network_name);
    }
    public void testMainActivityNotNull()
    {
        assertNotNull(mMainActivity);
    }
}

由於還沒開始重構,先測試 MainActivity 不為空值,其他的測項會在重構過程中一一加入。


 
開始重構!!!
首先來觀察 MainActivity 的欄位,e.g.

public class MainActivity extends Activity implements OnCreateContextMenuListener {
    protected static final String TAG = ConfigApplication.TAG;
    private BroadcastReceiver m_receiver;
    private Button m_AJConnect;
    private TextView m_currentNetwork;
    ArrayAdapter<Device> deviceAdapter;
...

第3行的 TAG 用來印出訊息,目的通常為 Debug 用。但一般來說 TAG 的值都為該類別名稱,以了解該類別內的流程順序。
因此將它改為MainActivity,把存取權限改為 private 。
觀察其他的欄位發現命名規則並沒有遵循一定的規則,這裡導入 android 的命名規則,參考這裡 android code style
因此修改以下欄位名稱
m_receiver -> mReceiver
m_AJConnect -> mAJConnectBtn
m_CurrentNetwork -> mCurrentNetWorkView
deviceAdapter 的存取範圍只在MainActivity 內,因此順便修改存取權限為 private
deviceAdapter -> mDeviceAdapter
完成後如下

public class MainActivity extends Activity implements OnCreateContextMenuListener {
    private static final String TAG = "MainActivity";
    private BroadcastReceiver mReceiver;
    private Button mAJConnectBtn;
    private TextView mCurrentNetworkView;
    private ArrayAdapter<Device> mDeviceAdapter;
...

執行測試!!
螢幕擷圖存為 2016-02-19 17:47:46
通過測試。
接著觀察 onCreate() 函式,它太長了,很明顯出現 “Long Method(過長函式)” 的壞味道。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        ConfigApplication application = (ConfigApplication) getApplication();
        // ************** AllJoyn Connect/Disconnect Button ******************
        mAJConnectBtn = (Button) findViewById(R.id.AllJoynConnect);
        mAJConnectBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynConnect))) {
                    allJoynConnect();
                } else if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynDisconnect))) {
                    allJoynDisconnect();
                }
            }
        });
        // ***************** Current Network *********************
        mCurrentNetworkView = (TextView) findViewById(R.id.current_network_name);
        String ssid = application.getCurrentSSID();
        mCurrentNetworkView.setText(getString(R.string.current_network, ssid));
        // ************** Announced names list ******************
        ListView listView = (ListView) findViewById(R.id.device_list);
        mDeviceAdapter = new ArrayAdapter<Device>(this, android.R.layout.simple_list_item_1);
        listView.setAdapter(mDeviceAdapter);
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Device selectedDevice = mDeviceAdapter.getItem(position);
                Intent intent = new Intent(MainActivity.this, ConfigActivity.class);
                intent.putExtra(ConfigApplication.EXTRA_DEVICE_ID, selectedDevice.appId);
                startActivity(intent);
            }
        });
        // ************** Class receiver ******************
        mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (ConfigApplication.ACTION_DEVICE_FOUND.equals(intent.getAction()) || ConfigApplication.ACTION_DEVICE_LOST.equals(intent.getAction())) {
                    mDeviceAdapter.clear();
                    HashMap<UUID, Device> deviceList = ((ConfigApplication) getApplication()).getDeviceList();
                    if (deviceList == null) {
                        return;
                    }
                    addAllDevices(deviceList.values());
                }
                else if (ConfigApplication.ACTION_CONNECTED_TO_NETWORK.equals(intent.getAction())) {
                    String ssid = intent.getStringExtra(ConfigApplication.EXTRA_NETWORK_SSID);
                    mCurrentNetworkView.setText(getString(R.string.current_network, ssid));
                }
            }
        };
        IntentFilter filter = new IntentFilter();
        filter.addAction(ConfigApplication.ACTION_DEVICE_FOUND);
        filter.addAction(ConfigApplication.ACTION_DEVICE_LOST);
        filter.addAction(ConfigApplication.ACTION_CONNECTED_TO_NETWORK);
        registerReceiver(mReceiver, filter);
    }

使用 ”Extract Method(提取函式)” 來重構它。
提取哪段呢?? 可以看到其內使用了註釋來說明部份區塊的功能,事實上這些註釋都可以使用具有描述性名稱的函式來取代。
因此我們先提取第9~21行,並命名為 initAJConnectBtn() 函式。

    private void initAJConnectBtn()
    {
        mAJConnectBtn = (Button) findViewById(R.id.AllJoynConnect);
        mAJConnectBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynConnect))) {
                    allJoynConnect();
                } else if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynDisconnect))) {
                    allJoynDisconnect();
                }
            }
        });
    }

刪除註釋並使用函式取代原區塊。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        ConfigApplication application = (ConfigApplication) getApplication();
        initAJConnectBtn();
        ...

接著準備提取第24~26行,在提取之前先來看看可能發生的問題。
在第26行需要 ssid 參數傳入,因此如果直接提取第24~26行就必須建立參數並傳入 e.g.

    private void initCurrentNetwork(ConfigApplication application)
    {
        mCurrentNetworkView = (TextView) findViewById(R.id.current_network_name);
        String ssid = application.getCurrentSSID();
        mCurrentNetworkView.setText(getString(R.string.current_network, ssid));
    }

但真的需要 application 參數嗎??
先回到未提取前的狀態來看,第25行的 ssid 是藉由 application.getCurrentSSID() 來取得。
而 application 則是在第6行的 (ConfigApplication) getApplication() 而來。
但在第6行的 application 區域變數除了供給第25行取得 ssid 外,沒有再做其他的事情。
因此可以使用 “Inline temp (內連化暫時變數)” 處理 ssid e.g.

    private void initCurrentNetwork()
    {
        mCurrentNetworkView = (TextView) findViewById(R.id.current_network_name);
        mCurrentNetworkView.setText(getString(R.string.current_network,
                ((ConfigApplication) getApplication()).getCurrentSSID()));
    }

在原本onCreate的第6行 ConfigApplication application = (ConfigApplication) getApplication(); 也可以刪除了
接著我們提取第29~42行並命名為 initAnnouncedNames() 函式 e.g.,

private void initAnnouncedNames()
    {
        ListView listView = (ListView) findViewById(R.id.device_list);
        mDeviceAdapter = new ArrayAdapter<Device>(this, android.R.layout.simple_list_item_1);
        listView.setAdapter(mDeviceAdapter);
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id)
            {
                Device selectedDevice = mDeviceAdapter.getItem(position);
                Intent intent = new Intent(MainActivity.this, ConfigActivity.class);
                intent.putExtra(ConfigApplication.EXTRA_DEVICE_ID, selectedDevice.appId);
                startActivity(intent);
            }
        });
    }

最後提取45~72行,分為2個函式,首先45~67行為初始化動作而68~72則是註冊的動作。
提取45~67為 initDeviceDetector 函式,而68~72為 registerDeviceDetector 函式,e.g.

    private void initDeviceDetector()
    {
        mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (ConfigApplication.ACTION_DEVICE_FOUND.equals(intent.getAction()) || ConfigApplication.ACTION_DEVICE_LOST.equals(intent.getAction())) {
                    mDeviceAdapter.clear();
                    HashMap<UUID, Device> deviceList = ((ConfigApplication) getApplication()).getDeviceList();
                    if (deviceList == null) {
                        return;
                    }
                    addAllDevices(deviceList.values());
                }
                else if (ConfigApplication.ACTION_CONNECTED_TO_NETWORK.equals(intent.getAction())) {
                    String ssid = intent.getStringExtra(ConfigApplication.EXTRA_NETWORK_SSID);
                    mCurrentNetworkView.setText(getString(R.string.current_network, ssid));
                }
            }
        };
    }
    private void registerDeviceDetector()
    {
        IntentFilter filter = new IntentFilter();
        filter.addAction(ConfigApplication.ACTION_DEVICE_FOUND);
        filter.addAction(ConfigApplication.ACTION_DEVICE_LOST);
        filter.addAction(ConfigApplication.ACTION_CONNECTED_TO_NETWORK);
        registerReceiver(mReceiver, filter);
    }

因為本次的重構改動到 Layout Component,因此需要在 Unit Test 加入關於 Layout Component 的測項。e.g.

    public void testAJConnectBtnStringCorrect()
    {
        assertEquals(getActivity().getString(R.string.AllJoynConnect), mAJConnectBtn.getText().toString());
    }
    public void testCurrentNetWorkViewStringCorrect()
    {
        String expect =getActivity().getString(R.string.current_network,
                ((ConfigApplication) getActivity().getApplication()).getCurrentSSID());
        assertEquals(expect, mCurrentNetworkView.getText().toString());
    }

啟動測試!!
螢幕擷圖存為 2016-02-22 14:24:01
完成這次重構後,onCreate() 函式內容的註解將被具有命名的函式取代掉,且其內容的函式呼叫會維持在相同的抽象層次, e.g.,

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        initAJConnectBtn();
        initCurrentNetwork();
        initAnnouncedNames();
        initDeviceDetector();
        registerDeviceDetector();
    }

 
 
 
 
 

分類
Refactoring

Null Object Pattern

當代碼中過度出現需要檢查 null (空值) 的情況,可以考慮使用 Null Object Pattern 取代相關的判斷式。

 
Account 為代表帳號的類別, e.g.

public class Account {
    private String mName;
    public Account(String name) {
        mName = name;
    }
    public String getName() {
        return mName;
    }
}

 
在客戶端需要常常檢查 Account 是否為空值才能進行下一步動作。e.g.

Account account = bank.getAccount();
if(account != null)
{
     //use account do something
}

 
重複檢查是否為 null,也是一種 Duplicated Code 的壞味道。
首先建立 NullAccount 來代表 Account 的 Null Object e.g.

public Account {
...
    private class NullAccount extends Account {
        private NullAccount(String name) {
            super(name);
        }
        @Override
        public String getName() {
            return "No name , This is NullAccount";
        }
    }
...
}

這裡使用了內部類別,是因為 NullAccount 為 Account 的一種型態,當然也可以轉換成獨立的 public 類別。
第11行修改了原有的方法,只要是在Account中所有的功能都必須被覆寫,以區別 NullAccount。
接著在 Account 加入讓外部取得 NullAccount 的方法 e.g.

public class Account {
...
    public static Account getNullAccount() {
            return new Account(null).new NullAccount(null);
    }
...
}

 
最後完整的 Account 如下

public class Account {
    private String mName;
    public Account(String name) {
        mName = name;
    }
    public String getName() {
        return mName;
    }
    public static Account getNullAccount() {
        return new Account(null).new NullAccount(null);
    }
    private class NullAccount extends Account {
        private NullAccount(String name) {
            super(name);
        }
        @Override
        public String getName() {
            return "No name , This is NullAccount";
        }
    }
}

 
修改 “所有” 會回傳 Account 的位置讓其返回 NullAccount,注意是所有會返回 Account 位置都必須修改!!
如原本 Bank 的 getAccount() 為

public class Bank {
...
    public Account getAccount()
    {
        return mAccount;
    }
...
}

改為

public class Bank {
...
    public Account getAccount()
    {
        return (mAccount == null) ? Account.getNullAccount() : mAccount;
    }
...
}

 
再也不需要檢查從 Bank 取的 Account 是否為 null 了

Account account = bank.getAccount();
String name = account.getName();

 
Note :
1. 使用 Null Object Pattern 只有在大量檢查空值的情況下才有用,若外部檢查的位置不多,使用這個 Pattern 的實質效益不大。
2. 以上為 Null Object Pattern 的基本型態,事實上 Null Object 本身可以修改為 Singleton Pattern
為了避免產生重複的實體,讓外部存取單一實體即可。
Singleton Null Object Pattern
修改 NullAccount e.g.

    private static class NullAccount extends Account {
        private static NullAccount sUniqueInstance = new NullAccount(null);
        private NullAccount(String name) {
            super(name);
        }
        public static NullAccount getInstance() {
            return sUniqueInstance;
        }
        @Override
        public String getName() {
            return "No name , This is NullAccount";
        }
    }

 
修改 Account 對外的介面 e.g.

public Account {
...
    public static Account getNullAccount() {
            return NullAccount.getInstance();
    }
...
}

如此產生的 Null Object 實體都是相同的。
 
3. Null Object Pattern 其實為 Special Case Pattern 的其中一種。
Special Case Pattern 請參考 patterns of enterprise application architecture(企業應用架構模式)。
 

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part4) : Refactor class(MainActivity.java)

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
完成 FP 類別的重構後,只剩下 MainActivity,開始修改它吧

Remove useless comment

首先把註解的程式碼移除,移除前 MainActivity 的總行數為 558 行,移除後MainActivity的總行數只剩 282 行。
為了確保沒有移除到不應該移除的部份,先跑 UnitTest 看看,結果如下。

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.222
     [exec]
     [exec] OK (2 tests)
...
...
BUILD SUCCESSFUL

成功通過測試,把本次的修改 merge 到遠端 Server 吧。
也許會覺得這次的修改部份不多,可以一併重構其他部份再merge,但使用版本控制就是盡量保持小部份但穩定的merge。
好處是萬一日後真的出現 bug,尋找 commit 可以切割的比較乾淨。
接著來看看變數的命名和存取權限部份。
 

Rename variable

重構前

...
public class MainActivity extends Activity
{
    static Camera c;
    static Parameters p;
    public LayoutParams lp;
    public PowerManager powerm;
    public PowerManager.WakeLock WL;
    public static int width;
    public static int height;
    public int moveX;
    public int backLightIndex;
    public int moveY;
    public int frontLightIndex;
    AdView adview;
    int testnum;
    boolean isCameraUse;
...

從 static Camera c 開始,單一字母的變數命名方式很難看出該變數代表的意義,把 c 改為 mCamera。
變數名稱前綴加上m,代表該變數為類別成員。
看看 c 的存取範圍,c 只有被 MainActivity 內部存取,因此可以縮減存取範圍為 private。
相同的方式來處理 static Parameters p , 將 p 改為 private Parameters mParameters;
其中 AdView adview , int testnum ,boolean isCameraUse 這3個變數並沒有被引用,因此把他們刪除掉。
重構完,如下

...
public class MainActivity extends Activity
{
    private Camera mCamera;
    private Parameters mParameters;
    private LayoutParams mLayoutParams;
    private PowerManager mPowerManager;
    private PowerManager.WakeLock mWakeLock;
    private int mScreenWidth;
    private int mScreenHeight;
    private int mXCoordinatesOfScreen;
    private int mYCorrdinatesOfScreen;
    private int mBackLightIndex;
    private int mFrontLightIndex;
...

執行 UnitTest 看看測試結果。

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.INSTRUMENTATION_RESULT: shortMsg=java.lang.RuntimeException
     [exec] INSTRUMENTATION_RESULT: longMsg=java.lang.RuntimeException: Fail to connect to camera service
     [exec] INSTRUMENTATION_CODE: 0
BUILD FAILED

發生 build failed , 觀察訊息看起來問題出在 RuntimeException : Fail to connect to camera service。
google 錯誤訊息 ,參考 http://stackoverflow.com/questions/23904459/android-java-lang-runtimeexception-fail-to-connect-to-camera-service
原因為當 Camera 使用後必須在 onPause()中 呼叫 release() 把資源釋放掉,最好是在 onResume()中 呼叫 open ()。
參考 http://developer.android.com/intl/zh-tw/reference/android/hardware/Camera.html#getNumberOfCameras()
了解原因之後就來修改 bug 吧,接下來先考慮甚麼時候要開啟 Camera , 根據 activity 的生命週期,
當 user 啟動 activity 或是暫停 activty 或是開啟另一個activity後,
除了 destroy 該 activity 之外,onResume() 一定都會被呼叫到,所以我們可以統一將初始化的動作放在 onResume()並針對不同狀態作處理。
加入 2 個方法如下,首先 initCamera 會初始化 mCamera 物件並設定其閃燈模式,第 12 行的 try catch 為了避免有些裝置沒有後置鏡頭其回傳值為 null 的情況。

    @Override
    protected void onResume()
    {
        super.onResume();
        initCamera();
        detBackLight();
        detFrontLight();
    }
    private void initCamera()
    {
        try {
            mCamera = Camera.open();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
        if (mCamera != null) {
            mParameters = mCamera.getParameters();
            mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            mCamera.setParameters(mParameters);
        }
    }

接著考慮甚麼時候需要把 camera release,雖然 android developer 建議在 onPause() 中 release , 但因為我們希望 camera 被移動到背景時也能正常工作。
因此 release 的時間點就在於 onDestroy() 方法中。
加入 onDestroy()

    @Override
    protected void onDestroy()
    {
        super.onPause();
        mCamera.release();
        mCamera = null;
    }

完成後,執行 UnitTest,結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.095
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

修正bug之後,針對方法來重構吧,首先從 showAdmob() 開始,
重構 showAdmob() 前,先來考慮 SRP 原則,只考慮 showAdmob 和 MainActivity 2者即可,
showAdmob() 主要有4個步驟
1.建立adView 物件
2.建立廣告layout
3.廣告layout加入adView物件
4.adview物件讀取廣告
如下

    private void showAdmob()
    {
        AdView adView = new AdView(this, AdSize.BANNER, "a150c81d1f5acca");
        LinearLayout layout = (LinearLayout) findViewById(R.id.AdLayout);
        layout.addView(adView);
        adView.loadAd(new AdRequest());
    }

 
照目前的設計,當 showAdmob()發生變化時,一定需要改動MainActivity,比如說 “a150c81d1f5acca” 需要改為別的號碼,或者想把 layout 改為別的顯示畫面。
這種高耦合的設計是違反 SRP 的。確實,顯示廣告是 MainActivity 的工作,但這不一定需要由 MainActivity 來完成。
我們可以建立另一個 AdmobManager,把顯示廣告的工作內容交給它,讓 MainActivity 簡單的呼叫 AdmobManager 的方法即可。
除了減低耦合以外,若日後有其他Activity 需要顯示廣告,只要呼叫 AdmobManager 即可,不必複製貼上同樣的內容,也可減少重複的程式碼。
Extract Class
建立 AdmobManager 並把 showAdmob() 搬移進來,AdmobManager 如下

package com.twoflashlight.utility;
import android.app.Activity;
import android.widget.LinearLayout;
import com.google.ads.AdRequest;
import com.google.ads.AdSize;
import com.google.ads.AdView;
import com.tylerfoxx.twoflash.R;
public class AdmobManager
{
    private static final String AD_UNIT_ID = "a150c81d1f5acca";
    public static void showAdmob(Activity activity)
    {
        AdView adView = new AdView(activity, AdSize.BANNER, AD_UNIT_ID);
        LinearLayout layout = (LinearLayout) activity.findViewById(R.id.AdLayout);
        layout.addView(adView);
        adView.loadAd(new AdRequest());
    }
}

在 MainActivity 的 init() 改為呼叫 AdmobManager.showAdmob(),如下

void init()
{
    ...
    AdmobManager.showAdmob(this);
    ...
}

完成後,執行UnitTest,結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.563
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

成功通過測試。
接著重構 init() ,首先修改存取權限為 private ,接著把方法重構為以下內容,其中 ScreenWakeLockManager 為延續 AdmobManager 的作法,
把 WakeLock 的實作內容提取到 ScreenWakeLockManager 。

    private void init()
    {
        countBackLightIndex();
        countFrontLightIndex();
        AdmobManager.showAdmob(this);
        ScreenWakeLockManager.makeScreenWakeLock(this);
        setScreenBrightness(0.1f);
    }
    private void countFrontLightIndex()
    {
        mScreenHeight = (getWindowManager().getDefaultDisplay().getHeight());
        mYCorrdinatesOfScreen = mScreenHeight / 2;
        mFrontLightIndex = 5;
    }
    private void countBackLightIndex()
    {
        mScreenWidth = (getWindowManager().getDefaultDisplay().getWidth());
        mXCoordinatesOfScreen = mScreenWidth / 2;
        mBackLightIndex = 1;
    }
    private void setScreenBrightness(float brightness)
    {
        mLayoutParams = getWindow().getAttributes();
        mLayoutParams.screenBrightness = brightness;
        getWindow().setAttributes(mLayoutParams);
    }

ScreenWakeLockManager 如下

package com.twoflashlight.utility;
import android.app.Activity;
import android.content.Context;
import android.os.PowerManager;
public class ScreenWakeLockManager
{
    private static final String DEBUG_TRACE_IDENTIFY = "ScreenWakeLockManager";
    public static void makeScreenWakeLock(Activity activity){
        PowerManager powerManager = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
        powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, DEBUG_TRACE_IDENTIFY).acquire();
    }
}

完成重構後,跑 UnitTest ,測試結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 3.595
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

成功通過。
接著是public void finishApp(), 重構後如下

    private void showExitDialog()
    {
        Builder dialog = new AlertDialog.Builder(MainActivity.this);
        dialog.setTitle("Warning");
        dialog.setMessage("Exit Application??");
        dialog.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
                nmShow();
                MainActivity.this.finish();
            }
        });
        dialog.setNegativeButton("No", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
            }
        });
        dialog.show();
    }

Run UnitTest!! e.g.

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 1.727
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

Success!!
接著是 void nmShow() , 重構後如下

private void showNotificationToTitleBar()
    {
        Intent intent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);
        Notification notification = new Notification();
        notification.tickerText = "TwoFlashLight!!!";
        notification.defaults = Notification.DEFAULT_ALL;
        notification.setLatestEventInfo(MainActivity.this, "TwoFlashLight", "Welcome to use!!!", pendingIntent);
        NotificationManager notificatoinManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificatoinManager.notify(0, notification);
    }

run UnitTest

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 3.88
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

Success!!
接著是 public boolean onTouchEvent()
重構後,改變為3個方法,改變為3個方法的原因為增加可讀性以及保持方法的抽象層次。

    public boolean onTouchEvent(MotionEvent event)
    {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                adjustLight(event);
                break;
        }
        return true;
    }
    private void adjustLight(MotionEvent event)
    {
        getCoordinatesOnScreen(event);
        detBackLight();
        detFrontLight();
    }
    private void getCoordinatesOnScreen(MotionEvent event)
    {
        mXCoordinatesOfScreen = (int) event.getX();
        mYCorrdinatesOfScreen = (int) event.getY();
    }

run UnitTest

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 2.689
     [exec]
     [exec] OK (2 tests)
...
BUILD SUCCESSFUL

重構public boolean onKeyDone()
重構變動的行數不多,主要是把 magic number 改為可讀性高的 KeyEvent.KEYCODE_BACK

    @Override
    public boolean onKeyDown(int keycode, KeyEvent event)
    {
        super.onKeyDown(keycode, event);
        TraceLogger.print("keycode:" + keycode);
        switch (keycode) {
            case KeyEvent.KEYCODE_BACK:
                showExitDialog();
                return true;
        }
        return false;
    }

由於變動到按鍵功能,必須增加按下 back key 的測項到 UnitTest 中。
但這時候出現另一個問題,要如何測試 dialog?
按下 back key 會呼叫 showExitDialog() 進而顯示 dialog , 但我們必須按下 dialog 上的按鈕(Yes / No),才有辦法確認功能是否正常。
但因為 dialog 是私有方法的區域變數,除非修改 MainActivity 讓 dialog 可以存取,否則無法在外部取得該 dialog。
這裡推薦一個好用的開源測試工具,Robotium。它把複雜測試指令包裝起來方便讓人呼叫,以本次的測試為例。
當按下 back key 之後,出現 dialog,可以輸入有關按鈕的文字,讓Robotium自動去尋找符合的元件,如下
第3行送出 back key 指令以顯示dialog
第4行尋找含有 “Warning” 的元件(dialog)
第5行點擊含有 “No” 的元件(No button)
即可測試按下 no 的功能是否正常。

    private void testExitDialogFunction()
    {
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText("Warning");
        mMainActivity.clickLongOnText("No");
        ...
    }

最後修改 UnitTest 如下,也加入模擬改變亮度測項(testChangeLightFunction)。

package com.twoflashlight.test;
import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent;
import com.robotium.solo.Solo;
import com.twoflashlight.main.MainActivity;
public class UnitTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private static final int MOVE_STEP_ON_SCREEN = 20;
    private Solo mMainActivity;
    public UnitTest() {
        super(MainActivity.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        mMainActivity = new Solo(getInstrumentation(), getActivity());
    }
    private void testChangeLightFunction()
    {
        testChangeBackLightToMin();
        testChangeFrontLightToMax();
        testChangeBackLightToMax();
        testChangeFrontLightToMin();
    }
    private void testChangeBackLightToMax()
    {
        float startX = 0;
        float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth();
        float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testChangeBackLightToMin()
    {
        float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth();
        float endX = 0;
        float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testChangeFrontLightToMax()
    {
        float startY = 0;
        float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight();
        float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testChangeFrontLightToMin()
    {
        float endY = 0;
        float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getHeight();
        float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getWidth() / 2;
        mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN);
    }
    private void testExitDialogFunction()
    {
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText("Warning");
        mMainActivity.clickLongOnText("No");
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText("Warning");
        mMainActivity.clickLongOnText("Yes");
    }
    public void testMainActivityFunction()
    {
        testChangeLightFunction();
        testExitDialogFunction();
        assertNotNull(mMainActivity);
    }
}

完成後執行 UnitTest,

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 16.019
     [exec]
     [exec] OK (1 test)
...
BUILD SUCCESSFUL

接著看看 UnitTest 的覆蓋率。

[EMMA v2.0.5312 report, generated Thu Oct 22 13:31:18 CST 2015]
-------------------------------------------------------------------------------
OVERALL COVERAGE SUMMARY:
[class, %]	[method, %]	[block, %]	[line, %]	[name]
100% (6/6)	86%  (24/28)	95%  (476/500)	91%  (112/123)	all classes
OVERALL STATS SUMMARY:
total packages:	2
total classes:	6
total methods:	28
total executable files:	4
total executable lines:	123
COVERAGE BREAKDOWN BY PACKAGE:
[class, %]	[method, %]	[block, %]	[line, %]	[name]
100% (3/3)	50%  (3/6)!	80%  (37/46)	77%  (10/13)!	com.twoflashlight.utility
100% (3/3)	95%  (21/22)	97%  (439/454)	93%  (102/110)	com.twoflashlight.main
-------------------------------------------------------------------------------

class 的覆蓋率 100% ,方法覆蓋率 86%,方法的覆蓋率包含類別建構式,在 com.twoflashlight.utility 內的 class 都是僅只有 static method , 不必產生其類別實體即可呼叫。
接著只剩下 detBackLight 和 detFrontLight 這2個方法等待重構。
準備重構 detBackLight 和 detFrontLight,首先更改2個方法的名稱為 changeBackLight 和 changeFrontLight 比較適合方法的意圖。
接著根據方法內容提取出不同功能的部份以 changeFrontLight 為例,
在方法內部其實可以再分為2個部份為
1.計算 front light 索引
2.設定螢幕亮度
於是我們再提取出這兩個方法如下。

    private void changeFrontLight()
    {
        calculateFrontLightIndex();
        setScreenBrightness();
    }

2個方法內容為

    private void calculateFrontLightIndex()
    {
        ArrayList<Integer> screenHeightQuarterIndex = new ArrayList<Integer>();
        int screenHeightQuarterMax = 10;
        for (int i = 0; i < screenHeightQuarterMax; i++) {
            screenHeightQuarterIndex.add(mScreenHeight * i / screenHeightQuarterMax);
        }
        for (int i = 0; i < screenHeightQuarterIndex.size() - 1; i++) {
            if (mYCorrdinatesOfScreen >= screenHeightQuarterIndex.get(i)
                    && mYCorrdinatesOfScreen <= screenHeightQuarterIndex.get(i + 1)) {
                mFrontLightIndex = i;
            }
        }
        mLayoutParams.screenBrightness = (float) mFrontLightIndex / screenHeightQuarterMax;
    }
    private void setScreenBrightness()
    {
        float screenBrightnessMin = 0.1f;
        if (mLayoutParams.screenBrightness <= screenBrightnessMin) {
            mLayoutParams.screenBrightness = screenBrightnessMin;
        }
        getWindow().setAttributes(mLayoutParams);
    }

接著使用同樣的方式重構 changeBackLight()
重構 changeBackLight 完成如下

    private void changeBackLight()
    {
        calculateBackLightIndex();
        setBackLightFlashMode();
    }
    private void setBackLightFlashMode()
    {
        if (mParameters != null) {
            if (mBackLightIndex == BackLightIndex.OFF.getIndex()) {
                mParameters.setFlashMode(Parameters.FLASH_MODE_OFF);
            } else if (mBackLightIndex == BackLightIndex.MACRO.getIndex()) {
                mParameters.setFocusMode("macro");
                mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            } else if (mBackLightIndex == BackLightIndex.AUTO.getIndex()) {
                mParameters.setFocusMode("auto");
                mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            }
            try {
                mCamera.setParameters(mParameters);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    private void calculateBackLightIndex()
    {
        ArrayList<Integer> screenWidthQuarterIndex = new ArrayList<Integer>();
        int screenWidthQuarterMax = 4;
        for (int i = 0; i < screenWidthQuarterMax; ++i) {
            screenWidthQuarterIndex.add(mScreenWidth * i / screenWidthQuarterMax - 1);
        }
        for (int i = 0; i < screenWidthQuarterIndex.size() - 1; ++i) {
            if (mXCoordinatesOfScreen >= screenWidthQuarterIndex.get(i)
                    && mXCoordinatesOfScreen <= screenWidthQuarterIndex.get(i + 1)) {
                mBackLightIndex = i;
            }
        }
    }

完成後一樣需要 run UnitTest

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 15.171
     [exec]
     [exec] OK (1 test)
...
BUILD SUCCESSFUL

接著進行本地化資源的動作,如果將 UI 上的顯示文字寫死在 java code 中,就無法針對不同的國家顯示不同的文字。
因此我們必須對資源進行本地化。android 系統預設會到 res/values/strings.xml 中尋找字串資源,預設的strings.xml如下

<resources>
    <string name="app_name">TwoFlashLight</string>
    <string name="menu_settings">Settings</string>
    <string name="title_activity_main">Two Flash Light</string>
    <string name="operate_hint">Drag around the screen , adjust the brightness of the back\n\n
	    Drag up or down the screen , adjust the front luminance\n\nPress the Home key for background use\n\n
	    please press the back key to close app</string>
    <string name="exit_dialog_title">Warning!!</string>
    <string name="exit_dialog_message">Exit Application??</string>
    <string name="exit_dialog_yes">Yes</string>
    <string name="exit_dialog_no">No</string>
    <string name="notification_text">TwoFlashLight!!!</string>
    <string name="notification_welcomeUse">Welcome to Use!!!</string>
</resources>

 
若需要針對不同國家進行資源的設定必須在 res/ 資料夾下建立相對應的資料夾,比如我們想針對繁體字串資源的設定就建立
res/values-zh-rTW 資料夾,並在資料夾中新建 strings.xml 如下,

<resources>
    <string name="app_name">雙面手電筒</string>
    <string name="menu_settings">設定</string>
    <string name="title_activity_main">雙面手電筒</string>
    <string name="operate_hint">左右拖曳畫面,調整背面亮度\n\n上下拖曳畫面,調整正面亮度\n\n背景使用請按Home鍵\n\n關閉程式請按返回鍵</string>
    <string name="exit_dialog_title">警告!!</string>
    <string name="exit_dialog_message">離開應用程式??</string>
    <string name="exit_dialog_yes">是</string>
    <string name="exit_dialog_no">否</string>
    <string name="notification_text">雙面手電筒!!!</string>
    <string name="notification_welcomeUse">歡迎使用!!!</string>
</resources>

完成後也必須修改 UnitTest,只要修改 testExitDialogFunction()即可

    private void testExitDialogFunction()
    {
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText(mMainActivity.getString(R.string.exit_dialog_title));
        mMainActivity.clickLongOnText(mMainActivity.getString(R.string.exit_dialog_no));
        mMainActivity.sendKey(KeyEvent.KEYCODE_BACK);
        mMainActivity.searchText(mMainActivity.getString(R.string.exit_dialog_title));
        mMainActivity.clickLongOnText(mMainActivity.getString(R.string.exit_dialog_yes));
    }

因為已經針對不同的語系設定不同的資源,所以測項也不可寫死在 java code。
Run UnitTest!!

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 15.668
     [exec]
     [exec] OK (1 test)
...
BUILD SUCCESSFUL

 
 

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part3) : Refactor class(FP.java)

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
去除掉沒有使用的類別之後,我們把目標轉向 FP.java
首先觀察類別名稱,FP 沒有傳達出任何有關這個類別的意義,於是再看看 FP 的內容。
發現有個 TLog 的常數以及 p 的靜態方法,先從 TLog 開始,
追蹤 TLog 的引用位置,發現只有在 p 的方法中有被引用,所以修改其存取範圍為 private , 看起來它似乎只是被拿來當識別用。
於是修改 TLog 的名稱為 LOG_IDENTIFY。
接著 p 方法,其引用位置在 MainActivity 的 detBlackLight , detFrontLight , onKeyDown的三個方法中。
它接受一個參數並呼叫 Log.v 並把其參數內容列印出來,看起來是當作列印訊息用。因此可以把 p 修改為 print 。
最後看起來 FP 的用途就是追蹤並列印訊息,所以把 FP 改為 TraceLogger 。
重構前

package com.twoflashlight.utility;
import android.util.Log;
public class FP {
	public static final String TLog = "Trace Log";
	public static void p(String msg){
		Log.v(TLog, msg);
	}
}

重構後

package com.twoflashlight.utility;
import android.util.Log;
public class TraceLogger {
	private static final String LOG_IDENTIFY = "Trace Log";
	public static void print(String msg) {
		Log.v(LOG_IDENTIFY, msg);
	}
}

重構應用的技術相當簡單, 主要是rename class,rename variable,rename method,雖然簡單卻可以提昇整個類別的可讀性。
因為重構改變 FP 的內部功能,所以我們必須增加新的測項到 UnitTest。
在 UnitTest.java 加入新的測項如下:

public void testTraceLoggerPrint()
{
    TraceLogger.print("Test message from UnitTest");
}

由於 TraceLogger 的 print 方法沒有回傳值,也只能對其方法進行呼叫測試。
在 local 端執行 UnitTest 。執行結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:..
     [exec] Test results for InstrumentationTestRunner=..
     [exec] Time: 1.623
     [exec]
     [exec] OK (2 tests)
...

由於這次加入 testTraceLoggerprint() 測項,因此在 [exec] OK 有2個 test 通過。
完成 local 端的測試,後即可 merge 到遠端 Server。
 
refactoring part4

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part2) : Remove unuse class

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
Remove unuse class
專案中的類別都需要維護的成本,沒有被引用的類別除了會增加維護的時間和人力以外,也很容易會造成 bug 的源頭,因為你不知道到底需不需要他們。
不過現在有了 version control ,你不需要擔心日後找不到他們。所以放心刪除吧。
尋找 unuse class的方法有很多,當類別不多時可以考慮一個一個找,當然也有一堆套件方便工作。
首先你可以藉由 ide 幫你找,使用 eclipse 在類別的名稱上按下 ctrl + alt + h , 或是點擊滑鼠右鍵選擇 open call hierarchy,都會幫你列出該類別被引用的位置。
但是類別中如果有 static 方法,因為 static 方法不需要建立類別實體就可呼叫。因此也需要對 static 方法實施上述的工作。
另一個快速的方式可以直接刪除或改名該類別,看看其他類別是否有出現error的狀況。
重構步驟為:
首先對 com.twoflashlight.utility 內的類別進行測試,除了 FP 這個類別以外其他的類別都沒有被引用,只留下 FP 並刪除其他類別。
接著對 com.twoflashlight.main 進行測試,除了 MainActivity 這個類別以外其他的類別也都沒有被引用,刪除其他類別。
刪除掉沒有使用的類別後,TwoFlashLight 只剩下2個類別。
最後在 local 端執行 UnitTest,結果如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 7.231
     [exec]
     [exec] OK (1 test)
...

通過 local 端 UnitTest 後,即可 merge 到遠端 Server 上。
 
refactoring part3

分類
Refactoring

重構專案紀錄(TwoFlashLight)(part1) : Rename Package

(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
先從程式碼的層面一步步開始重構吧,首先是 package 和 class 的名稱沒有符合 coding standard。
格式良好的 coding standard 會讓 code 看起來更簡潔更有規範,相對提高了可讀性。
 

Rename package

觀察 com/tylerfoxx/twoflash 中的 class , 其中 MainActivity 會被當作起始點啟動,所以先暫時把 package name 改為含有主要(main)的名稱。
之後重構分離 class 可以再建立其他的package 放入 class 也沒關係。將 package name 從 com/tylerfoxx/twoflash 改為 com/twoflashlight/main
觀察 FoXxLib 中的 class,發現 class 負責的工作似乎都不相關,看起來並沒有一定的關聯性,所以先改名為 utility 名稱。
將 package name 從 FoXxLib 改為 com/twoflashlight/utility。
完成修改後可先在 local 端啟動 UnitTest,避免讓本次重構產生其他的bug,確定 UnitTest 過關之後再 git merge 到遠端 Server (github)。
本地端啟動UnitTest的步驟為:
Step 1. 開啟終端機到TwoFlashLight/Two_FlashLight/test
Step 2. 確認已經有裝置連線後輸入 ant clean emma debug install test
Step 3. 最後在終端機會顯示這次的UnitTest狀態,如下

test:
[getlibpath] Library dependencies:
[getlibpath] No Libraries
     [echo] Running tests...
     [echo] Running tests ...
     [exec]
     [exec] com.twoflashlight.test.UnitTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 7.372
     [exec]
     [exec] OK (1 test)
     [exec]
     [exec]
...

結果代表本次的 UnitTest 成功通過。UnitTest 的類別位於 src/com/twoflashlight/test/UnitTest.java。
測試內容相當簡單,啟動MainActivity並測試其不為空值(null)。
當然,若重構的過程有修改到其他部份,也必須增加到 UnitTest 才行。
local 端測試 UnitTest 並成功通過後就可 git push 到遠端 Server上,再到 github 手動 merge 本次修改,每次完成 merge,jenkins 都會自動 build project。
除了即時通知本次的 merge 會不會造成 build fail , 也可以使用套件分析程式碼以了解專案的狀態。

分類
Refactoring

重構專案紀錄(TwoFlashLight)

Two Flash Light 是我在多年前寫的小型Android project,當初寫的時間很趕加上也沒什麼設計模式,原則和重構等等的概念。
因此無論是從具體的程式碼實作或是高層的抽象設計都是一塌糊塗。(不過它仍然活在google play上好好的)。
由於種種的缺陷因此把它拿來當作重構的目標,似乎再適合也不過。
接下來重構的過程都會一步一步紀錄在這裡,也會將它開源到github上。(https://github.com/testfoxx/TwoFlashLight.git)
確定重構前已經建立好一切的準備。包含version control tool(git) , CI Server(Jenkins)等等。
git clone 之後會是一個TwoFlashLight的資料夾,source code 的部份放在 Two_FlashLight 資料夾中。
相關的測試類別放在 TwoFlashLight/Two_FlashLight/test。
要說明的是重構前必須建立測試環境,以避免重構失敗所造成的影響。
使用工具為 android 的 InstrumentationTestRunner 搭配 jenkins emma plugin
emma plugin 可以讓你了解目前的測試覆蓋率有多少。每一次 merge code 都會更新覆蓋率。
 
OK~ 那麼就開始嘗試重構吧。
refactoring part1
refactoring part2
refactoring part3
refactoring part4
 
 
 
 
 

分類
Refactoring

Example: Replace Parameter with explicit methods(使用明確的方法取代參數)

當長if-else或是switch出現在方法內時通常代表該方法做了一件以上的事。

 
為了保持方法簡短和清晰度,適當的情況下,我們應該盡量避免長if-else或switch的出現。
如果switch或是if-else的判斷條件(參數)是可以掌控的,那麼可以使用 Replace Parameter with explicit methods 來消除 switch 或是 if-else,
 
toggleUI方法相當簡單,根據state的值判斷進入哪個條件。e.g.,

    private void toggleUI(UIState state, int labelTextId)
    {
        if (state == UIState.LOAD_PANEL) {
            progressBar.setVisibility(View.VISIBLE);
            languageSpinner.setVisibility(View.GONE);
        } else if (state == UIState.LANGUAGE_FOUND) {
            progressBar.setVisibility(View.GONE);
            languageSpinner.setVisibility(View.VISIBLE);
        }
        label.setText(labelTextId);
    }

 
觀察所有toggleUI的呼叫點會發現每個呼叫點傳入的參數都是可以控制的。e.g.,

    private void setUI()
    {
        ...
        toggleUI(UIState.LOAD_PANEL, R.string.label_loading);
    }
    void onReadyPanelDialog(AlertDialog dialog)
    {
         ...
         toggleUI(UIState.LANGUAGE_FOUND, R.string.label_language);
         ...
    }

 
因此我們就能夠把 toggleUI 拆開,分為2個方法 ,e.g.,

    private void toggleUIForLoadPanel(int labelTextId){
        progressBar.setVisibility(View.VISIBLE);
        languageSpinner.setVisibility(View.GONE);
        label.setText(labelTextId);
    }
    private void toggleUIForLanguageFound(int labelTextId){
        progressBar.setVisibility(View.GONE);
        languageSpinner.setVisibility(View.VISIBLE);
        label.setText(labelTextId);
    }

 
建立toggleUIForLoadPanel,toggleUIForLanguageFound 後取代的原本 toggleUI 呼叫點,

    private void setUI()
    {
        ...
        toggleUIForLoadPanel(R.string.label_loading);
    }
    void onReadyPanelDialog(AlertDialog dialog)
    {
         ...
         toggleUIForLanguageFound(R.string.label_language);
         ...
    }

 
再觀察 toggleUIForLoadPanel 和 toggleUIForLanguageFound 的唯一參數(int labelTextId)
發現該參數傳入的數值會跟著方法一致,
toggleUIForLoadPanel 傳入的是 R.string.label_loading
toggleUIForLanguageFound 傳入的是 R.string.label_language
因此可把參數直接刪除,在方法中直接指定即可,e.g.,

    private void toggleUIForLoadPanel(){
        progressBar.setVisibility(View.VISIBLE);
        languageSpinner.setVisibility(View.GONE);
        label.setText(R.string.label_loading);
    }
    private void toggleUIForLanguageFound(){
        progressBar.setVisibility(View.GONE);
        languageSpinner.setVisibility(View.VISIBLE);
        label.setText(R.string.label_language);
    }

 
最後刪除掉 toggleUI 方法。
把原本需要2個參數的toggleUI方法重構為2個不需要參數的方法,也讓方法的命名轉為更容易了解( toggleUIForLanguageFound , toggleUIForLOadPanel )
在重構結束後還會發現一些額外的驚喜,原本的 toggleUI 使用 UIState 列舉,而該列舉只使用在 toggleUI 內,隨著 toggleUI 被刪除,UIState 也不需要使用了。
 
 

分類
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 函式清晰度提高了不少,被提煉出來的方法也增加了重用性。