學會放下包袱,熱愛單例
原文連結 譯者:曾維朝
企業應用程式與移動應用程式有著截然不同的要求。你啟動一次企業應用程式,它會連續執行數月或數年。另一方面,大部分手機應用可能是被正在無聊排隊或者坐公交車的使用者啟動的,它們經常連續執行不超過幾分鐘,這就意味著移動應用程式必須即時開啟,而啟動一個企業應用程式則需要足夠長的時間。
對於企業應用,依賴注入和早期驗證是非常重要的, Spring為此提供了極大的便利。 但是別欺騙自己,Spring是好,但它不是萬金油。尤其在崇尚快速啟動、低記憶體消耗、避免介面的移動開發領域。
企業應用程式的瓶頸幾乎都在資料庫,在這裡或者其它的地方多花費幾毫秒並無大礙。而在效能弱得多的移動裝置上, 這些時鐘週期不僅算在使用者等待的時間上,並且會損耗裝置的電量。一個簡單的事實,使用介面來替換抽象父類,都要慢上1倍。甚至在巢狀呼叫的建構函式中多傳入個引數,稍微多幾層的呼叫,都會產生影響。
選擇性延遲載入單例
Java中的類載入是懶惰式的,Java虛擬機器只有在類被引用的時候才會將其載入。使用者短時間內執行的移動應用一般不會觸發載入其內部的所有類。如果使用者只是檢查未讀資訊後退出,則寫資訊相關的類是不必載入的。早期依賴注入打破了這種做法,所有的類在啟動時引入。大部分元件都將會被徒勞的初始化,因為其實它們從未被真正的使用。
所以我們在開發移動應用的做法應該效仿Java虛擬機器,而不是像Spring那樣。要儘可能只在需要的時候建立某個元件,最好的實現方式就是使用單例模式,但不是那種通常的嚴格的單例模式,而是一個可選的單例。使用公用的建構函式,信任你的使用者,並用getSharedFoo()來命名你的獲取器(getter),而不是使用getInstance()。我使用URL快取元件的例子給你示範下。
public class URLCache { private static URLCache sharedCache; public static URLCache getSharedURLCache() { synchronized (URLCache.class) { if (sharedCache == null) { sharedCache = new URLCache(); } } return sharedCache; } public URLCache() { // More code... } // Allot more code here... }
在我們想象中的HTTP提供者(HTTPProvider)中使用這個共享的URL快取(URLCache)元件,將會是超級簡單的, 但不是強制的:
public class HTTPProvider { public InputStream inputStreamForURL(Stringurl) { URLCache cache =URLCache.getSharedURLCache(); // Use the cache... } }
這裡最大的好處就是,如果這個應用程式的程式碼路徑從未執行到嘗試開啟一個輸入流,那麼URLCache的cache物件就永遠不必建立。用來讀取快取索引、驗證、等等的幾百毫秒時間被節省了。
但是測試呢?
對於單元測試和mocking元件,單例不是不好嗎? 他們以前是不好,如今我們有PowerMock,你真的應該用它。如果我們只是稍微改了下單例模式,而且可以外部設定共享的元件,那就連PowerMock實際上也不需要了:
public class URLCache { private static URLCache sharedCache; public static void setSharedURLCache(URLCachecache) { synchronized (URLCache.class) { sharedCache = cache; } } publicstatic URLCache getSharedURLCache() { synchronized (URLCache.class) { if (sharedCache == null) { sharedCache = new URLCache(); } } return sharedCache; } // Allot more code here...
上述新增的那小段程式碼讓我們可以在裝入單元測試類時設定自己的mockcache物件。簡單示例如下:
public class SomeTest extendsTestCase { public void setUp() { URLCache.setSharedURLCache(newNoOpURLCache()); } public void testRemoteResource() { assertNotNull(HTTPProvider.getSharedHTTPProvider().inputStreamForURL(TEST_URL)); } }
如果應用程式有特別的需求, 我們還可以顯式的過載一個單例。這些需求也許是低頻寬下的移動終端資料連線的一個更先進的快取方式, 或是在問題多多的JavaME平臺上的一個特殊實現。
要點
- 不要懼怕單例,它是個非常好的延遲建立的方式,可以大大改善移動應用程式的啟動時間和記憶體消耗。
- 避免使用介面,它們比類至少要慢1倍,而且介面不可以有用於獲取延遲建立的元件的靜態方法。
- 不要強制使用單例模式,為了測試和特殊情況的易於實現,永遠使用可選單例。