

最新在閱讀《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》一書,我覺得寫的很清晰,每一個(gè)知識點(diǎn)都有示例,通過示例更加容易理解。書中的知識點(diǎn)有些都接觸過,有的沒有接觸過,總之,通過閱讀這本書來梳理一下知識點(diǎn),可能有些東西在項(xiàng)目中一直在使用,然并不能籠統(tǒng),清理的說明理解它。本文主要是記錄閱讀這本書的知識點(diǎn)和自己的一些理解。一來整理知識點(diǎn),二來方便以后查看,快速定位。
單一職責(zé)原則 :優(yōu)化代碼第一步
單一職責(zé)原則(英文簡稱:SRP):對于一個(gè)類而言,應(yīng)該僅有一個(gè)引起它變化的原因。這個(gè)有點(diǎn)抽象,因?yàn)樵撛瓌t的劃分界面并不是那么清晰,很多時(shí)候靠個(gè)人經(jīng)驗(yàn)來區(qū)分。簡單來說就是一個(gè)類只負(fù)責(zé)一個(gè)功能,比如加減乘除應(yīng)分別對應(yīng)一個(gè)類,而不是把四個(gè)功能放在一個(gè)類中,這樣在只要有一個(gè)功能變化都需要更改這個(gè)類。
下面以實(shí)現(xiàn)一個(gè)圖片加載器(ImageLoader)來說明:
public class ImageLoader {
//圖片內(nèi)存緩存
private LruCache<String,Bitmap> mImageCache;
//線程池,線程數(shù)量為CPU的數(shù)量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public ImageLoader(){
initImageLoader();
}
//初始化
private void initImageLoader() {
//計(jì)算最大的可使用內(nèi)存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取四分之一作為最大緩存內(nèi)存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
public void displayImage(final String url, final ImageView imageView){
Bitmap bitmap = mImageCache.get(url);
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return;
}
if (imageView.getTag().equals(url)) {
imageView.setImageBitmap(bitmap);
}
mImageCache.put(url,bitmap);
}
});
}
}
我們一般都會這樣這樣簡單的實(shí)現(xiàn)一個(gè)圖片加載工具類,這樣寫功能雖然實(shí)現(xiàn)了,但是代碼是有問題的,代碼耦合嚴(yán)重,隨著ImageLoader功能越來越多,這個(gè)類會越來越大,代碼越來越復(fù)雜。按照單一職責(zé)原則,我們應(yīng)該把ImageLoader拆分一下,把各個(gè)功能獨(dú)立出來。

