分類
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,
而不需要在方法開頭檢查。
 
 
 

分類
Java

自定義異常(customize exception)

自定義異常目的
為了表示應用程序的一些特定錯誤訊息並提供新的含意。
如考慮伺服器的連線問題雖然可以全部使用 IOException 來表示,但各種連線問題卻可再度細分,如
伺服器關閉(ServiceClosedException) , 連線超時(ConnectTimeoutException),使用者中斷(ClientInterruptException)等等。
 
Note :
自定義異常必須在Throwable 體系中派生,否則不能在應用程序中傳播。
不可直接繼承Throwable,因為Throwable已經定義好2個主要分支(Error & Exception)
也不要繼承自Error 及其子類別,因為自定義通常不符合錯誤標準。
一般繼承自Exception


 
 
自定義異常步驟
1.定義異常類別
需要繼承Throwable 或是Throwable的子類別,並提供多個建構式,最好能夠傳入String 以便儲存錯誤訊息。

public class ServiceClosedException extends Exception
{
    private Date mConnectDate = new Date();
    public ServiceClosedException() {
    }
    public ServiceClosedException(String message) {
        super(message);
    }
    public ServiceClosedException(String message, Date connectDate){
        super(message);
        mConnectDate = connectDate;
    }
    public Date getConnectDate(){
        return mConnectDate;
    }
}

 
2.聲明方法拋出自定義類別

public void connectService() throws ServiceClosedException
{
}

 
3.找到故障點,建立異常並加上關鍵字 throw

public void connectService() throws ServiceClosedException
{
        if(!mIsServiceConnect){
            throw new ServiceClosedException();
        }
}

 
4.自定義異常完成後,client呼叫會丟出自定義異常的方法就必須處理或是聲明該自定義異常

    @Test
    public void testConnectService()
    {
        try {
            connectService();
        } catch (ServiceClosedException e) {
            e.printStackTrace();
        }
    }

 
 

分類
Java

處理異常的選項(handle or declare)

1.處理(handle) : 直接捕獲異常並處理。

處理異常動作:使用異常處理區塊(try-catch-finally)

  1. try:區塊內放置可能拋出異常語句。
  2. catch:區塊內處理異常的動作,動作步驟為產生異常後,try區塊停止動作,JVM尋找相關的catch區塊。可能會有多個catch區塊,異常會交給第一個符合的catch 區塊處理。
  3. finally:無論try catch 是否會執行,finally區塊一定會執行

處理異常規則:
try之後必須接著catch 或是 finallycatchfinally可以一起出現,但必須至少有一個。
在多個catch 區塊的情況下,經驗法則為從最具體排到最一般。
選擇處理的優點:在本地端直接處理,容易找到發生問題點,方法調用者不須多寫處理。
選擇處理的缺點:缺少通用性,無法選擇其他的處理方式,因為必須多寫處理的代碼因此增加該方法的複雜度。


 
 

2.聲明(declare) : 聲明該方法拋出異常,讓呼叫端接手處理。

聲明異常動作:在方法宣告中加上 throws ExceptionName
聲明異常的規則:

  1. 必須聲明方法中可能拋出的可檢測異常(checked exception),非檢測異常(unchecked exception)不是必須的,可聲明也可以不聲明。
  2. 聲明異常類別可以是其異常子類。

選擇聲明原因:

  1. 需要傳送異常發送到系統其他部份。
  2. 無法在本地端處理異常。

 
 

3.選擇處理或是聲明異常

經驗法則:盡可能處理異常,若無法處理才聲明異常。
 


 
 

4.標準異常處理選項

1.紀錄異常相關訊息

  • 使用標準輸出或錯誤流
  • 開發自定義類
  • 使用java 紀錄api (java.util.logging)

2.要求客戶端輸入訊息
透過GUIclient 選擇下一步
3.使用預設值或替換數據

  • 簡單情況將預設值設定為靜態常量加在程式碼中。
  • 複雜情況可將預設值紀錄在配置文件(Properties)中,並在發生問題時載入。

4.將控制轉移到應用程序的其他部份

  • 簡單情況提取catch 處理方法
    try {
          result = bufferReader.readLine();
        } catch (IOException e) {
            handleReadLineException();
        }
    }
    private void handleReadLineException(){
    // do handle exception for read line
    }
  • 複雜情況開發一個異常處理類別,來集中處理應用程序代碼

5.將異常轉換成其他形式

  • 可提供更適當的上下文用來描述異常
private void connect(String ip) throws ServerConnectionException
{
        try {
            //some exception
        } catch (IOException e) {
            throw new ServerConnectionException ("specify message from Server Connect", e);
        }
}

6.準備停止系統

  • 有打開的文件就關閉
  • 有連接的資源就關閉
  • 儲存必要資訊
  • 通知其他部份應用程序即將結束

7.重試操作

  • 等待一段時間後重試操作
try {
            connect();
    } catch (IOException e) {
            retryConnect();
    }
private void retryConnect()
{
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        try{
            connect();
        }catch(IOException e){
            //retry connect
        }
    }

 
8.替換或恢復操作

  • 如無法儲存在伺服器上就改為存在本機硬碟
       try {
            saveToServer();
        } catch (IOException e) {
            saveToLocalDisk();
        }
    private void saveToLocalDisk(){
    }

 
9.忽略問題

  • 當異常對系統其他部份沒有影響,如調用 I/O stream的關閉方法
private FileReader mFileReader;
...
...
try {
      mFileReader.close();
} catch (IOException e)
{
      e.printStackTrace();
}

 


 
 

5.解決異常建議事項

  • 盡可能處理異常:盡可能處理,除非條件不予許再考慮聲明異常。
  • 具體問題具體解決:需要為特定類型的問題建立特定的處理區塊。
  • 紀錄可能影響應用程序運行的異常:可以採取永久的方式紀錄影響應用程序的異常,可以幫助處理問題
  • 根據情況將異常轉換為業務上下文:建立異常的成本不高,可以考慮為特定的問題轉換特定的異常。

 
 

6.解決異常不建議事項

  • 不要忽略異常
try {
      printStream = new PrintStream(new File("./error_record"));
      System.setOut(printStream);
      printStream.println("error message");
     } catch (FileNotFoundException e)
{//do nothing here}

 

  • 不要使用覆蓋式異常處理
    try {
          printStream = new PrintStream(new File("./error_record"));
          System.setOut(printStream);
          printStream.println("error message");
        } catch (Exception e) {
               // do something
        }
  • 不要將特定異常轉為一般異常
    try {
          printStream = new PrintStream(new File("./error_record"));
          System.setOut(printStream);
          printStream.println("error message");
        } catch (IOException e) {
                throw new Exception(exceptionMessage);
        }
  • 不要處理能夠避免的異常(使用判斷式取代拋出異常)
  • 使用
    if(printStream != null){
        printStream.println("error message");
    }
    取代
    try{
         printStream.println("error message");
    }catch(NullPointerException e){
         System.out.println(''PrintStream is null'');
    }