封裝

——“面向對象的三大特性是什麼?”
——“封裝、繼承、多態。”

這大概是最容易回答的面試題了。但是,封裝、繼承、多態到底是什麼?它們在面向對象中起到了什麼樣的作用呢?

封裝

“封裝”這個詞是從Encapsulate翻譯過來的,我覺得這個翻譯簡直太妙了:“封裝”,一“封”一“裝”。“封”就是信息隱藏,不想公開的數據、方法都隱藏在對象內部。“裝”就是把數據和方法都放到一個對象中。

webp

↑ 琥珀就是一種非常美的“封裝”。

例如我們最簡單的一個Bean:

public class Data{
    private String idNo;
    
    public Data(String idNo){
        this.idNo = idNo;
    }
    
    public String getIdNo(){
        return this.idNo;
    }
    
    private Date parseBirthday(String idNumber){
        if(idNumber.length() == 15){
            String birthdayStr = idNumber.substring(5,8);
            return DatUtils.parse(birthdayStr,"yyMMdd");
        }else if(idNumber.length() == 18){
            String birthdayStr = idNumber.substring(5,13);
            return DatUtils.parse(birthdayStr,"yyyyMMdd");
        }
        return null;        
    }

    public Date getBirthday(){
        if(this.idNo != null){
            return parseBirthday(idNo);
        }
        return null;
    }
}

在這個Bean中,idNo和getBirthay()/parseBirtdhay(String)就是“裝”在Data這個對象中的數據和方法。而且,idNo和parseBirthday(String)被“封”了在Data對象中,也就是除了它自己之外,其它對象都訪問不到。這也就是“封裝”的最基本的含義:把數據和方法“裝”在一起,並且給其中私密的部分貼上“封”條,不容他人染指。

webp

↑ 還記得這個嗎?裝進去之後要及時貼上封條,不然大魔王就要逃出來了。圖源網。

封裝與面向對象

有了封裝之後,數據和方法才能夠組合在一起。這二者組合在了一起,才能構建出“對象”和“類”來——類是指“一組具有相同屬性和行爲的對象的抽象”,類中的“屬性”就是它的數據,而“行爲”則是指它的方法。類和對象是面向對象的最基本的磚瓦、螺釘。沒有它們,面向對象根本無從談起。就像上面那個Data類:沒有數據idNo,它只能算是一堆函數——也許可以叫做頭文件;沒有那些方法,它只能算是一個全局變量——或者可以叫做結構體。

如果仍用職位與員工的關係來理解面向對象的話,“裝”就是一名員工學習職業技能、準備辦公用品,以完成職位要求的工作。例如,一名員工需要掌握數據結構、算法、編程語言、系統設計等技能,還需要有一臺安裝了編程環境並能訪問StatckOverflow的電腦,才能滿足“開發工程師”的職位要求。

而“封”呢?我相信大多數人在接到工作任務時,都不願意對方來干涉自己要怎麼完成這項工作——開發不會願意讓產品來告訴自己系統怎麼設計、代碼怎麼寫的。在工作對接和彙報時,也一定是“結果導向”,而不會把“我前前後後提交了12次代碼,第一次提交了接口A的功能代碼,第二次提交了接口A的自測代碼……”這種細節都彙報上去。這些做法,其實就是“封”:保證完成工作職責,但是具體怎麼完成的,就交由員工自己靈活處理。

如果不這樣“封裝”,恐怕什麼工作都做不了。腦子裏沒有“裝”上足夠的職業技能,工位上沒有“裝”上必要的工作設備,誰能幹活兒?工作的時候老有人來打擾你,要麼說你的代碼不夠優雅你的顯示器不夠清晰、要麼找你修個打印機修個飲水機,誰能幹活兒?

webp

↑ 近期在家辦公的各位,相信都體會到工作時沒人打擾的重要性了吧!圖源網。

可見,封裝是面向對象的基礎。沒有封裝,就沒有對象,更沒有面向對象。

封裝與抽象

如果只要組成對象,只要能“裝”就可以了,不一定非要把數據和方法“封”起來。但是,要構建一個良好的抽象,光有“裝”就不夠了。抽象要把底層細節隱藏起來,就必須要有“封”。否則,誰都能操作對象的任何數據或者方法,談何“隱藏細節”?如果說“裝”是面向對象的基礎,那麼“封”就是抽象的基礎。

webp

↑ 就像插線板一樣:能“裝”下各種線路、元件,還要能“封”成簡單的三腳插座、兩腳插座。圖源網。

以我們最常見的接口-實現類的爲例:接口定義的就是“抽象”,而實現類就是“細節”。只要嚴格面向接口編程,我們就可以只使用接口,而不用關注接口下到底是哪個實現類:實際上這就是最基本的的一種“封”。