ImageLoader修改代碼如下:
public class ImageLoader {
//圖片緩存
ImageCache mImageCache = new ImageCache();
//線程池,線程數(shù)量為CPU的數(shù)量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public void displayImage(final String url, final ImageView imageView){
.............
}
}
public class ImageCache {
//圖片內(nèi)存緩存
private LruCache<String,Bitmap> mImageCache;
public ImageCache(){
initImageCache();
}
//初始化
private void initImageCache() {
//計(jì)算最大的可使用內(nèi)存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取四分之一作為最大緩存內(nèi)存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
public Bitmap get(String url){
return mImageCache.get(url);
}
public void put(String url,Bitmap bitmap){
mImageCache.put(url,bitmap);
}
}
上述代碼將ImageLoader一分為二,ImageLoader只負(fù)責(zé)圖片加載的邏輯,ImageCache負(fù)責(zé)緩存策略,這樣,ImageLoader的代碼變少了,職責(zé)也清晰了,并且如果緩存策略改變了的話,只需要修改ImageCache而不需要在修改ImageLoader了。從這個(gè)例子可以更加清晰的理解什么是單一職責(zé)原則,如何去劃分一個(gè)類的職責(zé),每個(gè)人的看法不同,這需要根據(jù)個(gè)人的經(jīng)驗(yàn),業(yè)務(wù)邏輯而定。
開閉原則:讓程序更穩(wěn)定,更靈活
開閉原則(英文縮寫為OCP): 軟件中的對象(類,函數(shù),模塊等)對于擴(kuò)展是開放的,但是對于修改是封閉的。在軟件的生命周期內(nèi),因?yàn)樽兓?,升級和維護(hù)的原因需要對原代碼修改時(shí),可能會將錯(cuò)誤引入已經(jīng)經(jīng)過測試的舊代碼,破壞原有的系統(tǒng)。因?yàn)楫?dāng)需求變化時(shí),我們應(yīng)盡可能的通過擴(kuò)展來實(shí)現(xiàn),而不是修改原來的
代碼。
在實(shí)際的開發(fā)過程中,只通過繼承的方式來升級,維護(hù)原有的系統(tǒng)只是一個(gè)理想化的狀態(tài),修改原代碼,擴(kuò)展代碼往往是同時(shí)存在的。我們應(yīng)盡可能的影響原代碼。避免引入的錯(cuò)誤造成系統(tǒng)的破壞。
還是上面的那個(gè)例子,雖然通過內(nèi)存緩存解決了每次都從網(wǎng)絡(luò)下載圖片的問題,但是Android內(nèi)存有限,并且當(dāng)應(yīng)用重啟后內(nèi)存緩存會丟失。我們需要修改一下,增加SD卡緩存,代碼如下:
public class ImageLoader {
//內(nèi)存緩存
ImageCache mImageCache = new ImageCache();
//SD卡緩存
DiskCache mDiskCache = new DiskCache();
//線程池,線程數(shù)量為CPU的數(shù)量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public void displayImage(final String url, final ImageView imageView){
//先從內(nèi)存緩存中讀取,如果沒有再從SD卡中讀取
Bitmap bitmap = mImageCache.get(url);
if(bitmap == null){
bitmap = mDiskCache.get(url);
}
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
//從網(wǎng)絡(luò)下載圖片
..........
}
}
public class DiskCache {
private final static String cacheDir = "sdcard/cache/";
/* 從緩存中獲取圖片 */
public Bitmap get(String url){
return BitmapFactory.decodeFile(cacheDir + url);
}
/* 將圖片添加到緩存中 */
public void put(String url,Bitmap bitmap){
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if(fileOutputStream != null){
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
上述代碼我們增加了SD卡緩存,我們在顯示圖片的時(shí)候先判斷內(nèi)存緩存中是否存在如果不存在就在SD卡中找,否則再從網(wǎng)絡(luò)下載,這樣就會有一個(gè)問題,每增加一個(gè)新的緩存方法,我們都需要修改原來的代碼,這樣可能引入Bug,而且會使原來的代碼越來越復(fù)雜,還有用戶也不能自定義緩存方法。我們具體使用哪一種緩存方法是通過
if條件判斷的,條件太多,是很容易寫錯(cuò)的。而且代碼會越來越臃腫,并且可擴(kuò)展性差??蓴U(kuò)展性是框架的重要特性之一。
根據(jù)開閉原則,當(dāng)軟件需求改變的時(shí)候,我們應(yīng)該通過擴(kuò)展的方式實(shí)現(xiàn),而不是修改自己的代碼。對上述代碼進(jìn)行優(yōu)化:

public class ImageLoader {
//默認(rèn)緩存方式為內(nèi)存緩存
ImageCache mImageCache = new MemoryCache();
//線程池,線程數(shù)量為CPU的數(shù)量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
//設(shè)置緩存方式
public void setImageCache(ImageCache cache){
mImageCache = cache;
}
public void displayImage(final String url, final ImageView imageView){
//先從緩存中讀取
Bitmap bitmap = mImageCache.get(url);
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
//網(wǎng)絡(luò)下載圖片
............
}
}
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}
通過上述代碼我們可以看出,ImageLoader增加了一個(gè)方法
setImageCache,我們可以通過該方法設(shè)置緩存方式,這就是我們常說的依賴注入。當(dāng)然我們還可以自定義自己的緩存方式,只需要實(shí)現(xiàn)ImageCache這個(gè)接口即可。然后再調(diào)用setImageCache這個(gè)方法來設(shè)置。而不需要修改ImageLoader的代碼。這樣當(dāng)緩存需求改變的時(shí)候我們可以通過擴(kuò)展的方式來實(shí)現(xiàn)而不是修改的方法,這就是所說的開閉原則。同時(shí)是ImageLoader的代碼更簡潔,擴(kuò)展性和靈活性也更高。
里氏替換原則:構(gòu)建擴(kuò)展性更好的系統(tǒng)
里氏替換原則(英文縮寫為LSP):所有引用基類的地方都必須能夠透明的使用其子類的對象。我們知道面向?qū)ο笥腥筇匦裕悍庋b,繼承和多態(tài),里氏替換原則就是依賴?yán)^承和多態(tài)這兩大原則,里氏替換原則簡單來說就是:只要是父類引用出現(xiàn)的地方都可以替換成其子類的對象,并且不會產(chǎn)生任何的錯(cuò)誤和異常。
下面以Android中的Window和View的關(guān)系的例子來理解里氏替換原則:

