面向物件六大設計原則
最新在閱讀《Android原始碼設計模式解析與實戰》一書,我覺得寫的很清晰,每一個知識點都有示例,通過示例更加容易理解。書中的知識點有些都接觸過,有的沒有接觸過,總之,通過閱讀這本書來梳理一下知識點,可能有些東西在專案中一直在使用,然並不能籠統,清理的說明理解它。本文主要是記錄閱讀這本書的知識點和自己的一些理解。一來整理知識點,二來方便以後檢視,快速定位。
單一職責原則 :優化程式碼第一步
單一職責原則(英文簡稱:SRP):對於一個類而言,應該僅有一個引起它變化的原因。這個有點抽象,因為該原則的劃分介面並不是那麼清晰,很多時候靠個人經驗來區分。簡單來說就是一個類只負責一個功能,比如加減乘除應分別對應一個類,而不是把四個功能放在一個類中,這樣在只要有一個功能變化都需要更改這個類。
下面以實現一個圖片載入器(ImageLoader)來說明:
public class ImageLoader {
//圖片記憶體快取
private LruCache<String,Bitmap> mImageCache;
//執行緒池,執行緒數量為CPU的數量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public ImageLoader(){
initImageLoader();
}
//初始化
private void initImageLoader() {
//計算最大的可使用記憶體
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取四分之一作為最大快取記憶體
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);
}
});
}
}
我們一般都會這樣這樣簡單的實現一個圖片載入工具類,這樣寫功能雖然實現了,但是程式碼是有問題的,程式碼耦合嚴重,隨著ImageLoader功能越來越多,這個類會越來越大,程式碼越來越複雜。按照單一職責原則,我們應該把ImageLoader拆分一下,把各個功能獨立出來。
ImageLoader修改程式碼如下:
public class ImageLoader {
//圖片快取
ImageCache mImageCache = new ImageCache();
//執行緒池,執行緒數量為CPU的數量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public void displayImage(final String url, final ImageView imageView){
.............
}
}
public class ImageCache {
//圖片記憶體快取
private LruCache<String,Bitmap> mImageCache;
public ImageCache(){
initImageCache();
}
//初始化
private void initImageCache() {
//計算最大的可使用記憶體
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取四分之一作為最大快取記憶體
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只負責圖片載入的邏輯,ImageCache負責快取策略,這樣,ImageLoader的程式碼變少了,職責也清晰了,並且如果快取策略改變了的話,只需要修改ImageCache而不需要在修改ImageLoader了。從這個例子可以更加清晰的理解什麼是單一職責原則,如何去劃分一個類的職責,每個人的看法不同,這需要根據個人的經驗,業務邏輯而定。
開閉原則:讓程式更穩定,更靈活
開閉原則(英文縮寫為OCP): 軟體中的物件(類,函式,模組等)對於擴充套件是開放的,但是對於修改是封閉的。在軟體的生命週期內,因為變化,升級和維護的原因需要對原始碼修改時,可能會將錯誤引入已經經過測試的舊程式碼,破壞原有的系統。因為當需求變化時,我們應儘可能的通過擴充套件來實現,而不是修改原來的
程式碼。
在實際的開發過程中,只通過繼承的方式來升級,維護原有的系統只是一個理想化的狀態,修改原始碼,擴充套件程式碼往往是同時存在的。我們應儘可能的影響原始碼。避免引入的錯誤造成系統的破壞。
還是上面的那個例子,雖然通過記憶體快取解決了每次都從網路下載圖片的問題,但是Android記憶體有限,並且當應用重啟後記憶體快取會丟失。我們需要修改一下,增加SD卡快取,程式碼如下:
public class ImageLoader {
//記憶體快取
ImageCache mImageCache = new ImageCache();
//SD卡快取
DiskCache mDiskCache = new DiskCache();
//執行緒池,執行緒數量為CPU的數量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public void displayImage(final String url, final ImageView imageView){
//先從記憶體快取中讀取,如果沒有再從SD卡中讀取
Bitmap bitmap = mImageCache.get(url);
if(bitmap == null){
bitmap = mDiskCache.get(url);
}
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
//從網路下載圖片
..........
}
}
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卡快取,我們在顯示圖片的時候先判斷記憶體快取中是否存在如果不存在就在SD卡中找,否則再從網路下載,這樣就會有一個問題,每增加一個新的快取方法,我們都需要修改原來的程式碼,這樣可能引入Bug,而且會使原來的程式碼越來越複雜,還有使用者也不能自定義快取方法。我們具體使用哪一種快取方法是通過
if
條件判斷的,條件太多,是很容易寫錯的。而且程式碼會越來越臃腫,並且可擴充套件性差。可擴充套件性是框架的重要特性之一。
根據開閉原則,當軟體需求改變的時候,我們應該通過擴充套件的方式實現,而不是修改自己的程式碼。對上述程式碼進行優化:
public class ImageLoader {
//預設快取方式為記憶體快取
ImageCache mImageCache = new MemoryCache();
//執行緒池,執行緒數量為CPU的數量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
//設定快取方式
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;
}
//網路下載圖片
............
}
}
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}
通過上述程式碼我們可以看出,ImageLoader增加了一個方法
setImageCache
,我們可以通過該方法設定快取方式,這就是我們常說的依賴注入。當然我們還可以自定義自己的快取方式,只需要實現ImageCache
這個介面即可。然後再呼叫setImageCache
這個方法來設定。而不需要修改ImageLoader
的程式碼。這樣當快取需求改變的時候我們可以通過擴充套件的方式來實現而不是修改的方法,這就是所說的開閉原則。同時是ImageLoader
的程式碼更簡潔,擴充套件性和靈活性也更高。
里氏替換原則:構建擴充套件性更好的系統
里氏替換原則(英文縮寫為LSP):所有引用基類的地方都必須能夠透明的使用其子類的物件。我們知道面向物件有三大特性:封裝,繼承和多型,里氏替換原則就是依賴繼承和多型這兩大原則,里氏替換原則簡單來說就是:只要是父類引用出現的地方都可以替換成其子類的物件,並且不會產生任何的錯誤和異常。
下面以Android中的Window和View的關係的例子來理解里氏替換原則:
//視窗類
public class Window {
public void show(View child){
child.draw();
}
}
建立檢視抽象類,測量檢視的寬高為公共程式碼,繪製交給具體的子類去實現
public abstract class View {
public abstract void draw();
public void measure(int width,int height){
//測量檢視大小
...........
}
}
//文字類具體實現
public class TextView extends View {
@Override
public void draw() {
//繪製文字
...........
}
}
//按鈕類具體實現
public class Button extends View {
@Override
public void draw() {
//繪製按鈕
.............
}
}
上述示例中,Window依賴於View,View定義了一個檢視抽象,
measure
是各個子類共享的方法,子類通過重寫View的draw
方法來實現具體各自特色的內容。任何繼承View的子類都可以設定給show
方法,這就是所說的里氏替換原則,通過裡式替換,就可以自定義各種各樣的,千變萬化的View,然後傳遞給Window,Window負責組織View,並將View顯示到螢幕上。
上面ImageLoader的例子也體現了里氏替換原則,可以通過setImageCache
方法來設定各種各樣的快取方式,如果 setImageCache
中的cache物件不能被子類替換,那麼又怎麼能設定各種各樣的快取方式呢?
依賴倒置原則:讓專案擁有變化的能力
依賴倒置原則(英文縮寫為DIP)指代了一種特定的解耦方式,使得高層次的模組不依賴於低層次模組的實現細節的目的,依賴模組被顛倒了。這個概念更加的抽象,該怎麼理解呢?
依賴倒置原則有幾個關鍵的點:
* 1.高層模組不應該依賴底層模組,兩者都應該依賴其抽象。
* 2.抽象不應該依賴細節。
* 3.細節應該依賴抽象。
在Java語言中,抽象就是介面或者抽象類,兩者都是不能直接被例項化的;細節就是實現類,實現介面或者繼承抽象類而產生的類就是細節,可以直接例項化;高層模組就是呼叫端;底層模組就是實現端。
依賴倒置原則在Java語言中的表現就是:模組間的依賴通過抽象發生,實現類直接不能直接發生依賴,其依賴關係是通過介面或者抽象類產生的。
如果類與類之間直接依賴於細節,那麼它們之間就有直接的耦合,當需求變化的時候,意味著要同時修改依賴者的程式碼。這就限制了系統的可擴充套件性。
public class ImageLoader {
//直接依賴於細節
MemoryCache mImageCache = new MemoryCache();
...................
}
ImageLoader
直接依賴於MemoryCache
,MemoryCache
是一個具體的實現,這就導致ImageLoader
直接依賴於細節,當MemoryCache
不能滿足而被其他快取實現替換時,就必須需要修改ImageLoader
的程式碼。
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}
public class ImageLoader {
//依賴於抽象,並且有一個預設的實現
ImageCache mImageCache = new MemoryCache();
......................
在這裡我們建立了
ImageCache
抽象,並且讓ImageLoader
直接依賴於抽象而不是具體的細節,當需求變化時,只需要實現ImageCache
或者繼承已有的類來完成相應的快取功能。然後再將具體的實現注入到ImageLoader
中,保證了系統的高擴充套件性。這就是依賴倒置原則。
介面隔離原則:讓系統擁有更高的靈活性
介面隔離原則(英文縮寫為LSP):客戶端不應該依賴它不需要的介面。另外一種定義是:類間的依賴關係應該建立在最小的介面上。介面隔離原則將龐大,臃腫的介面拆分成更小更具體的介面,這樣客戶端只需要知道它感興趣的方法。介面隔離的目的是解開耦合,從而容易重構更改和重新部署。
介面隔離原則說白了就是讓依賴的介面儘可能的小,看一下上個例子實現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
都是非常簡單的程式碼,但是會嚴重影響程式碼的可讀性,並且多層級的大括號很容易將程式碼寫到錯誤的層級中。那麼如何解決這樣的問題呢?Java中有一個Closeable介面,該介面標識了一個可關閉的物件,它只有一個close方法。實現該介面的類有很多,FileOutputStream也實現了該介面,當程式有有多個可關閉的物件時,如果都像上述程式碼那樣在finally中去關閉,就非常的麻煩了。
我們可以抽取一個工具類來專門去關閉需要關閉的物件。
public class CloseUtils {
/**
* 關閉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可以用到多個可以關閉物件的地方,保證了程式碼的重用性,它依賴於Closeable抽象而不是具體的實現,並且建立在最小的依賴原則之上,他只需要知道物件是否可關閉,其他的一概不關心,這就是介面隔離。如果現在只需要關閉一個物件時,它卻暴露了其他的介面方法,比如OutputStream的write方法,這使得更多的細節暴露在客戶端面前,還增加了使用難度。而通過Closeable介面將可關閉的物件抽象起來,這樣客戶端只需要依賴Closeable就可將其他的細節隱藏起來,客戶端只需要知道這個物件可關閉即可。
在上述的ImageLoader中,只需要知道該快取物件有讀取和快取的介面即可,其他的一概不管,這樣快取的具體實現是對ImageLoader隱藏的。這就是用最小化介面隔離了實現類的細節。
Robert C Martin在21世紀早期將單一職責,開閉原則,里氏替換,介面隔離和依賴倒置5個原則定義為SOLID原則,作為面向物件程式設計的5個基本原則。當這些原則在一起使用時,它使得一個軟體系統更清晰,更簡單,最大程度的擁抱變化。
迪米特原則:更好的可擴充套件性
迪米特原則(英文縮寫為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 {
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
中,就弱化了中介的作用,而且導致Tenant
和Room
耦合度較高。當Room
變化的時候,Tenan
t也必須跟著變化,而且Tenant
還和Mediator
耦合,這樣關係就顯得有些混亂了。
我們需要根據迪米特原則進行解耦。
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的職責,根據租戶的條件檢索符合的房子,並且將房子返回給使用者即可。這樣租戶就不需要知道有關Room的細節,比如和房東籤合同,房產證的真偽等。只需要關注和我們相關的即可。
總結
在應用開發過程中,我們不僅要完成應用的開發工作,還需要在後續的升級,維護中讓應用系統能夠擁抱變化。擁抱變化意味著在滿足需求且不破壞系統穩定的前提下保持高擴充套件性,高內聚,低耦合,在經歷了各個版本變更之後依然保持清晰,靈活,穩定的系統架構。那麼遵守面向物件的六大原則是我們邁向的第一步。