或許有的掘友們發現了,在閱讀 Android 系統底層源碼或者開源框架源碼時,發現內部大量的設計模式,如果你對設計模式不懂的話,那麼閱讀源碼真的是寸步難行。那麼這篇文章我們先來學習面向對象的六大原則,設計模式大概 23 種,後面我們一步一步來學習它。
單一職責原則
單一職責原則的英文名稱是 Single Responsibility Principle ,縮寫是 SRP 。 SRP 的定義是:就一個類而言,應該僅有一個引起變化的原因。簡單的來說,就是一個類中應該是一組相關性很高的函數、數據的封裝。單一職責的劃分界限也並不是那麼的清晰,很多時候都是靠個人經驗來給定界限,當然,最大的的問題就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。
下面我們就以 圖片加載庫 的例子代碼來對類的職責簡單說明下,在設計一個圖片加載庫之前,我們需要先大概畫下 UML 類圖,有了 UML 圖之後寫代碼就能更加的清晰。
從上面 UML 類圖可以看出 ImageLoader 只負責加載圖片,MemoryCache 實現 IImageCache 負責往內存中存/取緩存,到這裏也許有的同學對單一職責有了一定概念了,相信看完下面的代碼,你已經對單一職責掌握的差不多了,直接上代碼
public class ImageLoader {
/**
* 內存緩存
*/
private IImageCache mMemoryCache;
/**
* 圖片下載
*/
private IDownloader mImageDownloader;
/**
* 線程池
*/
private ExecutorService mExecutorService;
/**
* 主線程管理
*/
private Handler mHandler = new Handler(Looper.getMainLooper());
private static ImageLoader instance;
public static ImageLoader getInstance() {
if (instance == null)
instance = new ImageLoader();
return instance;
}
public ImageLoader() {
//圖片緩存
this. mMemoryCache = new MemoryCache();
//圖片下載
this.mImageDownloader = new HttpURLConnectionDownloaderImp();
//線程池,線程數據量爲 CPU 的數量
this.mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
/**
* 加載圖片
*/
public void loadImage(final String url, final ImageView imageView) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
imageView.setTag(url);
//如果內存緩存中沒有圖片,就開啓網絡請求去下載
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap imager = mImageDownloader.downLoader(url);
if (imager == null) return;
if (imageView.getTag().equals(url)) {
displayImage(imager, imageView);
}
mMemoryCache.put(url,imager);
}
});
}
/**
* 顯示圖片
*
* @param downBitmap
* @param imageView
*/
private void displayImage(final Bitmap downBitmap, final ImageView imageView) {
mHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(downBitmap);
}
});
}
}
public class MemoryCache implements IImageCache {
/**
* 初始化內存緩存
*/
private LruCache<String, Bitmap> mMemoryLru;
public MemoryCache() {
init();
}
private void init() {
int currentMaxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//內存緩存的大小
int cacheSize = currentMaxMemory / 4;
mMemoryLru = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
}
@Override
public void put(String url, Bitmap bitmap) {
mMemoryLru.put(url,bitmap);
}
@Override
public Bitmap get(String url) {
return mMemoryLru.get(url);
}
}
通過上面代碼可以看出 ImageLoader 負責圖片加載的邏輯,而 MemoryCache 負責緩存,這 2 個類職責分明,就像公司裏面不同部門幹不同的活一樣。但是,如果這 2 類寫在一起的話,缺點一下就出來了,不僅功能職責不分明,而且代碼也比較臃腫,耦合太重。
現在雖然代碼結構變得清晰,職責也分明瞭,但是可擴展性還需要進一步優化,下面我們就來慢慢優化吧。
開閉原則
開閉原則英文全稱是 Open Close Principle,縮寫 OCP ,它是 Java 世界裏最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。
開閉原則的定義是:軟件中的對象 (類、模塊、函數等) 應該對於擴展是開放的,但是,對於修改是封閉的 這就是開放-關閉原則。
上一小節的 ImageLoader 職責單一,結構清晰,應該算是一個不錯的開始了,但是 Android 中應用內存是有限制的,當應用重新啓動,那麼原有的緩存就不在了。現在我們加上本地磁盤緩存,爲了遵從開閉原則的思想,我又對 ImageLoader 重新設計了。
public class ImageLoader {
private String TAG = getClass().getSimpleName();
/**
* 默認內存緩存
*/
private IImageCache mMemoryCache;
/**
* 線程池
*/
private ExecutorService mExecutorService;
/**
* 主線程管理
*/
private Handler mHandler = new Handler(Looper.getMainLooper());
private static ImageLoader instance;
public static ImageLoader getInstance() {
if (instance == null)
instance = new ImageLoader();
return instance;
}
public ImageLoader() {
mMemoryCache = new MemoryCache();
//線程池,線程數據量爲 CPU 的數量
mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
/**
* 用戶配置緩存策略
*
* @param imageCache
*/
public void setImageCache(IImageCache imageCache) {
this.mMemoryCache = imageCache;
}
/**
* 加載圖片
*/
public void loadImage(final String url, final ImageView imageView) {
.....
}
/**
* 顯示圖片
*
* @param downBitmap
* @param imageView
*/
private void displayImage(final Bitmap downBitmap, final ImageView imageView) {
.....
}
}
//磁盤緩存
public class DiskCache implements IImageCache {
private DiskLruCache mDiskLruCache;
private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB
//IO緩存流大小
private static final int IO_BUFFER_SIZE = 8 * 1024;
//緩存個數
private static final int DISK_CACHE_INDEX = 0;
public DiskCache(Context context) {
try {
File cacheDir = CacheUtils.getDiskCacheDir(context, "bitmapCache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, ImageLoaderUtils.getAppVersion(context), 1, MAX_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void put(String url, Bitmap bitmap) {
OutputStream outputStream = null;
DiskLruCache.Snapshot snapshot = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
String key = ImageLoaderUtils.hashKeyForDisk(url);
try {
snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
InputStream inputStream = ImageLoaderUtils.bitmap2InputStream(bitmap, 50);
in = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
editor.commit();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (snapshot != null) {
snapshot.close();
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public Bitmap get(String url) {
//通過key值在緩存中找到對應的Bitmap
Bitmap bitmap = null;
String key = ImageLoaderUtils.hashKeyForDisk(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null) return null;
//得到文件輸入流
InputStream fileInputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (fileInputStream != null)
bitmap = BitmapFactory.decodeStream(fileInputStream);
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
public class DoubleCache implements IImageCache {
private String TAG = getClass().getSimpleName();
/**
* 內存緩存
*/
private IImageCache mMemoryCache;
/**
* 磁盤緩存
*/
private IImageCache mDiskCache;
public DoubleCache(Context context) {
this.mMemoryCache = new MemoryCache();
this.mDiskCache = new DiskCache(context);
}
@Override
public void put(String key, Bitmap bitmap) {
mMemoryCache.put(key, bitmap);
mDiskCache.put(key, bitmap);
}
@Override
public Bitmap get(String url) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap != null) {
Log.i(TAG,"使用內存緩存");
return bitmap;
}
Log.i(TAG,"使用磁盤緩存");
return mDiskCache.get(url);
}
}
public interface IImageCache {
/**
* 存圖片
*/
void put(String url, Bitmap bitmap);
/**
* 獲取圖片
*/
Bitmap get(String url);
}
IImageCache 接口簡單定義了 存儲/獲取 兩個函數,緩存的 url 就是圖片網絡地址,值就是緩存的圖片,經過這次重構我們擴展了內存/磁盤緩存,細心的同學可能注意到了, ImageLoader 類中增加了一個 setImageCache (IImageCache cache) 函數,用戶可以通過該函數來設置緩存,也就是通常說的依賴注入。下面看看怎麼配置:
public void config() {
//使用雙緩存
ImageLoader.getInstance().setImageCache(new DoubleCache(getApplicationContext()));
//用戶自定義
ImageLoader.getInstance().setImageCache(new IImageCache() {
@Override
public void put(String url, Bitmap bitmap) {
}
@Override
public Bitmap get(String url) {
return null;
}
});
}
在上述代碼中,通過 setImageCache() 方法注入不同的緩存實現,這樣不僅能夠使 ImageLoader 更簡單,健壯,也使得 ImageLoader 的可擴展性,靈活性能高,MemoryCache 、DiskCache 、DoubleCache 緩存圖片的具體實現完全一樣,但是,他們的一個特點是都實現了 ImageCache 接口,並且通過 setImageCache() 注入到 IImageCache 中,這樣就實現了千變萬化的緩存策略,且擴展不會導致內部的修改,哈哈,這就是我們之前所說的開閉原則。
里氏替換原則
里氏替換原則英文全稱是 Liskov Substitution Principle , 縮寫是 LSP。LSP 的第一種定義是:**如果對每一個類型爲 S 的對象 O1, 都有類型爲 T 的對象 O2, 使得以 T 定義的所有程序 P 在所有的對象 O1都替換成 O2 時,程序 P 的行爲沒有發生變化,那麼類型 S 是類型 T 的子類型。**上面這種描述確實有點不好理解,我們再來看第二種里氏替換原則定義:所有引用基類的地方必須能透明地使用其子類的對象。
我們知道,面嚮對象語言的三大特點是 繼承,封裝,多態,里氏替換原則就是依賴於 繼承,多態這兩大特性。里氏替換原則通俗來說的話就是,只要父類能出現的地方子類就可以出現,而且替換爲子類也不會產生任何錯誤或異常,使用者可能根本不用知道是父類還是子類,但是反過來就不行了,有子類出現的地方,父類就不一定能適應,說了這麼多,其實最終總結就兩個字:抽象。
爲了我們能夠深入理解直接看下面代碼示例吧:
//框口類
public class Window{
public void show(View view){
view.draw();
}
}
//建立視圖對象,測量視圖的寬高爲公用代碼,繪製實現交給具體的子類
pubic abstract class View{
public abstract void draw();
public vid measure(int width,int height){
//測量視圖大小
}
}
public class ImageView extends View{
draw{
//繪製圖片
}
}
... extends View{
...
}
上述示例代碼中, Window 依賴於 View , 而 View 定義了一個視圖抽象, measure 是各個子類共享的方法,子類通過重寫 View 的draw 方法實現具有各自特色的功能,在這裏,這個功能就是繪製自身的內容,在任何繼承 View 類的子類都可以傳遞給 show 函數,這就是所說的里氏替換。
里氏替換原則的核心原理是抽象,抽象又依賴於繼承這個特性,在 OOP 當中,繼承的優缺點都相當明顯,優點:
- 代碼複用,減少創建類的成本,每個子類都擁有父類的方法和屬性;
- 子類於父類基本相似,但又與父類有所區別;
- 提高代碼的可擴展性;
繼承的缺點:
- 繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法;
- 可能造成子類代碼冗餘,靈活性降低,因爲子類必須擁有父類的屬性和方法。
事務都是都利和弊,須合理利用。
繼續拿上面的 ImageLoader 緩存策略來說明裏氏替換原則,用戶只需要指定具體的緩存對象就可以通過 ImageCache 的 setImageCache() 函數就可以替換 ImageLoader 的緩存策略,這就使得 ImageLoader 的緩存系統有了無限的可能性,也保證了可擴展性。
開閉和里氏往往是生世相依,不離不棄,通過里氏替換來達到程序的擴展,對修改的關閉效果。然而,這兩個原則都同時強調了一個 OOP 的重要性 - 抽象,因此,在開發過程中,運用抽象是走向代碼優化的重要一步。
依賴倒置原則
依賴倒置原則英文全稱是 Dependence Inversion Principle, 簡寫 DIP 。依賴倒置原則指代了一種特定的解耦形式,使得高層次的模塊不依賴於底層次模塊的實現細節的目的,依賴模塊被顛倒了。這個概念有點不好理解,這到底是什麼意思勒?
依賴倒置有幾個關鍵點:
- 高層模塊不應該依賴底層模塊,兩者都應該依賴起抽象;
- 抽象不應該依賴細節;
- 細節應該依賴抽象;
在 Java 語言中,抽象就是接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,其特點就是可以直接實例化,也就是可以加上一個 new 關鍵字產生一個對象。高層模塊就是調用端,底層模塊就是具體實現類。依賴倒置原則在 Java 語言中的表現就是: 模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過接口或抽象類產生的 ,這又是一個將理論抽象化的實例,其實一句話可以概括:面向接口編程,或者說是面向抽象編程,面向接口編程是面向對象精髓之一,也就是上面兩節強調的抽象。
這裏我們還是以 ImageLoader 來說明,先看下面代碼:
public class ImageLoader {
private String TAG = getClass().getSimpleName();
/**
* 默認內存緩存(直接依賴於細節,而不是抽象)
*/
private MemoryCache mMemoryCache;
/**
* 線程池
*/
private ExecutorService mExecutorService;
/**
* 主線程管理
*/
private Handler mHandler = new Handler(Looper.getMainLooper());
private static ImageLoader instance;
public static ImageLoader getInstance() {
if (instance == null)
instance = new ImageLoader();
return instance;
}
public ImageLoader() {
mMemoryCache = new MemoryCache();
//線程池,線程數據量爲 CPU 的數量
mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
/**
* 用戶配置緩存策略
*
* @param imageCache
*/
public void setImageCache(MemoryCache imageCache) {
this.mMemoryCache = imageCache;
}
...
}
上面代碼 ImageLoader 直接依賴於細節 MemoryCache ,如果框架升級需有多級緩存也就是內存 + SD 卡緩存策略,那麼就又需要改 ImageLoader 中的代碼,如下:
public class ImageLoader {
private String TAG = getClass().getSimpleName();
/**
* 默認內存緩存(直接依賴於細節,而不是抽象)
*/
private DoubleCache mMemoryCache;
/**
* 線程池
*/
private ExecutorService mExecutorService;
/**
* 主線程管理
*/
private Handler mHandler = new Handler(Looper.getMainLooper());
private static ImageLoader instance;
public static ImageLoader getInstance() {
if (instance == null)
instance = new ImageLoader();
return instance;
}
public ImageLoader() {
mMemoryCache = new DoubleCache();
//線程池,線程數據量爲 CPU 的數量
mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
/**
* 用戶配置緩存策略
*
* @param imageCache
*/
public void setImageCache(DoubleCache imageCache) {
this.mMemoryCache = imageCache;
}
...
}
在 ImageLoader 中我們把默認內存緩存改成了雙緩存,這樣不僅違背了沒有開閉原則,也沒有依賴於抽象,所以下面的代碼纔是正確的:
public class ImageLoader {
private String TAG = getClass().getSimpleName();
/**
* 默認內存緩存 默認依賴於抽象
*/
private IImageCache mMemoryCache;
private static ImageLoader instance;
public static ImageLoader getInstance() {
if (instance == null)
instance = new ImageLoader();
return instance;
}
public ImageLoader() {
...
}
/**
* 用戶配置緩存策略 注入抽象類
*
* @param imageCache
*/
public void setImageCache(IImageCache imageCache) {
this.mMemoryCache = imageCache;
}
}
在這裏實現類沒有發生直接的依賴,而是通過抽象發生的依賴。滿足了依賴倒置基本原則,想要讓程序更爲靈活,那麼抽象就是邁出靈活的第一步。
接口隔離原則
接口隔離原則英文全稱是 InterfaceSegregation Principles, 縮寫 ISP 。接口隔離原則的目的是系統解耦,從而容易重構、更改和重新部署。說白了就是讓客服端依賴的接口儘可能地小,這樣說可能還有點抽象,還是以一個示例說明一下
未優化的接口
public class DiskCache implements IImageCache {
private DiskLruCache mDiskLruCache;
private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB
//IO緩存流大小
private static final int IO_BUFFER_SIZE = 8 * 1024;
//緩存個數
private static final int DISK_CACHE_INDEX = 0;
@Override
public void put(String url, Bitmap bitmap) {
.....
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (snapshot != null) {
snapshot.close();
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
可以看見上面一段代碼雖然功能達到了要求,但是各種 try…catch 嵌套,不經影響代碼美觀,而且可讀性差。我們可以看 Cloaseable 這個類的實現差不多 160 多個實現類,如果每個類都 close 那不的瘋了,我們直接抽取一個 CloseUtils 如下:
public class CloaseUtils {
public static void close(Closeable... closeable) {
if (closeable != null) {
try {
if (closeable.length == 1){
closeable[0].close();
return;
}
for (int i = 0; i < closeable.length; i++) {
closeable[i].close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
支持同時關閉一個,或多個實現類的 close。
改造之後的代碼:
public class DiskCache implements IImageCache {
private DiskLruCache mDiskLruCache;
private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB
//IO緩存流大小
private static final int IO_BUFFER_SIZE = 8 * 1024;
//緩存個數
private static final int DISK_CACHE_INDEX = 0;
@Override
public void put(String url, Bitmap bitmap) {
.....
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
CloaseUtils.close(snapshot,out,in);
}
}
}
是不是清爽多了,一行代碼解決了剛剛差不多 10 行代碼的邏輯。而且這裏基本原理就是依賴於 Closeable 抽象,而不是具體實現類(這不就是我們剛剛纔說了的依賴倒置原則嘛),並且建立在最小化依賴原則的基礎上,它只需要知道這個對象是否關閉,其它一概不關心,也就是這裏的接口隔離原則。
迪米特原則
迪米特原則英文的全稱爲 Law of Demeter , 縮寫是 LOD , 也稱爲最少知識原則。雖然名字不同,但描述的是同一個原則:一個對象應該對其他對象有最少的的瞭解。通俗的將,一個類應該對自己需要耦合或調用的類知道的最少,類的內部如何實現與調用者或者依賴者沒有關係,調用者或者依賴着只需要知道它需要的方法即可,其他的可一概不用管。類與類之間關係密切,耦合度就越大,當一個類發生改變時,對另一個類的影響也越大。
下面以一個租房例子說明:
/**房子*/
public class Room {
//面積
public float area;
//價錢
public float price;
public Room(float area,float price){
this.area = area;
this.price = price;
}
}
/**中介*/
public class Mediator{
List <Room> mRooms = new ArrayList<Room>();
public Mediator(){
for(i = 0; i < 5 ; i ++){
mRoom.add(new Room(14 + i,(14 + i) * 150));
}
}
public List<Room> getAllRooms(){
return mRooms;
}
}
/**租客**/
public class Tenant {
public void rentRoom(float roomArea,float roomPrice,Mediator mediator){
List<Room> rooms = mediator.getAllRooms();
for(Room room : rooms){
if(isSuitable(roomArea,roomPrice,room)){
Log.i(TAG,"租到房子了");
bread;
}
}
}
//租金要小於等於指定的值,面積要大於等於指定的值
public boolean isSuitable(float roomArea,float roomPrice,Room room){
return room.price <= roomPrice && room.area >= roomArea;
}
}
上面的代碼中可以看到,Tenant 不僅依賴了 Mediator 類,還需要頻繁得於 Room 類打交道。租客只是找一個房子而已,如果把這些功能都放在 Tenant 類裏面,那中介都沒有存在感了吧?耦合太重了,我們只需要跟中介通信就行了,繼續重構代碼;
//中介
public class Mediator{
List<Room> mRooms = new ArrayList<Room>();
/**構造房子**/
public Mediator(){
for(i = 0; i < 5 ; i ++){
mRoom.add(new Room(14 + i,(14 + i) * 150));
}
}
public Room rentOut(float area,float price){
for(Room room : mRooms){
if(isSuitable(area,price,room)){
return room;
}
}
return null;
}
public boolean isSuitable(float area,float price ,Room room){
return room.price <= price && room.area >= area
}
}
//租客
public class Tenant{
/**是否租到房子了*/
public Room rentRoom(float roomArea,float roomPrice,Mediator mediator){
return mediator.rentOut(roomArea,roomPrice);
}
}
總結
從六大原則中我們得出了重要的結論,就是一定要有抽象的思維,面向抽象或面向接口編程。在應用開發過程中,最難的不是完成開發工作,而是後續的維護和迭代工作是否擁有可變性,擴展性,在不破壞系統的穩定性前提下依然保持 二高一低原則(高擴展,高內聚,低耦合) 在經歷多個版本的迭代項目依然保持清晰,靈活,穩定的系統架構。當然這是我們一個比較理想的情況,但是我們需要往這個方向去實現努力,就相當於接口(想法)出來了,我們要去實現(接口實現類)它,遵循面向對象六大原則就是我們走向靈活軟件之路所邁出的第一步,加油!