顯示具有 技術文章 標籤的文章。 顯示所有文章
顯示具有 技術文章 標籤的文章。 顯示所有文章

2012年11月11日 星期日

曾遇到的DB問題 (一)


三個跟DB相關的問題,兩個是我還在android時遇到的;一個則是在我開始寫VB6以後看到的。

()
當時是有一個ANR(Application Not Responding)issue,後來根據log發現是因為發生DB lock,結果造成等待DB等到發生了ANR。那為什麼會發生DB lock呢?我記得當時我跟一位同事看code看了很久,看起來好像都沒有問題,直到一直查到源頭,另一個同事寫的存取DBlibrary,終於讓我們發現了一個問題。原本library的設計概念是把存取DB的類別使用singleton模式來實作,但是卻沒有將建構子的存取權限設為private,使得在開發Provider時誤用new來實體化DBHelper


所以當多執行緒操作DB時,就可能發生new兩次實體的狀況。也就是說當A物件先使用DBHelper,然後B物件又使用DBHelper,接著就會發生DB lock。使得B物件無法使用DBHelper,於是他只好等待到天荒地老到發生ANR。發現了這個root cause後,我們便將建構子的存取權限設為private,並把要使用到DBHelper物件的程式碼,改為以getInstance來獲取實體,這個issue也就這樣解了。


()
第二個也是DB的問題,這個問題是在第一次使用該AP時才會發生的issue,而且還必須依照某一特定的步驟才會發生。問題是這樣的,有一個應用程式A,還有一個應用程式A的外掛程式B。當使用者還沒第一次執行過A之前,先使用了B就會發生B無法外掛到A的問題。這個問題很特殊,而且可能跟上面DB lock問題一起發生。我試了很久才終於找到這個issue的重覆產生步驟,因為它只會發生在A從來沒有執行過的情況下,而這個情況通常只會發生在剛刷完新ROM的機子中。

主要的原因是這樣的:
1. 在第一次執行A之前,A所需要用到的資料庫是完全不存在的(我想這應該只會發生在Android)
2. 目前實作的機制是將可以外掛到A的來源資料儲存在DB中。因此,資料庫在創建時必須自動加入A可以接受外掛的來源,否則外掛程式皆無法外掛到A之中。

A從沒被執行過,DB還沒產生出來時。在執行B的時候,B會透過一些機制來使用AProvider,接著A會發現沒有DB存在,然後開始create一個DB。當DB建好後,再透過一些機制將外掛來源寫進DB中。也就是說,這是一個兩步驟的動作,所以當A只做到第一個動作時,B便開始想外掛進A,然後就會出現A發現DB中並沒有B可作為外掛來源的資料,於是拒絕B的外掛,並造成B外掛失敗。這個問題簡單來說,其實是因為多執行緒的時間差造成的問題。最後的解法是,當A確定DB建好且外掛來源也已經寫進資料庫中時,再發送一個訊息給B,讓B再做一次外掛即可解決。

2012年11月4日 星期日

讀松本行弘的程式世界之突發奇想~~

最近看在松本行弘的程式世界這本書,裡面提到一個他大力提倡的繼承方法:mix-in。
書中討論了很多關於單一繼承、多重繼承及mix-in繼承的方法。

單一繼承:單純的繼承關係,不過限制太多有什會產生不直覺的繼承方式。

例如:


因為沒有辦法多重繼承,使得ReadWriteStream只能繼承於其中一個類別。

也因為只能繼承於單一類別,另一個ReadStream中的功能就只能用複製的方式移植到ReadWriteStream中。這樣會使得程式碼重複,而程式碼重複會產生難以維護及擴充的缺點(DRY,Don’t repeat yourself)。

多重繼承:可以同時繼承好幾的類別,就像是一個人可以同時是爸爸,也可以有老公和員工的其他身分。不過可能導致複雜的繼承關係圖及優先順序的問題。

例如:



不同於單一繼承,ReadWriteStream可以直接繼承ReadStream及WriteStream,減少程式碼的重複。

