都說面向對象編程,偷偷告訴你面向對象編程的6大設計原則

都說面向對象編程,偷偷告訴你面向對象編程的6大設計原則

最近在閱讀《Android源碼設計模式解析與實戰》一書,我覺得寫的很清晰,每一個知識點都有示例,通過示例更加容易理解。書中的知識點有些都接觸過,有的沒有接觸過,總之,通過閱讀這本書來梳理一下知識點,可能有些東西在項目中一直在使用,然並不能籠統,清理的說明理解它。本文主要是記錄閱讀這本書的知識點和自己的一些理解。一來整理知識點,二來方便以後查看,快速定位。

單一職責原則 :優化代碼第一步

單一職責原則(英文簡稱:SRP):對於一個類而言,應該僅有一個引起它變化的原因。這個有點抽象,因為該原則的劃分界面並不是那麼清晰,很多時候靠個人經驗來區分。簡單來說就是一個類只負責一個功能,比如加減乘除應分別對應一個類,而不是把四個功能放在一個類中,這樣在只要有一個功能變化都需要更改這個類。

下面以實現一個圖片加載器(ImageLoader)來說明:

public class ImageLoader {

//圖片內存緩存

private LruCache 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(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拆分一下,把各個功能獨立出來。

都說面向對象編程,偷偷告訴你面向對象編程的6大設計原則

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 mImageCache;

public ImageCache(){

initImageCache();

}

//初始化

private void initImageCache() {

//計算最大的可使用內存

final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

//取四分之一作為最大緩存內存

final int cacheSize = maxMemory / 4;

mImageCache = new LruCache(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條件判斷的,條件太多,是很容易寫錯的。而且代碼會越來越臃腫,並且可擴展性差。可擴展性是框架的重要特性之一。

根據開閉原則,當軟件需求改變的時候,我們應該通過擴展的方式實現,而不是修改自己的代碼。對上述代碼進行優化:

都說面向對象編程,偷偷告訴你面向對象編程的6大設計原則

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的關係的例子來理解里氏替換原則:

都說面向對象編程,偷偷告訴你面向對象編程的6大設計原則

//窗口類

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 mRooms = new ArrayList<>();

public Mediator(){

for (int i = 0; i < 5; i++) {

mRooms.add(new Room(14 + i,(14 + i) * 150));

}

}

public List 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 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變化的時候,Tenant也必須跟著變化,而且Tenant還和Mediator耦合,這樣關係就顯得有些混亂了。

我們需要根據迪米特原則進行解耦。

都說面向對象編程,偷偷告訴你面向對象編程的6大設計原則

public class Mediator {

private List 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的細節,比如和房東籤合同,房產證的真偽等。只需要關注和我們相關的即可。

總結

在應用開發過程中,我們不僅要完成應用的開發工作,還需要在後續的升級,維護中讓應用系統能夠擁抱變化。擁抱變化意味著在滿足需求且不破壞系統穩定的前提下保持高擴展性,高內聚,低耦合,在經歷了各個版本變更之後依然保持清晰,靈活,穩定的系統架構。那麼遵守面向對象的六大原則是我們邁向的第一步。


分享到:


相關文章: