抽象

抽象這個東西,說起來很抽象,其實很簡單。

WHAT

抽象是什麼?按維基百科的說法:“在計算機科學中,抽象化(英語:Abstraction)是將數據與程序以它的語義來呈現出它的外觀,但是隱藏起它的實現細節。”這個定義也許還有些“抽象”,舉幾個例子來看,它其實簡單。

“做技術、如藝術”,計算機中的“抽象”與藝術中的“抽象”頗有異曲同工之妙<br/>

“抽象”在我們的日常工作和生活中比比皆是。例如,我們經常會說,“我是一個開發”,“這事兒你得找產品”,這裏的“開發”、“產品”,都是一種抽象。它們定義了“開發要寫設計、寫代碼、寫單測”、“產品要寫ppt、寫word、寫excel”這樣一種“語義外觀”,但是它們自己並不會寫代碼或寫文檔,這些實現細節隱藏在“職位”之下、由具體的“員工”來完成。

看到職位,我們就能知道這是做什麼的;但具體怎麼做?只有底下的員工最清楚。

在技術上,這樣的例子更是俯拾皆是。例如,Slf4j提供了一個日誌的抽象,它定義了“怎麼打印日誌”這個“語義外觀”,但是它隱藏了實際打印日誌的實現細節——是log4j、還是logback?使用Slf4j時我們是不知道的。還有,Jdbc的Driver、Connection和Statement定義了“怎麼操作數據庫”這個“語義外觀”,但是它也沒有實際去操作數據庫。這些實現細節是由抽象之下的具體實現來處理的。