不過ReadStream及WriteStream重複的Stream的功能,可能會讓ReadWriteStream搞不清楚要用哪一個繼承下來的功能。

Mix-in:類似多重繼承,不過在第二個之後的父類別,必需是要滿足下列兩個條件的類別。

1. 不能獨自建立實體的抽象類別
2. 不能繼承自不是mix-in類別的類別
透過mix-in的方法就可以將原本多重繼承的架構簡化,並避免掉鑽石問題。
例如:


其實我重點不是要談繼承這個議題,只是想要把mix-in這種的繼承方式帶出來,因為我接下來是像要對這樣的架構作討論。

根據上面的mix-in的圖,可以發現每個子類別都繼承於Stream這個抽象的類別,在混入Read和Write這兩實作功能的類別(或稱模組)。
這樣的架構,讓我想到設計模式中的橋接模式。

橋接模式:在於將抽象與實現分離,使兩者都可以獨立地演化。這邊所謂的抽象,指的是指應用程式行為定義的演化,而實現指的是應用程式實作時,所需使用的特定API或平台。(出自良葛格筆記)


以ReadWriteStream來看,就會像下面的圖一樣。



透過橋接模式,就可以在原類別用composite的方式混入想要的功能了。


好吧,其實以上都是我自己胡思亂想的~~


那些年,我們一起寫的BUG-Primitive VS. Wrapper Class

人生只有比較,就失去自己的意義了。
                                                                                           - 俠刀 蜀道行


這個bug是實際發生在我身邊的,事情是這樣的:
有一天,我的兩個同事拿著兩支手機在測一個行為,很簡單的行為。就是從資料庫中抓一個整數值,再用現存的值跟資料庫得到的值做比較,如果一樣就顯示結果A;否則就顯示結果B。
但是兩個人卻測出不同的結果,然後又試了好幾支不同的手機後,結果發現偏偏只有那一支有不一樣的結果,真的是非常的奇怪。然而,在經過我同事細心的測試和追查後,終於找到原因,原來那支手機剛剛被我用過,結果被我搞爛了。

可是為什麼會這麼容易被我搞爛呢?其實是因為程式有bug,在因緣巧合下讓我把它從潛在的狀態中解放出來,所以並不是我的錯喔!而那段程式片段概略如下:

int getFromDB() {
        //do something
        return value;
}

Integer value1 = mValue;
Integer value2 = getFromDB();
if (value1 == value2) {
        System.out.println(“A”);
}
else {
        System.out.println(“B”);
}

根據正確的邏輯,我們要得到的結果是A,而不是B。也就是說,在一般情況下value1和value2會擁有相同的值。上述的程式碼乍看之下,好像沒有什麼問題,但它還是造成了兩個不一樣的結果。我想,眼尖的人也許已經發現到問題在哪裡。問題就發生在if條件判斷式的部分。value1和value2是物件而不是基本型別的變數,如果直接用==比較,比較的是物件的記憶體位址而不是它們實際的值。既然比較的是記憶體位址,兩個不同的變數,應該存在於不同的記憶體位址,應該永遠只會走到B的路線。可是弔詭的事發生了,大部分的手機走的都是A路線,只有那個被我搞爛的手機走到了B路線。

其實這個現象是由於Java中wrapper class和autoboxing的機制所造成。在解釋原因前,首先介紹Java中的wrapper class。wrapper class的出現是因為,Java裡面提供的許多API只能針對物件(例如,Collection),無法適用於基本型別(即int, long, char, boolean等)。為了使這些基本型別也可以適用,所以Java創造了與這些基本型別相對應的wrapper class。

boolean <-> Boolean
byte <-> Byte
char <-> Character
short <->Short
int <-> Integer
long <-> Long
float <-> Float
double <-> Double

至於autoboxing,就是自動的把primitive轉成wrapper的機制;而與unboxing就是自動把wrapper轉成primitive的機制。例如,在Java中如果直接將基礎型別的值或字面常數(例如,1, true, ’a’, “ABC”)賦值給wrapper class的話,Java就會啟動autoboxing的機制,產生一個屬於該型別的物件,並把該值存到該物件中,讓基礎型別變成物件。再深入一點探討的話就會發現,這些自動產生的wrapper class物件其實會構成一個pool。當autoboxing再度發生時,Java就會先檢查pool中是否有相同的值,有的話就直接回傳有相同值的物件;否則的話,就會再產生一個新的物件來存這個值。

