分類
Coding Principle

計算函式複雜度(by decision points – Tom McCabe)

使用 Tom McCabe 的 decision points(決策點)計算函式複雜度。
Step 1.從函式開頭以 1 開始計算。
Step 2.遇到if , while , repeat , for , and , or 類似關鍵字,加 1。
note : switch 每一個case 都要加1。
e.g.

    private void exampleCountDecisionPoints()
    {
        if((isProcess && isDone) || !isLimit) && numberLines > numberLinesLimit){
        }
    }

if +1
&& + 1
|| + 1
&& +1
total decision points = 5


 
複雜度對應原則表

0 ~ 5  -> 複雜度低可以接受
6 ~ 10 -> 複雜度中考慮簡化
10up    -> 複雜度高必須簡化

 
 

分類
Coding Principle

布林表達式(Boolean Expression)

布林表示式:
1.不要用數字(0,1)來表示布林邏輯,改用boolean。
2.使用隱式表達。e.g.

if(isDone == true) -> if(isDone)

3.可用解釋性布林變數(重構:引入解釋性變數)增加可讀性。
4.把複雜的表達式改為為布林函式(重構:分解條件式)。
5.盡量在if-else中把判斷條件轉為肯定並互換 if else的代碼。
6.使用狄摩根簡化否定的表達式。
邏輯互換表

not A and not B → not(A or B)
not A and B → not(A or not B)
A and not B → not(not A or B)
A and B → not(not A or not B)
not A or not B → not( A and B)
not A or B → not(A and not B)
A or not B → not(not A and B)
A or B → not(not A and not B)

 
 

分類
Coding Principle

撰寫迴圈的原則(loop principle)

Java 迴圈表達的方式共有 while,do-while,for,for-each。
以下列出撰寫迴圈需要注意的原則。
1.迴圈的種類可以分為以下幾種:

計次循環:循環次數固定。

連續求值循環:預先不知道循環次數,會在每次循環檢查中止條件。

無限循環:不會停止的循環。

迭代器循環:對容器每個元素進行操作。


 
2.通用性:

1.迴圈內使用的變數相關初始化動作盡量靠近迴圈前方。

2.使用while(true) or for(;;) 代表無限循環。

3.在適當的情況多使用for 迴圈,for好處在於所有的判斷條件都寫在頂端。

4.使用{}包括循環體。

5.避免空循環。

6.把控制循環操作(i++)放在循環的開始或是循環的結束而不是放在中間。

7.避免依賴循環結束後再對循環下標取值的動作。

8.對於容易出錯的迴圈考慮使用安全計數器。

9.了解continue用途。

10.小心巢狀區塊中break的退出範圍。

11.小心有很多break的循環。

12.不要使用浮點數來代表循環變量。

13.在巢狀循環使用有意義的下標名取代i e.g. i → xxxIndex

14.迴圈深度不要超過2層。


 
2.while迴圈:

1.只知道中止條件但預先不知道循環要多少次結束就使用while迴圈。

2.while迴圈依照中止條件可分為3種。

1.在開頭檢查中止條件為while迴圈,e.g.

while(condition){
//do action
}

2.在結尾檢查中止條件為do-while迴圈,e.g.

do{
//do action
}
while(condition)

3.在中間檢查結束條件為while迴圈但中止條件在中間,使用中間檢查結束條件注意兩點,

1.檢查條件集中同一個位置。

2.考慮使用註解說明退出條件。e.g.

while(true){
    //do action
    if(condition){
        break;
    }
    //do action
}

3.while迴圈適合複雜的情況。


 
3.for迴圈:

1.如果需要次數固定的循環就使用for迴圈。

2.如果在迴圈中必須存在結束條件就盡量改用while迴圈。

3.不要在for迴圈中修改下標值使其終止,如果需要修改下標值,改用while。

4.for迴圈適合簡單的情況。


4. for-each迴圈:

1.適合用於數組或容器的各項元素操作。

2.優點為消除循環控制索引出錯的可能性。


 
 

分類
Coding Principle

撰寫條件式的原則(condition principle)

Java 條件式主要有 if-else 以及 switch 2種,撰寫條件式有一些原則可以參考。
 
1.通用性原則:

    1.條件式的判斷邏輯順序可以依照 “正常流程 or 重要性 > 執行頻率 > 字母或數字順序” 來排列。