//窗口類
public class Window {
public void show(View child){
child.draw();
}
}
建立視圖抽象類,測量視圖的寬高為公共代碼,繪制交給具體的子類去實(shí)現(xiàn)
public abstract class View {
public abstract void draw();
public void measure(int width,int height){
//測量視圖大小
...........
}
}
//文本類具體實(shí)現(xiàn)
public class TextView extends View {
@Override
public void draw() {
//繪制文本
...........
}
}
//按鈕類具體實(shí)現(xiàn)
public class Button extends View {
@Override
public void draw() {
//繪制按鈕
.............
}
}
上述示例中,Window依賴于View,View定義了一個(gè)視圖抽象,
measure是各個(gè)子類共享的方法,子類通過重寫View的draw方法來實(shí)現(xiàn)具體各自特色的內(nèi)容。任何繼承View的子類都可以設(shè)置給show方法,這就是所說的里氏替換原則,通過里式替換,就可以自定義各種各樣的,千變?nèi)f化的View,然后傳遞給Window,Window負(fù)責(zé)組織View,并將View顯示到屏幕上。
上面ImageLoader的例子也體現(xiàn)了里氏替換原則,可以通過setImageCache方法來設(shè)置各種各樣的緩存方式,如果 setImageCache中的cache對象不能被子類替換,那么又怎么能設(shè)置各種各樣的緩存方式呢?
依賴倒置原則:讓項(xiàng)目擁有變化的能力
依賴倒置原則(英文縮寫為DIP)指代了一種特定的解耦方式,使得高層次的模塊不依賴于低層次模塊的實(shí)現(xiàn)細(xì)節(jié)的目的,依賴模塊被顛倒了。這個(gè)概念更加的抽象,該怎么理解呢?
依賴倒置原則有幾個(gè)關(guān)鍵的點(diǎn):
- 1.高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象。
- 2.抽象不應(yīng)該依賴細(xì)節(jié)。
- 3.細(xì)節(jié)應(yīng)該依賴抽象。
在Java語言中,抽象就是接口或者抽象類,兩者都是不能直接被實(shí)例化的;細(xì)節(jié)就是實(shí)現(xiàn)類,實(shí)現(xiàn)接口或者繼承抽象類而產(chǎn)生的類就是細(xì)節(jié),可以直接實(shí)例化;高層模塊就是調(diào)用端;底層模塊就是實(shí)現(xiàn)端。
依賴倒置原則在Java語言中的表現(xiàn)就是:模塊間的依賴通過抽象發(fā)生,實(shí)現(xiàn)類直接不能直接發(fā)生依賴,其依賴關(guān)系是通過接口或者抽象類產(chǎn)生的。
如果類與類之間直接依賴于細(xì)節(jié),那么它們之間就有直接的耦合,當(dāng)需求變化的時(shí)候,意味著要同時(shí)修改依賴者的代碼。這就限制了系統(tǒng)的可擴(kuò)展性。
public class ImageLoader {
//直接依賴于細(xì)節(jié)
MemoryCache mImageCache = new MemoryCache();
...................
}
ImageLoader直接依賴于MemoryCache,MemoryCache是一個(gè)具體的實(shí)現(xiàn),這就導(dǎo)致ImageLoader直接依賴于細(xì)節(jié),當(dāng)MemoryCache不能滿足而被其他緩存實(shí)現(xiàn)替換時(shí),就必須需要修改ImageLoader的代碼。
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}
public class ImageLoader {
//依賴于抽象,并且有一個(gè)默認(rèn)的實(shí)現(xiàn)
ImageCache mImageCache = new MemoryCache();
......................
在這里我們建立了
ImageCache抽象,并且讓ImageLoader直接依賴于抽象而不是具體的細(xì)節(jié),當(dāng)需求變化時(shí),只需要實(shí)現(xiàn)ImageCache或者繼承已有的類來完成相應(yīng)的緩存功能。然后再將具體的實(shí)現(xiàn)注入到ImageLoader中,保證了系統(tǒng)的高擴(kuò)展性。這就是依賴倒置原則。
接口隔離原則:讓系統(tǒng)擁有更高的靈活性
接口隔離原則(英文縮寫為LSP):客戶端不應(yīng)該依賴它不需要的接口。另外一種定義是:類間的依賴關(guān)系應(yīng)該建立在最小的接口上。接口隔離原則將龐大,臃腫的接口拆分成更小更具體的接口,這樣客戶端只需要知道它感興趣的方法。接口隔離的目的是解開耦合,從而容易重構(gòu)更改和重新部署。
接口隔離原則說白了就是讓依賴的接口盡可能的小,看一下上個(gè)例子實(shí)現(xiàn)SD卡緩存的代碼:
/* 將圖片添加到緩存中 */
public void put(String url,Bitmap bitmap){
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if(fileOutputStream != null){
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我們看到這段代碼的可讀性非常的差,各種
try...catch都是非常簡單的代碼,但是會嚴(yán)重影響代碼的可讀性,并且多層級的大括號很容易將代碼寫到錯(cuò)誤的層級中。那么如何解決這樣的問題呢?Java中有一個(gè)Closeable接口,該接口標(biāo)識了一個(gè)可關(guān)閉的對象,它只有一個(gè)close方法。實(shí)現(xiàn)該接口的類有很多,F(xiàn)ileOutputStream也實(shí)現(xiàn)了該接口,當(dāng)程序有有多個(gè)可關(guān)閉的對象時(shí),如果都像上述代碼那樣在finally中去關(guān)閉,就非常的麻煩了。
我們可以抽取一個(gè)工具類來專門去關(guān)閉需要關(guān)閉的對象。
public class CloseUtils {
/**
* 關(guān)閉Closeable對象
* @param closeable
*/
public static void closeQuietly(Closeable closeable){
if(null != closeable){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用工具類替換上述的代碼
public void put(String url,Bitmap bitmap){
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
CloseUtils.closeQuietly(fileOutputStream);
}
}
這樣代碼就簡潔多了,并且CloseUtils可以用到多個(gè)可以關(guān)閉對象的地方,保證了代碼的重用性,它依賴于Closeable抽象而不是具體的實(shí)現(xiàn),并且建立在最小的依賴原則之上,他只需要知道對象是否可關(guān)閉,其他的一概不關(guān)心,這就是接口隔離。如果現(xiàn)在只需要關(guān)閉一個(gè)對象時(shí),它卻暴露了其他的接口方法,比如OutputStream的write方法,這使得更多的細(xì)節(jié)暴露在客戶端面前,還增加了使用難度。而通過Closeable接口將可關(guān)閉的對象抽象起來,這樣客戶端只需要依賴Closeable就可將其他的細(xì)節(jié)隱藏起來,客戶端只需要知道這個(gè)對象可關(guān)閉即可。
在上述的ImageLoader中,只需要知道該緩存對象有讀取和緩存的接口即可,其他的一概不管,這樣緩存的具體實(shí)現(xiàn)是對ImageLoader隱藏的。這就是用最小化接口隔離了實(shí)現(xiàn)類的細(xì)節(jié)。
Robert C Martin在21世紀(jì)早期將單一職責(zé),開閉原則,里氏替換,接口隔離和依賴倒置5個(gè)原則定義為SOLID原則,作為面向?qū)ο缶幊痰?個(gè)基本原則。當(dāng)這些原則在一起使用時(shí),它使得一個(gè)軟件系統(tǒng)更清晰,更簡單,最大程度的擁抱變化。
迪米特原則:更好的可擴(kuò)展性
迪米特原則(英文縮寫為LOD):也稱為最少知識原則,一個(gè)對象應(yīng)該對其他對象有最少的理解。通俗的講,一個(gè)類對自己需要耦合或者調(diào)用的類知道的最少,類的內(nèi)部如果實(shí)現(xiàn)與調(diào)用者或依賴者沒有關(guān)系。調(diào)用者或依賴者只需要知道它調(diào)用的方法即可,其他的一概不知。
下面以租房的例子來理解說明這個(gè)原則。
租房大多數(shù)通過中介來租,我們假設(shè)設(shè)定的情景為:我們只要求房子的面積和租金,其他的一概不管,中介提供給我們符合要求的房子。
public class Room {
public float area;
public float price;
public Room(float area, float price) {
this.area = area;
this.price = price;
}
}
public class Mediator {
private List<Room> mRooms = new ArrayList<>();
public Mediator(){
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(14 + i,(14 + i) * 150));
}
}
public List<Room> getRooms(){
return mRooms;
}
}
public class Tenant {
private float roomArea;
private float roomPrice;
private static final float diffArea = 0.0001f;
private static final float diffPrice = 100.0001f;
public void rentRoom(Mediator mediator){
List<Room> rooms = mediator.getRooms();
for (Room room : rooms) {
if(isSuitable(room)){
System.out.print("租到房子了" + room.toString());
break;
}
}
}
private boolean isSuitable(Room room){
return Math.abs(room.price - roomPrice) < diffPrice
&& Math.abs(room.area - roomArea) < diffArea;
}
}
從上面的代碼看出,
Tenant不僅依賴Mediator,還需要頻繁的與Room打交道,租戶類只需要通過中介找到一間符合要求的房子即可。如果把這些檢索都放在Tenant中,就弱化了中介的作用,而且導(dǎo)致Tenant和Room耦合度較高。當(dāng)Room變化的時(shí)候,Tenant也必須跟著變化,而且Tenant還和Mediator耦合,這樣關(guān)系就顯得有些混亂了。
我們需要根據(jù)迪米特原則進(jìn)行解耦。

public class Mediator {
private List<Room> mRooms = new ArrayList<>();
public Mediator(){
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(14 + i,(14 + i) * 150));
}
}
public Room rentOut(float price,float area){
for (Room room : mRooms) {
if (isSuitable(price,area,room)) {
return room;
}
}
return null;
}
private boolean isSuitable(float price,float area,Room room){
return Math.abs(room.price - price) < Tenant.diffPrice
&& Math.abs(room.area - area) < Tenant.diffArea;
}
}
public class Tenant {
private float roomArea;
private float roomPrice;
public static final float diffArea = 0.0001f;
public static final float diffPrice = 100.0001f;
public void rentRoom(Mediator mediator) {
Room room = mediator.rentOut(roomPrice, roomArea);
if(null != room){
System.out.print("租到房子了" + room.toString());
}
}
}
我們將對Room的操作移到了Mediator中,這本來就是Mediator的職責(zé),根據(jù)租戶的條件檢索符合的房子,并且將房子返回給用戶即可。這樣租戶就不需要知道有關(guān)Room的細(xì)節(jié),比如和房東簽合同,房產(chǎn)證的真?zhèn)蔚取V恍枰P(guān)注和我們相關(guān)的即可。
總結(jié)
在應(yīng)用開發(fā)過程中,我們不僅要完成應(yīng)用的開發(fā)工作,還需要在后續(xù)的升級,維護(hù)中讓應(yīng)用系統(tǒng)能夠擁抱變化。擁抱變化意味著在滿足需求且不破壞系統(tǒng)穩(wěn)定的前提下保持高擴(kuò)展性,高內(nèi)聚,低耦合,在經(jīng)歷了各個(gè)版本變更之后依然保持清晰,靈活,穩(wěn)定的系統(tǒng)架構(gòu)。那么遵守面向?qū)ο蟮牧笤瓌t是我們邁向的第一步。