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