怎麼打印日誌?Slf4j會告訴你:logger.info(

即使是業務系統中,“抽象”的實例也隨處可見。一個設計良好的接口就是一個業務的抽象,它定義了這項業務支持哪些操作。例如,我們有一個短信簽約接口定義了submit、sendCode、submitCode三個方法,本質上就是定義了“短信簽約操作有三個步驟”這樣一個業務抽象。至於每個步驟都是如何實現的,這是底層邏輯的事情了——實際上,這三個步驟的底層用的是同一個方法。又如,我們有一個凍結訂單的接口,定義了frozenFlow、frozenLimit、forzenTransport三個方法,這也就是定義了“凍結一筆訂單必須凍結Flow、Limit和Transport這三類數據”這樣一個業務抽象。至於這三類數據具體如何凍結麼——就我們系統來說,有的業務是直接邏輯刪除,有的業務是把數據回滾到初始狀態,有的業務則乾脆不需要處理Transport數據——這又是底層邏輯需要考慮的事情了。

所以,抽象是什麼?抽象就是這樣一個東西:它告訴了你自己能做什麼、但不告訴你它是怎麼做的。

就像抽象藝術:就算明白告訴你這是藝術,你也不明白它怎麼就成了藝術了<br/>

WHY

設計出一個好的抽象,除了能隱藏底層實現之外,還有什麼好處嗎?我們爲什麼要在“抽象”這虛的東西上下功夫呢?

借用另一篇文章的話來說:抽象設計得越好,代碼就越簡單易用;代碼可替代性就越好;可擴展性就越好。

簡單易用

爲什麼說抽象設計得越好、代碼就越簡單易用呢?因爲一個好的抽象設計隱藏了它的底層實現,使得我們在使用它的時候,不需要關注底層的細節。就好比開自動擋的車時不用關心離合換擋的事兒,開起來當然比手動擋要簡單方便啦。

手動還是自動?這是一個問題。

例如,我們看看下面這個接口:

public interface QueryService{
    public Bean queryFromRemote(long id);
    public Bean queryFromLocal(long id);
}

這個接口提供了兩個方法,兩個方法的入參、出參都是一模一樣的,區別只在於方法名——以及名字所暗示的,是從“遠程”查詢、還是從“本地”查詢。如果調用方在使用時,確實需要區分數據來源,這個設計倒也無可厚非。但是,實際上調用這兩個方法時,所有的代碼都是這個樣子的:

Bean bean = queryService.queryFromLocal(id);
if(bean == null){
    bean = queryService.queryFromRemote(id);
}
if(bean == null){
    throw new Excepton();
}

這樣的代碼出現了至少五次。囉嗦嗎?囉嗦。麻煩嗎?麻煩。聞着臭嗎?臭。爲什麼每次調用這個接口時都要這麼寫呢?因爲這個接口把自己底層的實現——是從遠程獲取數據、還是從本地獲取數據——暴露出來了。換句話說,這個接口的抽象設計得不夠好。如果我們把這個接口設計成這樣:

public interface QueryService{
    public Beean query(long id);
}

順便,底層這樣實現:

public class QueryServiceImpl{
    public Bean query(log id ){
        Bean bean = queryFromLocal(id);
        if(bean == null){
            bean = queryFromRemote(id);
        }
        if(bean == null){
            throw new RuntimeException();
        }
        return bean;
    }
}

那麼,我們就可以這樣調用這個接口了:

Bean bean = queryService.query(id);

這樣重新設計/實現過之後,使用起來是不是簡單、方便多了?這就是良好的抽象設計的第一個優點。

可替代性

爲什麼說抽象設計得好,代碼的可替代性就越好呢?這同樣是因爲一個好的抽象設計隱藏了它的底層實現,無論我們怎麼更換實現細節,只要對外抽象不變,調用方都不受影響。這就好比我們去銀行櫃檯取錢:只要能把錢正確取出來,櫃員是男是女、是胖是瘦、甚至於是活人還是機器,這都無所謂。

看看,是不是哪個妹子都OK?

我參與設計過一套賬務系統,把所有賬戶間的轉賬操作全部抽象爲這樣一個接口,它所表達的業務含義是:從賬戶from向賬戶to轉入金額amount元,記賬科目是type:

public interface AccountService{
    public void trans(Account from, Account to, Money amount, TransType type);
}

在這個接口的“掩護”下,我們更換過很多種底層實現方式:單邊賬、雙邊賬、會計科目記賬;同步操作、異步操作、批量操作;等等等等。沒有一次變更影響到了接口調用方,最終找到了既能滿足所有業務功能、又提高了處理性能的最佳方案。這就是在好的抽象設計下的代碼可替代性帶來的好處。

也有反面例子。我參與設計過一套Java操作Excel文件的工具,底層用的是POI組件。這套工具的核心接口大概是這個樣子的:

public interface ExcelService<T>{
    public List<T> read(HSSFWorkbook workBook);
}

這個接口的功能,簡單來說就是傳入一個Excel文件、並把其中的數據解析爲對象T。它的主要問題在於:底層實現——也就是HSSFWorkbook——被暴露出來了。這就導致了這個接口只能解析2003版的Excel文件,面對用戶上傳的2007版Excel文件,它就無能爲力了。而且,如果要把工具升級到2007版,所有調用方都必須跟着一起改:在我們的系統裏,這意味着要多修改二十多處代碼、多回歸測試幾十個功能。其中的困難可想而知。

如果這個接口設計得更好,它的底層代碼的可替代性就更高,重構、優化、需求變更時需要修改的地方就更少。改得越少,開發的工作量、加班量就越少,出bug的機率也會更少。

可擴展性

爲什麼說抽象設計得好,代碼的可擴展性就越好呢?這和可替代性有相似之處:根子上還是因爲一個好的抽象設計能隱藏它的底層實現。就像家裏給小孩兒燉湯;媽媽去廚房嚐了一勺,然後多撒了一把蔥花;姥姥又去嚐了一勺,然後多加了點薑片;奶奶又去嚐了一勺,然後多加了點花椒……(最後留給小孩兒的就只剩一勺濃湯寶了哈哈)。

我們有一個查銀行卡列表的接口,客戶端查到列表後,需要根據不同的場景來展示或“置灰”某些卡。例如,劃扣場景下,不支持自動劃扣的卡就必須置灰;解綁定場景下, 跟某些業務綁定的卡就必須置灰;業務綁卡場景下,已經跟該業務綁定的卡就必須置灰……等等等等。

我們爲這個業務所設計的抽象是這樣子的:

public interface CardListService{
    List<Card> query(long userId, Scene scene);}
//核心實現是這樣的
public class CardListServiceImpl{
    private Map<Scene, CardListService> serviceMap;
    public List<Card> query(long userId, Scene scene){
        return serviceMap.get(scene).query(userId, scene);
    }
}
// 返回字段是這樣的
public class Card{
    // 客戶端根據這個字段的值來判斷當前銀行卡是展示還是置灰
    private boolean enabled;
    // 其它卡號、銀行名等字段,和accessor略去
}
// 入參是這樣的
public enum Scene{
     DEDUCT,
    UN_BIND,
    BIND;
}

客戶端不需要關注List&lt;Card&gt;中的銀行卡是不是支持自動劃扣、是不是和某個業務綁定,只需要根據返回結果中的enabled字段來展示或置灰即可。由服務端來根據客戶端傳入的Scene來判斷這些卡是否應當展示。而且,無論哪個Scene下要增加邏輯,或者要增加新的Scene,都只需要服務端做出修改,客戶端是不需要變的。而且即使是服務端,需要修改或增加的代碼量也不大,非常簡單。

簡單易用、可替代和可擴展這些,對於業務系統的重要性有時甚至比對技術中間件還要高。業務系統的一個重要特點,就是業務需求在不停變化、頻繁變化:今天需求是這樣,明天就推翻不做了,後天又重新提出來,大後天再改一版……如果系統的設計實現被需求牽着鼻子走,那開發就有改不完的代碼、加不完的班了。好好地設計一套業務抽象,讓系統和代碼簡單易用、易於替換、易於擴展,纔有可能在少修改代碼、甚至不修改代碼的基礎上去滿足多變的業務需求。開發才能從業務代碼中釋放出來,去提升自己、優化系統。

不加班、不禿頭<br/>

HOW

怎樣設計一個好的抽象呢?其實我們已經有很多方法論/工具箱了:高內聚/低耦合、封裝/繼承/多態、SOLID、設計模式……等等等等,不一而足。只不過以前討論它們的時候,更多地是在“就事論事”地討論它們自身,而並沒有考慮到它們與“抽象”的關係。怎樣從業務抽象的角度去理解和應用這些方法和工具、又怎樣運用它們來建立良好的業務抽象呢?下回分解吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章