導語
讓你的代碼更加優美。
主要內容
- 單一職責原則——優化代碼的第一步
- 開閉原則——讓程序更穩定靈活
- 里氏替換原則——構建擴展性更好的系統
- 依賴倒置原則——讓項目擁有變化的能力
- 接口隔離原則——讓系統有更高的靈活性
- 迪米特原則——更好的可擴展性
具體內容
單一職責原則
單一職責原則的英文名名稱是Single Responsibility Principle,縮寫是SRP。SRP的定義是:就一個類而言,應該僅有一個引起它變化的原因。簡單來說,一個類中應該是一組相關性很高的函數、數據的封裝。
比如Pear是一家電子產品商,它要生產pad,phone,watch等設備,但是有一些重復的功能,如果分別設計一套,很顯然并不劃算,那么接口定義上我們就可以根據功能劃分設定單一職責的接口:
接口的定義
//可以撥打電話
interface Callable{
void call ();
}
//可以觸摸控制
interface Touchable{
void touch();
}
//可以消息提醒
interface MessagePromptable{
void prompt();
}
//可以接入鍵盤
interface KeyBoardMatchable{
void match();
}
實現接口的類依舊單一職責
class StandardCall implements Callable{
@Override
public void call() {
System.out.println("Call to somebody!");
}
}
class StandardTouch implements Touchable{
@Override
public void touch() {
System.out.println("touch to press the button!");
}
}
class StandardPromt implements MessagePromptable{
@Override
public void prompt() {
System.out.println(" someone contact to you,sir!");
}
}
class StandardMatch implements KeyBoardMatchable{
@Override
public void match() {
System.out.println("The keyBoard is ready to work!");
}
}
產品的生產
我們如果基于我們現有的技術生產一部手機,那么我們需要它能打電話,觸屏控制和消息提醒:
//在聲明這臺手機時我們就明確知道了它的功能
class MyPhone implements Callable, MessagePromptable, Touchable{
//無需重復研發已有的技術,直接裝載即可
private Callable caller = new StandardCall();
private MessagePromptable prompter = new StandardPromt();
private Touchable toucher = new StandardTouch();
@Override
public void call() {
caller.call();
}
@Override
public void prompt() {
prompter.prompt();
}
@Override
public void touch() {
toucher.touch();
}
}
public class SRPTest {
public static void main ( String [] args ){
MyPhone phone = new MyPhone();
phone.call();
phone.prompt();
phone.touch();
}
}
假如我們需要出一款新的手機,但是我們只是擁有了新的呼叫技術,那么只需要在實現這項技術時繼承Callable接口,然后在之前手機new的Callable的具體實例換成新的技術即可,只需要修改一行代碼,是不是感覺棒棒的。職責的單一,對于我們對于現有類的修改造成的影響有了約束。
那么如果我想生產一個Pad呢,同理啊,只需要在已有技術上裝載即可啊,Pad類依舊只是單一的整合技術形成產品的職責,整合成產品和研發出技術的職責分離,為我們的類的拓展帶來了方便。
class MyPad implements Touchable,KeyBoardMatchable{
Touchable toucher = new StandardTouch();
KeyBoardMatchable matcher = new StandardMatch();
@Override
public void match() {
toucher.touch();
}
@Override
public void touch() {
matcher.match();
}
}
下面一個例子,我們的接口依舊單一職責,但是接聽和撥打電話的功能往往是不可分的,他們會同時發生變化,所以我們可以提供一個同時繼承兩個接口的實現類。
class CallAndPrompt implements Callable,MessagePromptable{
@Override
public void call() {
System.out.println("Hello, I have some thing to tell you!");
}
@Override
public void prompt() {
System.out.println("Hello,what do you want to tell me!");
}
}
//在聲明這臺手機時我們就明確知道了它的功能
class MyPhone implements Callable,MessagePromptable,Touchable{
//無需重復研發已有的技術,直接裝載即可
private Callable caller = new CallAndPrompt();
//不同的接口調用同一個實現類的不同功能
private MessagePromptable prompter = (MessagePromptable)caller;
private Touchable toucher = new StandardTouch();
@Override
public void call() {
caller.call();
}
@Override
public void prompt() {
prompter.prompt();
}
@Override
public void touch() {
toucher.touch();
}
}
開閉原則
開閉原則的英文全稱是Open Close Principle,縮寫是OCP,它是Java世界里最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對于擴展是開放的,但是,對于修改是封閉的。
優點:按照OCP原則設計出來的系統,降低了程序各部分之間的耦合性,其適應性、靈活性、穩定性都比較好。當已有軟件系統需要增加新的功能時,不需要對作為系統基礎的抽象層進行修改,只需要在原有基礎上附加新的模塊就能實現所需要添加的功能。增加的新模塊對原有的模塊完全沒有影響或影響很小,這樣就無須為原有模塊進行重新測試。
如何實現“開-閉”原則
在面向對象設計中,不允許更改的是系統的抽象層,而允許擴展的是系統的實現層。換言之,定義一個一勞永逸的抽象設計層,允許盡可能多的行為在實現層被實現。
解決問題關鍵在于抽象化,抽象化是面向對象設計的第一個核心本質。
對一個事物抽象化,實質上是在概括歸納總結它的本質。抽象讓我們抓住最最重要的東西,從更高一層去思考。這降低了思考的復雜度,我們不用同時考慮那么多的東西。換言之,我們封裝了事物的本質,看不到任何細節。
在面向對象編程中,通過抽象類及接口,規定了具體類的特征作為抽象層,相對穩定,不需更改,從而滿足“對修改關閉”;而從抽象類導出的具體類可以改變系統的行為,從而滿足“對擴展開放”。
對實體進行擴展時,不必改動軟件的源代碼或者二進制代碼。關鍵在于抽象。
接口的定義
public interface ImageCache {
public Bitmap get(String url);
public void put(String url, Bitmap bmp);
}
實現接口
// 內存緩存MemoryCache類
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mMemeryCache;
public MemoryCache() {
// 初始化LRU緩存
}
@Override
public Bitmap get(String url) {
return mMemeryCache.get(url);
}
@Override
public void put(String url, Bitmap bmp) {
mMemeryCache.put(url, bmp);
}
}
// SD卡緩存DiskCache類
public class DiskCache implements ImageCache {
@Override
public Bitmap get(String url) {
// 改為從本地文件獲取圖片
return null;
}
@Override
public void put(String url, Bitmap bmp) {
// 將Bitmap寫入文件中
}
}
// 雙緩存DoubleCache類
public class DoubleCache implements ImageCache {
ImageCache mMemoryCache = new MemoryCache();
ImageCache mDiskCache = new mDiskCache();
// 先從內存中獲取圖片,如果沒有再從SD卡中獲取
public Bitmap get(String url) {
public Bitmap bitmap = mMemoryCache.get(url);
if(bitmap == null) {
bitmap = mDiskCache.get(url);
}
return bitmap;
}
// 將圖片緩存到內存和SD卡中
public void put(String url, Bitmap bmp) {
mMemoryCache.put(url, bmp);
mDiskCache.put(url, bmp);
}
}
實現圖片加載器類
public class ImageLoader {
// 圖片緩存,并設置了內存緩存為默認方式
private ImageCache mImageCache = new MemoryCache();
// 注入緩存實現 利用了向上轉型
public void setImageCache(ImageCache imageCache) {
mImageCache = imageCache;
}
public void displayImage(String imageUrl, ImageView imageView) {
Bitmap bitmap = mImageCache.get(imageUrl);
if(bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
// 圖片沒有緩存,提交到線程池中下載圖片
submitLoadRequest(imageUrl, imageView)
}
public void submitLoadRequest(final String imageUrl, final ImageView imageView) {
// 下載圖片
// 緩存
mImageCache.put(imageUrl, bitmap);
}
// 省略其他成員變量和方法
}
調用方法
// 使用方法 只是通過傳入不同實現就可以切換緩存方式
ImageLoader loader = new ImageLoader();
loader.setImageCache(new MemoryCache()); // 使用內存緩存
loader.setImageCache(new DiskCache()); // 使用SD卡緩存
loader.setImageCache(new DoubleCache()); // 使用雙緩存
// 使用自定義的圖片緩存實現
loader.setImageCache(new ImageCache() {
@Override
public Bitmap get(String url) {
// 改為從本地文件獲取圖片
return null;
}
@Override
public void put(String url, Bitmap bmp) {
// 將Bitmap寫入文件中
}
});
我們看得到通過setImageCache(ImageCache imageCache) 方式注入不同的緩存實現,使得ImageLoader代碼變得更簡單,健壯,提升高了它的靈活性和可擴展性,如果還有還有新的緩存方式,只需要去實現ImageCachej接口就可以使用了。
所以當需求發生變化時,應該盡量通過擴展的方式來實現變化,而不是通過修改已有代碼來實現,但要做到開閉原則,首先我們應該先寫出更易擴展的代碼。
里氏替換原則
里氏替換原則英文全稱是Liskov Substitution Principle,縮寫是LSP。LSP的第一種定義是:如果對每一個類型為S的對象O1,都有類型為T的對象O2,使得以T定義的所有程序P在所有的對象O1都代換成O2時,程序P的行為沒有發生變化,那么類型S是類型T的子類型。
或者說是:所有引用基類的地方必須能透明地使用其子類的對象。
就像開閉原則中舉的例子,創建了一個ImageCache,而其他緩存類都是他的實現類,而setImageCache(ImageCache imageCache) 需要的就是ImageCache類型,這時候我們就可以使用MemoryCache,DiskCache,DoubleCache來替換ImageCache的工作。ImageCache確定了規范,而新的緩存需求都可以通過實現它然后替換ImageCache來工作,從而保證了可擴展性。
也可以看一下系統代碼
// 窗口類
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 Button extends View {
public void draw() {
// 繪制按鈕
}
}
// TextView的具體實現
public class TextView extends View {
public void draw() {
// 繪制文本
}
}
故里氏替換原則就是通過建立抽象,建立規范,然后在運行時通過具體實現來替換掉抽象,從而保證了系統的擴展性和靈活性。可見,在開發過程中運用抽象是走向代碼優化的重要一步。
開閉原則和里氏替換原則往往都是一同出現的,通過里氏替換原則達到對擴展的開發,對修改關閉的效果。
依賴倒置原則
依賴倒置原則英文全稱是Dependence Inversion Principle,縮寫是DIP。依賴倒置原則指代了一種特定的解耦形式,使得高層次的模塊不依賴于低層次的模塊的實現細節的目的,依賴模塊被顛倒了。
依賴倒置原則的三個關鍵點:
- 高層次模塊不應該依賴于底層模塊,兩者都應該依賴其抽象。
- 抽象不應依賴細節。
- 細節應該依賴抽象。
抽象就是指接口或者抽象類;細節就是實現類;高層模塊就是調用端,低層模塊就是具體實現類。
依賴倒置原則在Java中表現就是:模塊間依賴是通過抽象發生的,實現類之間并不產生直接依賴關系,其依賴關系是通過接口或抽象類產生的。
一句話概括:面向接口編程,或者說面向抽象編程。
我們依然可以通過上面的例子繼續說明,代碼如下:
// 如果在ImageLoader中直接這樣寫的話
// 就是直接依賴于細節(直接依賴實現類)
private DoubleCache mCache = new DoubleCache();
public void setImageCache(DoublieCache cache) {
mCache = cache;
}
而我們的代碼卻直接完成1.2.3.4這四個原則
// 依賴于抽象,通過向上轉型,有一個默認的實現類
private ImageCache mImageCache = new MemoryCache();
// 設置緩存策略,依賴于抽象
public void setImageCache(ImageCache imageCache) {
mImageCache = imageCache;
}
依賴于抽象,依賴于基類,這樣當需求發生變化,只需要實現ImageCache或者繼承已實現的之類都可以完成緩存功能,然后將實現注入到setImageCache(ImageCache imageCache)就可以了。
接口隔離原則
接口隔離原則英文全稱是InterfaceSegregation Principles,縮寫是ISP。ISP的定義是:客戶端不應該依賴它不需要的接口。或者說類的依賴關系應該將在最小的接口上。
接口隔離的目的是系統接口耦合,從而容易重構、更改和重新部署。一句話:讓客戶端依賴的接口盡可能小。
舉一個例子,當我們在使用流的時候我們需要在finally中判斷是否為空,如果不為空需要close()它,但每次使用流,都這么寫,也會讓代碼變得不優美,這個時候我們考慮借助外力,就比如Java為我們提供了一個Closeable接口,而它有100多個實現類,所以那些類都可以使用它,代碼如下:
// 這就是修改之前的代碼 try/catch中還有try/catch
FileOutputStream fileOutputStream = null;
try {
// 邏輯省略
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 寫了個CloseUtil類,然后西面提供這個靜態方法,所有實現了Closeable的類都可以調用這個方法
public static void closeQuietly (Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 我們只需要在finally中調用這一句話就好了
CloseUtil.closeQuietly(xxx);
不僅讓代碼的可讀性增加了,還保證了它的重用性,這里也用到了依賴倒置原則,closeQuietly()方法的參數就一個抽象,做到了我只需要知道這個對象是可關閉的,其他一概不管辛,也就是作者所說的接口隔離原則。
迪米特原則
迪米特原則英文全稱為Law of Demeter,縮寫是LOD,也稱為最少知識原則(Least Knowledge Principle)。LOD的定義是:一個對象應該對其他對象有最少的了解。
通俗的講,一個類應該對自己需要耦合或者調用的類知道的最少,類的內部如何實現與調用者或者依賴者沒有關系,只需要知道它需要的方法即可,其他的一概不管,類與類之間的關系越密切,耦合度也就越大。
迪米特原則還有一個英文解釋:Only talk to your immediate friends.翻譯過來也就是說之與直接朋友進行通信。
還是前面的ImageLoder,緩存這塊是已經搞定了。假如在某次加載圖片中,,緩存沒找到就需要聯網去服務器拿圖片,并且需要存到緩存中以備下次直接從緩存加載,ok,很快可以寫出這樣的代碼:
public class ImageLoder {
private ImageCache mImageCache = new DoubleCache();
//...
public void dispalyImage(String url, ImageView imageView) {
Bitmap bitmap = mImageCache.get(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
HttpImage4Service.down4Service(url, imageView, mImageCache);
}
}
HttpImage4Service下載類中從網絡中加載圖片方法:
public static void down4Service(String url, ImageView imageView, ImageCache imageCache) {
//...從網絡拉取圖片
//回調↓
imageView.setImageBitmap(bitmap4Service);//顯示圖片
imageCache.put(url, bitmap4Service);//存到緩存中
}
分析下這樣設計的耦合情況,
- ImageLoder調用ImageCache和HttpImage4Service。
- HttpImage4Service調用ImageCache。
三個類之間是否知道的最少?試想一下,從網絡拉取圖片跟緩存這樣兩個類應該有關聯嗎?實際上是沒必要的,根據最少知識原則,改進之后應該是下面這樣的:
public class ImageLoder {
private ImageCache mImageCache = new DoubleCache();
//...
public void dispalyImage(String url, ImageView imageView) {
Bitmap bitmap = mImageCache.get(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
bitmap=HttpImage4Service.down4Service(url);//只負責下載圖片
imageView.setImageBitmap(bitmap);
imageCache.put(url, bitmap);//存到緩存中
}
}
改進后三個類中只有ImageLoder調用HttpImage4Service和ImageCache中的方法,其余沒有任何調用關系,耦合度降低。
在MVP中,View層和Model層拒絕通信,也是符合最少知識原則的,達到降低耦合效果,同時可擴展性會大大增加。
總結
應用開發,最難的不是完成開發工作,而是維護和升級。為了后續能夠很好的維護和升級,我們的系統需要在滿足穩定性的前提下保持以下三個特性:
- 高可擴展性
- 高內聚
- 低耦合