因為這樣的機制,當value1和value2值一樣的時候,就會指向同一個記憶體,使得流程往分支A地方走。既然是這樣的話,怎麼會導致另一個同事出現往B走的現象呢?其實這又牽扯到autoboxing的另一個重要的部分。
Autoboxing的原意是要減少實體化的物件數,因此透過建立了一個pool來儲存這些物件,但這也造成另一個問題,也就是這些物件不會隨便被刪除掉。如果沒有限制的建立這些物件,記憶體一定很快就會用光的,所以Java給每個wrapper class的pool設立了最大值的暫存限制。也就是說,當要賦予的值超出某個範圍的話,就會以一般物件建立的方式建立(ex. Integer I = new Integer(321);)。

Boolean:  (全部暫存)
Byte:         (全部暫存)
Character:    [0, 127] 暫存
Short :         [-128, 127] 暫存
Long:           [-128, 127] 暫存
Float:        (沒有暫存)
Double:     (沒有暫存)

因此,當value1或value2的值超過127時,value1和value2就會指向不同的記憶體位置,使得程式往分支B的部分走。到此我們就可以清楚的了解到,造成我的兩個同事測出不同結果的關鍵原因。

透過這個例子,我們可以體悟到一個重要的原則:只要操作到物件之間的比較,盡量使用equals或是compareTo的方法,而不要直接>, <, ==等運算子來比較,除非有特殊的需求。

//Fixed
if (value1.equals(value2)) {
        System.out.println(“A”);
}
else {
        System.out.println(“B”);
}

偽技術文章 - 重構

看那看不到的東西,聽那聽不到的聲音,知那不知道的事物,才是真理。
                                                                                             - 達摩


繼數學女孩系列(*1)後,結城浩的第三本書(其實應該是第四本)終於入手:Java 重構。這本書除了翻譯有些怪怪的、程式碼部分的單字大小寫有些有問題;其餘都還不錯,範例淺顯易懂,而且重要的一些重構手法都有提到。

最近時間比較多一些,於是開始K這本書,看了大約一半,突然有了一些心得。書中常常重複的提及:
1. 重構,在不改變外部可見的程式行為的前提下,將程式內部的結構進行改善。
2. 重構一定要Step by step。
3. 重構過程中,需讓現存及新的方法同時並存,再用新的取代現存的。
4. 重構每一步都要經過仔細的測試(一般會撰寫單元測試搭配重構)。

這樣的定義,讓我想到前幾個月我在修改負責的AP時所做過的事。由於leader的一些疑問,因此要求我將使用SharedPreference(*2)儲存的機制,修改成使用Database來儲存。

這個問題困擾我很久,因為SharedPreference幾乎跟整個AP交纏在一起,我想這就是鮑伯大叔提到的僵化性(Rigidity)(*3),也就是牽一髮而動全身,修改一部分全部的地方就要跟著改。

過了幾天,我還是想不出一個簡單可以修改的方法,於是突然想到:不動手,想太多都是沒有用的。於是我開始動手改,果然隨便改一下就編譯不過了,過了幾個小時,整個code終於被我改爛了,YA!然後我又默默的從Codebase中下載一份全新、未改爛的程式碼。

有了上次的經驗,經我反思後,想到了一個方法:就是讓現存的方法和新的方法同時並存,然後再去比較使用新方法獲得的值是否跟現存方法的一樣,如果不一樣就表示新方法有bug。於是我依循著這個原則,開始了我的修改程式大作戰。

一開始因為怕改爛,所以一次改一小小的部分,然後測試了一下好像OK耶,於是就繼續改下去。慢慢的愈改愈有自信,就想說一次改多一點吧!結果,又被我改爛了,害我花了點時間Rollback到沒改爛的進度。這讓我學到,真的還是要一步一步的修改,確認無問題再繼續往下走。

終於,兩種儲存的方法終於並存且可以獲得相同的值。接著,我慢慢的將判斷式中有使用到值,從由SharedPreference獲得的方法改成由Database獲得的方法,改好一小部分就測試一下。慢慢的改慢慢的測,終於SharedPreference已經不被原來的程式所使用到。於是我就忿忿然將所以存取SharedPreference的方法砍掉,真的是超爽的。

回到開頭提到的Java重構原則,恰恰都跟我在修改程式的過程中所體會到的不謀而合。
1. 在替換Database的儲存方法後,使用者使用的功能要跟原來的一樣。
2. 讓SharedPreference和Database的儲存方法並存,再慢慢的替換它們。
3. 急躁的修改易造成失敗。
4. 不斷的測試這兩種方法獲得的資料是否相同。

經過書籍的閱讀及這次的經驗,我覺得重構只是一種系統化的流程,只要根據這個流程並小心的修改,應該都不太容易失敗;反而是為什麼要重構,重構前後對開發者或現存程式碼有什麼影響才是最重要的。如果只為了重構而重構,只會讓程式變得更加複雜與難以維護;適當的考慮每個環節,評估現有程式的結構性,找出重構與不重構的平衡點,我想才是重構真正的原意吧!


莫名其妙寫了一堆,其實自己也不是很懂,果然資管的就是愛嘴砲。

註:
*1. 結城浩所編寫的關於數學的高校愛情小說。
*2. SharedPreference是android提供的一種儲存機制,可以將資料存成xml的格式,並寫入到磁碟中。
*3. 僵化性:系統難以修改,因為每一個修改都會迫使系統其他部分也要變動 (出自:敏捷軟體開發 - 原則、樣式及實務一書) 。

偽技術文章 - assert

最近空閒的時間比較多一些,所以萌起了寫技術文章的念頭,這一部分是我在公司寫的關於Java中assert的介紹與使用。

Assert is a keyword in java, just like for, if, switch, etc. It means assert is a native function in java.
Usage:
        int x = 5;
        assert x > 4;

If the condition is true, it will do nothing; Otherwise, it will throw java.lang.AsserError and then crash the process.

Note that assert will always be compiled in byte code. It means more you write, then the larger byte code you obtain.
By the way, the assertion mode is disabled in default java command. We need to use parameters to enable it.
Usage:
        javac Test.java
        java -ea Test   <= enable assertion mode
        java -da Test   <= disable assertion mode

In android, we can use adb command to open assertion mode:
        adb shell setprop debug.assert 1   <= enable assertion mode
        adb shell setprop debug.assert 0   <= disable assertion mode

Using assert instead of comments is a common way. But we need to be careful not to change the original data and function.
Furthermore, it cannot be used to do error handling because behavior would be changed if assert is disabled.
Example 1:
        x = getValue();
         /* 20 <= x <= 50 */

Using assert instead of comment:
        assert isBetween(x, 20, 50);
        private boolean isBetween(int value, int lb. int ub) {
                if(value >= lb && value <= ub)
                        return true;
                else
                        return false;
        }

Example 2:
        x = 19;
        if( x>=20 && x <= 50 )
                doSomething(x);
        else
                throw new Exception(“Not between 20 and 50.”);
Bad codes
        x = 19;
        assert isBetween(x, 20, 50);
        doSomething(x);

The example of snippet shows that if x is not between 20 and 50, it will throw an exception and doSomething(x) is unreachable.
In bad codes, using assert instead of comments will change the original behavior. The function doSomething(x) will always be reached whatever x value if assert is disabled.

As mentioned above, it may obtain the larger byte code because of more assert. However, we can use the if-condition to make assert unreachable in order to avoid this problem.
Usage:
        private static final boolean DEBUG = false;
        int x = 24;
        if(DEBUG) {
                assert isBetween(x, 20, 50);
        }

Per this method, assert will be removed by code optimization in compilation process.

P.S. We also can use junit assert api for debugging.
http://developer.android.com/reference/junit/framework/Assert.html