e.g. 判斷英文字母或是數字,因為2者都是屬於正常流程,重要性也相同,但英文字母的執行頻率比數字高,

因此應該把英文字母的判斷放在上方。

private void isNumberOrLetter(char source)
{
        if (source == 'a' || source == 'b' || source == 'c' ...) {
        } else if (source == '1' || source == '2' ...) {
        }
}

    2.switch用於簡單的條件判斷,if-else用於複雜的條件判斷。

    3.若執行區塊的複雜度高,可以使用 “重構:提取函式” 來提取複雜度高的區塊。

    4.if-then-else 最後一個 else 和 switch 的 default 必須注意不包含的條件,可以使用 assert 或 throw new illegalArgumentException 來處理。

    5.為了提高可讀性,判斷邏輯的寫法可以依照數軸來寫。e.g.

    判斷 i 是否在 min 和 max之間 → min <= i && i <= max

if(0 <= i && i <= 10)

判斷 i是否在 min 和 max 之外 → i < min or i > max

if(i < 0 || i >10)

判斷 i 應該小於或大於某個值 → i < min  or  i > max 。 ref to: The art of readable code

if(i < 0)
if(i > 10)

 


2. If-else:

    1.不要寫空的if,可以反轉if判斷條件並把原本對應else的動作放到if中。e.g.

if(condition){
}else{
//do else action
}

改為

if(!condition){
//do else action
}

    2.判斷條件多且複雜時,可使用 “重構分解條件式”。e.g.

if(color == green && color == red){
}else if(color == red && color == black){
}else if(color == black && color == green){
}

改為

if(isGreenAndRed(color)){
}else if(isRedAndBlack(color)){
}else if(isBlackAndGreen(color)){
}

    3.分解巢狀條件式,可以使用 “重構-以衛述句取代巢狀條件式” 。


3. Switch:
    1.不要摻雜多個case,每個case都應該使用break跳出。

分類
Android Uncategorized

查表法(表驅動法)(Table-Driven-Methods )

查表法(Table-Driven-Method)主要是藉由表格的維度和對應的數值來替代冗長的判斷式(if-else or switch)
查表法根據不同的查表方式,可分為三種:
直接存取(direct access):可從表格中直接讀取數值。
索引存取(indexed access):先透過索引取出key值,再從key值取出數值。
階梯存取(stair-step access):對於不同的數值範圍有效。
 


 
直接存取:
如果要查詢月份對應的天數,最直接的寫法 e.g.,

    private int getDaysOfMonth(int month)
    {
        if (month == 1) {
            return 31;
        } else if (month == 2) {
            return 28;
        } else if (month == 3) {
            return 31;
        } else if (month == 4) {
            return 30;
        } else if (month == 5) {
            return 31;
        } else if (month == 6) {
            return 30;
        } else if (month == 7) {
            return 31;
        } else if (month == 8) {
            return 31;
        } else if (month == 9) {
            return 30;
        } else if (month == 10) {
            return 31;
        } else if (month == 11) {
            return 30;
        } else if (month == 12) {
            return 31;
        } else {
            throw new IllegalArgumentException();
        }
    }

可以藉由以下表格來取代