曾有一位同事這樣評價接口-實現類的編程方式:根本找不到使用了哪個實現類。雖然這種方式在寫代碼時會有一點不便,但正是“找不到實現類”的接口隱藏了底層實現“細節”,從而爲業務提供了一個良好的抽象、爲系統留下了充分的可擴展性。

但是,如果不能把實現細節“封”起來,那麼即使使用了接口,也不能算是一個好的“抽象”。例如,我們系統中有這樣一個接口:

public interface PayService{
    PayResult pay(PayReq req);}public class BuyBizImpl implements BuyBiz{
    private PayService payService;
    public void buy(Order order){
        PayReq req = geneartePayReq(order);
        PayResult result = payService.pay(req);
        // 略
    }
}

這是一個支付業務的接口,通過調用一個支付服務來實現業務功能。但是由於某些原因,我們要切換到另一套支付系統中。並且,由於另一些原因,這個切換過程有一個過渡期,在過渡期內有一部分用戶仍然要使用原支付服務。於是,這個接口和它的調用方式就變成了這個樣子:

public interface PayService{
    PayResult pay(PayReq req);
    PayResult newPay(NewPayReq req);
    boolean useNewPay(Order order);}public class BuyBizImpl implements BuyBiz{
    private PayService payService;
    public void buy(Order order){   
        PayResult result;
        if(payService.useNewPay(order)){
            PayReq req = geneartePayReq(order); 
            result = payService.newPay(req);
        }else{
            NewPayReq req = genearteNewPayReq(order); 
            result = payService.pay(req);
        }
        // 略
    }
}

修改後的代碼仍然使用了一個接口來“裝”下新老支付服務的相關功能;但這個接口絲毫沒有起到“封”的作用:它把接口底層的細節完全暴露在了接口之外。這樣的接口並不能稱之爲合格的“抽象”。

封裝與高內聚低耦合

結合高內聚低耦合來理解的話,顯然,“裝”是實現高內聚的手段,而“封”是低耦合的保證。

"高內聚"與"低耦合"是軟件設計和開發中經常出現的一對概念。它們既是做好設計的途徑,也是評價設計好壞的標準。"高內聚"是說,一個業務應當儘量把它所涉及的功能和代碼放到一個模塊中。"低耦合"則是說,一個業務應當儘量減少對其它業務或功能模塊的依賴。
高內聚與低耦合

如果編程語言不支持“裝”,那麼它就很難把“涉及的功能和代碼放到一個模塊中”。而如果編程語言不做好“封”,那麼它也無法阻止不同的業務模塊相互耦合。

webp

↑ 就像麪包板互聯一樣:既不能把相關元件組裝在一起、又不能把無關元件隔離開,最後就只能亂成一團。

例如,Java開發規範禁止把對象屬性的可見性設置爲public,就是通過“封”住對象屬性來降低代碼間的耦合度。我們可以看看一個簡單、但是真實的例子:

public class User{
    public int age;
    public String idNo;
    // 其它,略}public class InsuranceService{
    public Insurance apply(User user){
        if(user.age <= 18 or user.age >= 80){
            throw new BizException();
        }
        // 其它,略
    }
}

這是我剛參加工作時寫的一段代碼。在上面的代碼中,User類直接把自己內部的數據age暴露在外;而外部服務也毫不客氣地直接使用了這個字段——並且這樣的地方還有很多處。

一切看起來都很好。直到有一天,我們發現用戶會爲了繞過業務上對年齡的限制而謊報年齡。爲了解決這個問題,我們需要修改用戶年齡的取值方式:由用戶自己填寫年齡改爲用戶只填寫身份證號、系統根據身份證號計算出他的年齡。這個需求本來不大,何況系統中本來就採集並且校驗了用戶的身份證號。

但是這個小小的需求卻帶來了系統代碼上的巨大變化,並導致了開發和測試的工作量暴漲,還由於代碼的改動和測試不到位引發了若干個線上bug……這一切的罪魁禍首,就是User中的public int age與外部功能模塊之間的高度耦合。如果當初能夠把這個字段“封”起來、把操作age的方法“封”起來,這個需求只要不到十行代碼就能搞定、半個小時就能開發完、可以100%測試覆蓋、更不可能出什麼線上bug:

public class User{
    private int age;
    private String idNo;
    public int getAge(){
        // 這是原先的取值方法
        // return this.age;
        // 這是修改後的取值方法,“不到十行”=。=
        return IdCardUtils.parseAge(this.idNo);
    }}public class InsuranceService{
    public Insurance apply(User user){
        if(user.getAge() <= 18 or user.getAge() >= 80){
            throw new BizException();
        }
        // 其它,略
    }
}

webp

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