private static final int[] DAYS_OF_MONTH = {
            31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

取值如下,第7行為使用查表法的動作。

    @Test
    public void testGetDayOfMonth()
    {
        int[] months = {1,2,3,4,5,6,7,8,9,10,11,12};
        for(int month : months){
            int resultBySwitch = getDaysOfMonth(month);
            int resultByTable = DAYS_OF_MONTH[month - 1];
            assertEquals(resultBySwitch, resultByTable);
        }
    }

 


 
索引存取:
如果條件判斷式的變數型態不是int , 就無法使用陣列來建表,必須改用Map,而 Map 的 key 值即是索引值。e.g.

    private int getDayOfMonthByName(String nameOfMonth)
    {
        if (nameOfMonth.equals("Jan")) {
            return 31;
        } else if (nameOfMonth.equals("Feb")) {
            return 28;
        } else if (nameOfMonth.equals("Mar")) {
            return 31;
        } else if (nameOfMonth.equals("Apr")) {
            return 30;
        } else if (nameOfMonth.equals("May")) {
            return 31;
        } else if (nameOfMonth.equals("Jun")) {
            return 30;
        } else if (nameOfMonth.equals("Jul")) {
            return 31;
        } else if (nameOfMonth.equals("Aug")) {
            return 31;
        } else if (nameOfMonth.equals("Sep")) {
            return 30;
        } else if (nameOfMonth.equals("Oct")) {
            return 31;
        } else if (nameOfMonth.equals("Nov")) {
            return 30;
        } else if (nameOfMonth.equals("Dec")) {
            return 31;
        } else {
            throw new IllegalArgumentException();
        }
    }

使用Map取值,第1到10行為初始化Map , 第14行為使用Map取值。

        Map<String, Integer> DAY_OF_MONTH = new HashMap<String,Integer>();
        String[] nameOfMonth = new String[] {
                "Jan", "Feb", "Mar", "Apr", "May","Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
        };
        Integer[] numberDayOfMonth = {
                31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        };
        for (int i = 0; i < nameOfMonth.length; ++i) {
            DAY_OF_MONTH.put(nameOfMonth[i], numberDayOfMonth[i]);
        }
        for(String name : nameOfMonth){
            int resultByMethod = getDayOfMonthByName(name);
            int resultByMap = DAY_OF_MONTH.get(name);
            assertEquals(resultByMethod, resultByMap);
        }

 


 
階梯存取:
假設現在要對不同分數給予等級的判斷, e.g.

    private String getNameOfGradeByMethod(int score)
    {
        if (score >= 90) {
            return "A";
        } else if (score >= 80 && score < 90) {
            return "B";
        } else if (score >= 70 && score < 80) {
            return "C";
        } else if (score >= 60 && score < 70) {
            return "D";
        } else if (score < 60) {
            return "F";
        } else {
            throw new IllegalArgumentException();
        }
    }

需要建立2個對應的table , 以及求值的函式(getNameOfGradeByTable)。 e.g.

    private static final String NAME_OF_GRADE_LEVEL[] = {
            "A", "B", "C", "D", "F"
    };
    private static final int NUMBER_OF_GRADE_LEVEL[] = {
            90, 80, 70, 60
    };
    private String getNameOfGradeByTable(int score)
    {
        int gradeLevel = 0;
        while (NAME_OF_GRADE_LEVEL[gradeLevel] != NAME_OF_GRADE_LEVEL[NAME_OF_GRADE_LEVEL.length - 1]) {
            if (score < NUMBER_OF_GRADE_LEVEL[gradeLevel]) {
                ++gradeLevel;
            } else {
                break;
            }
        }
        return NAME_OF_GRADE_LEVEL[gradeLevel];
    }

雖然整體看起來比判斷式來的長,但若是接下來需要增加更多的判斷如 60, 50, 40, 30, 20, 10。
對於查表法只要在 table 加入相對應數值即可。
也適合用於沒有規則變化的求值。


 
事實上查表法無法去除掉原判斷式的邏輯,它只是把邏輯搬移到表中。表格的複雜度會跟著原判斷式的邏輯成正比。
另外也需要提供空間儲存表格。
查表法可以減少程式碼的長度,但無法簡化程式碼。
對可讀性的幫助並不大,反而需要完全理解表格才能修改原功能或是增加新功能。
因此最適合的情況為冗長但判斷邏輯簡單的條件式。
更複雜的查表法範例 ref
 

分類
Check Style

使用 Apache Maven Checkstyle Plugin 產生 checkstyle report

Check Style 是一個相當好用的程式碼檢測工具,主要用來 code review 是否有不符合code standard 的部份。
但它並沒有產生整體報告的功能,因此當面對大量檔案需要 code review,還是必須花上不少時間來整理。
Apache Maven Checkstyle Plugin(amcp) 剛好可以補足其缺少報告的部份。
以下介紹如何安裝以及產生報告
首先安裝 Maven (由於 amcp 的要求,Maven 版本必須是第3版,參考這裡)

sudo apt-get install maven

接著下載該套件

wget http://apache.stu.edu.tw/maven/plugins/maven-checkstyle-plugin-2.17-source-release.zip

解壓縮

7z x maven-checkstyle-plugin-2.17-source-release.zip

移動到plugin目錄下並安裝,安裝過程中會自動下載相關套件。

cd maven-checkstyle-plugin-2.17
mvn install

完成後就可以開始使用了。
首先建立測試資料夾

mkdir test

移動到測試資料夾中並建立src資料夾,src資料夾存放要檢查的source code

cd test
mkdir src

在 src 中建立一個簡單的 SimpleObject.java

package com.foxx.char6_7;
public class SimpleObject
{
    private int mId;
    private String mName;
    public SimpleObject(int id, String name)
    {
        mId = id;
        mName = name;
    }
    public int getId()
    {
        return mId;
    }
    public void setId(int id)
    {
        mId = id;
    }
    public String getName()
    {
        return mName;
    }
    public void setName(String name)
    {
        mName = name;
    }
}

接著移動到test資料夾並建立pom.xml

cd ../
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>foxx.test</groupId>
  <artifactId>example_for_generate_checkstyle_report</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- custome checkstyle rules location
    <checkstyle.config.location>/full/path/of/your/checkstyle/rule/file</checkstyle.config.location> -->
  </properties>
<build>
  <sourceDirectory>src</sourceDirectory>
</build>
<reporting>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-checkstyle-plugin</artifactId>
				<version>2.17</version>
			</plugin>
		</plugins>
	</reporting>
</project>

pom.xml 有幾點需要注意。
第10~11行指定 checkstyle 的 rule file , 如果沒有指定預設為使用sun checkstyle rule。
第16行指定檢查的資料夾,該資料夾下所有的.java都會被檢查。
第19~27行指定產生報告用的plugin , 即為amcp。
建立完 pom.xml 之後,就可以執行amcp。
移動 test 資料夾中並輸入

mvn clean checkstyle:check

clean 為清除之前建立的檔案(主要是target資料夾),而 checkstyle:check 為執行checkstyle的檢查並產生報告
完成後在 test 資料夾中會產生 target 資料夾,而報告則是 checkstyle-result.xml 檔案, e.g.

<checkstyle version="6.11.2"><file name="/home/foxxtseng/temp/maven_and_checkstyle/test/src/SimpleObject.java"><error line="0" severity="error" message="Missing package-info.java file." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck"/><error line="3" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck"/><error line="4" column="1" severity="error" message="'{' at column 1 should be on the previous line." source="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck"/><error line="5" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocVariableCheck"/><error line="6" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocVariableCheck"/><error line="7" severity="error" message="Line has trailing spaces." source="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck"/><error line="8" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck"/><error line="8" column="25" severity="error" message="Parameter id should be final." source="com.puppycrawl.tools.checkstyle.checks.FinalParametersCheck"/><error line="8" column="33" severity="error" message="Parameter name should be final." source="com.puppycrawl.tools.checkstyle.checks.FinalParametersCheck"/><error line="9" column="5" severity="error" message="'{' at column 5 should be on the previous line." source="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck"/><error line="14" column="5" severity="error" message="Method 'getId' is not designed for extension - needs to be abstract, final or empty." source="com.puppycrawl.tools.checkstyle.checks.design.DesignForExtensionCheck"/><error line="14" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck"/><error line="15" column="5" severity="error" message="'{' at column 5 should be on the previous line." source="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck"/><error line="19" column="5" severity="error" message="Method 'setId' is not designed for extension - needs to be abstract, final or empty." source="com.puppycrawl.tools.checkstyle.checks.design.DesignForExtensionCheck"/><error line="19" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck"/><error line="19" column="23" severity="error" message="Parameter id should be final." source="com.puppycrawl.tools.checkstyle.checks.FinalParametersCheck"/><error line="20" column="5" severity="error" message="'{' at column 5 should be on the previous line." source="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck"/><error line="24" column="5" severity="error" message="Method 'getName' is not designed for extension - needs to be abstract, final or empty." source="com.puppycrawl.tools.checkstyle.checks.design.DesignForExtensionCheck"/><error line="24" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck"/><error line="25" column="5" severity="error" message="'{' at column 5 should be on the previous line." source="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck"/><error line="29" column="5" severity="error" message="Method 'setName' is not designed for extension - needs to be abstract, final or empty." source="com.puppycrawl.tools.checkstyle.checks.design.DesignForExtensionCheck"/><error line="29" column="5" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck"/><error line="29" column="25" severity="error" message="Parameter name should be final." source="com.puppycrawl.tools.checkstyle.checks.FinalParametersCheck"/><error line="30" column="5" severity="error" message="'{' at column 5 should be on the previous line." source="com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck"/><error line="33" severity="error" message="Line has trailing spaces." source="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck"/><error line="34" severity="error" message="Line has trailing spaces." source="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck"/></file></checkstyle>

預設報告的格式並不容易查看,換個指令。

mvn clean site

site 指令會另外產生一個html格式的報告,報告位於 target/site/checkstyle.html  。
html格式的報告相當容易查看。
螢幕擷圖存為 2016-03-16 10:07:41
使用 amcp 最大的好處在於需要 code review 的時候,先把需要check的檔案checkout下來,再 run amcp ,
會自動檢查所有的檔案並產生報告,不用一個一個抓violation, 抓到天昏地暗了。
 

分類
Android

Android Error : Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser;

Description:

引入外部 Library , 啟動 android app 時出現。

[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$1;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$2;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$3;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$ServiceTableModel;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Main;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Main$SampleListener;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Responder;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Ljavax/jmdns/package-info;
[2016-03-04 10:20:58 - Dex Loader] Unable to execute dex: Too many errors
[2016-03-04 10:20:58 - SmackForXep0174] Conversion to Dalvik format failed: Unable to execute dex: Too many errors

即使把所有變動都移除,讓專案回復未引用 Library 再重新 build,錯誤訊息一樣產生。
必須把 eclipse 重新啟動,錯誤訊息才會消失。
 

Solution:

從錯誤訊息去尋找相關線索。
一連串的錯誤訊息提示都和 jmdns有關,在app中 也引用了 jmdns Library。
StackOverFlow 提示 ref: http://stackoverflow.com/questions/11697979/android-build-with-jmdns-fails
看起來是SourceForge 上檔案有問題,根據 StackOverFlow 提示改換 maven 上的 Library
app 成功啟動!!

分類
Ubuntu tools

Install Gradle for Ubuntu12.04, 14.04, 14.10

For Ubuntu 14.10, Ubuntu 14.04, Ubuntu 12.04

$ sudo add-apt-repository ppa:cwchien/gradle
$ sudo apt-get update
$ sudo apt-get install gradle
remove gradle
$ sudo apt-get remove gradle

 
 
 

分類
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();
    }

 
 
 
 
 

分類
Java

檢查方法參數的有效性

撰寫方法時容易忽略”非正常流程”的處理,特別是在趕工的時候。
對於檢查方法參數的有效性也有一套準則,從 Effective Java 整理出來。
Rule:
1.對於方法或是建構式的參數限制應該寫在註解中,並在方法的開頭檢查參數。
2.public 方法或是建構式可以使用 throw new xxxException 並在方法宣告加上註解@throws 說明違反參數規則會出現的異常。
3.private 方法可以使用 assert 斷言來限制, assert 必須開啟才有效。
(For eclipse → right click class in package explorer → run as → run configuration → arguments → VM arguments → type -ea)
4.不需要作檢查的情況為檢查成本太大,或是檢查會隱含在計算過程中(即使做了檢查也沒多大意義)。


 
jdk 中 File 類別的建構式,第7行檢查參數的有效性不可為null。

    /**
    ...
     * @throws  NullPointerException
     *          If the pathname argument is null
     */
    public File(String pathname) {
        if (pathname == null) {
            throw new NullPointerException();
        }
        this.path = fs.normalize(pathname);
        this.prefixLength = fs.prefixLength(this.path);
    }

另一個私有建構式也做了參數的檢查(第7,8行)

    /**
     * Internal constructor for already-normalized pathname strings.
     * The parameter order is used to disambiguate this method from the
     * public(File, String) constructor.
     */
    private File(String child, File parent) {
        assert parent.path != null;
        assert (!parent.path.equals(""));
        this.path = fs.resolve(parent.path, child);
        this.prefixLength = parent.prefixLength;
    }

 
 
至於第4點的後段”檢查會隱含在計算過程中” e.g.

    private File createFile(String filePath)
    {
        assert filePath != null;
        File result = new File(filePath);
        return result;
    }

第3行使用了 assert 來檢查傳入的參數是否為 null , 其實就是不需要的檢查。若是傳入 null,在第4行會丟出NullPointerException,
而不需要在方法開頭檢查。