1. Clean Code - 對象和數(shù)據(jù)結(jié)構(gòu)

        共 7202字,需瀏覽 15分鐘

         ·

        2022-03-26 14:33

        1、數(shù)據(jù)抽象

        不曝露數(shù)據(jù)細節(jié),更愿意以抽象形態(tài)表述數(shù)據(jù)。

        代碼 1

        public?class?Point{
        ????public?double?x;
        ????public?double?y;
        }

        代碼 2

        public?interface?Point{
        ????double?getX();
        ????double?getY();
        ????void?setCartesian(double?x,?double?y);
        ????
        ????double?getR();
        ????double?getTheta();
        ????void?setPolar(double?r,?double?theta);
        }

        上面兩段代碼都表示笛卡爾兒平面上的一個點,你覺得哪個代碼更好些?

        答案是代碼 2,為什么代碼 2 更好一些,原因有以下兩個方面。

        首先代碼 1 沒有封裝。
        代碼 1 中的 x 和 y 是完全暴露的,任何人都可以直接訪問和設(shè)置新的值。
        代碼 2 中加了一層封裝,你只能通過get方法獲取坐標值和set方法設(shè)置原子坐標值。

        其次是具象與抽象。
        代碼 1 是具象的一個點,只能表示在直角坐標系中的一個點。
        代碼 2 是抽象的一個點,既可以表示直角坐標系中的一個點,也可表示極坐標系中的一個點。

        2、數(shù)據(jù)、對象的反對稱性(過程式代碼和面向?qū)ο蟠a的反對稱性)

        我們用一個例子來說明這個規(guī)則的含義。

        現(xiàn)在我們有一個需求,需要繪制三種幾何圖形,分別是正方形、長方形和圓形,并計算每個圖形的面積。

        代碼 1 是過程式代碼,每個形狀類都是簡單的數(shù)據(jù)結(jié)構(gòu),只負責存儲數(shù)據(jù),不具備行為。具體的計算行為放在了 Geometry 類中。

        代碼 1

        public?class?Square{
        ????public?Point?topLeft;
        ????public?double?side;
        }

        public?class?Rectangle{
        ????public?Point?topLeft;
        ????public?double?width;
        ????public?double?height;
        }

        public?class?Circle{
        ????public?Point?center;
        ????public?double?radius;
        }

        public?class?Geometry{
        ????public?final?double?PI?=?3.14159265358;
        ????
        ????public?double?area(Object?shape)?throws?NoSuchShapeException{
        ????????if(shape?instanceof?Square){
        ????????????Square?square?=?(Square)shape;
        ????????????return?square.side?*?square.side;
        ????????}?else?if?(shape?instanceof?Rectangle){
        ????????????Rectangle?rec?=?(Rectangle)shape;
        ????????????return?rec.width?*?rec.hight;
        ????????}?else?if?(shape?instanceof?Circle){
        ????????????Circle?cir?=?(Circle)shape;
        ????????????return?PI?*?cir.radius?*?cir.radius;
        ????????}
        ????????throw?new?NoSuchShapeException();
        ????}
        }

        代碼 2 是面向?qū)ο蟠a。每個形狀類是一個對象,不僅存儲數(shù)據(jù),還包含其行為。這三個形狀類都實現(xiàn) Shape 接口,每種形狀類都有各自的計算面積的方法。

        代碼 2

        public?interface?Shape{
        ????double?area();
        }

        public?class?Square?implements?Shape{
        ????private?Point?topLeft;
        ????private?double?side;
        ????
        ????public?double?area(){
        ????????return?side*side;
        ????}
        }

        public?class?Rectangle?implements?Shape{
        ????private?Point?topLeft;
        ????private?double?width;
        ????private?double?height;
        ????
        ????public?double?area(){
        ????????return?width?*?height;
        ????}
        }

        public?class?Circle?implements?Shape{
        ????private?static?final?double?PI?=?3.14159265358;?
        ????
        ????private?Point?topLeft;
        ????private?double?radius;
        ????
        ????public?double?area{
        ????????return?PI?*?radius?*?radius;
        ????}
        }

        有人可能會說,既然 Java 是一門面向?qū)ο箝_發(fā)語言,那肯定代碼 2 要優(yōu)于代碼 1,我們在平常的開發(fā)過程中,要多寫代碼 2 這種面向?qū)ο蟠a,少寫代碼 1 這種過程式代碼。

        但是?。?!并不是這樣的!實際情況是,在某種情況下,過程式代碼優(yōu)于面向?qū)ο蟠a,在另一種情況下,面向?qū)ο蟠a優(yōu)于過程式代碼。我們是選擇過程式代碼還是面向?qū)ο蟠a,需要根據(jù)具體情況具體分析

        舉個栗子!現(xiàn)在增加了一個需求,計算這三種圖形的周長。如果是代碼 1 的話,只需要給 Geometry 類添加一個 perimeter() 函數(shù),三個形狀類不會受到任何影響。如果是代碼 2 的話,需要給每個形狀類都添加一個 perimeter() 函數(shù)。

        現(xiàn)在增加另外一個需求,需要添加一個新形狀。如果是代碼 2 的話,只需要添加一個新的形狀類,既有的形狀類不會受到任何影響。如果是代碼 1 的話,Geometry 類的每個函數(shù)都要修改,增加一個 if else 分支。

        所以,我們得出以下結(jié)論:
        過程式代碼便于在不改動既有數(shù)據(jù)結(jié)構(gòu)的前提下添加新的函數(shù),而面向?qū)ο蟠a便于在不改動既有函數(shù)的前提下添加新類

        所以,對于面向?qū)ο筝^難的事,對于過程式代碼卻較容易,反之亦然!這就是過程式代碼和面向?qū)ο蟠a的反對稱性!由于數(shù)據(jù)結(jié)構(gòu)是過程式代碼的基本單元,對象是面向?qū)ο蟠a的基本單元,所以,這也可以說是數(shù)據(jù)和對象的反對稱性。

        在任何復(fù)雜系統(tǒng),都會有需要添加新數(shù)據(jù)類型而不是新函數(shù)的時候,這時,對象和面向?qū)ο缶捅容^適合。另一方面,也會有想要添加新函數(shù)而不是數(shù)據(jù)類型的時候,在這種情況下,數(shù)據(jù)結(jié)構(gòu)和過程式代碼更合適。

        3、得墨忒耳定律

        著名的得墨忒耳定律認為,模塊不應(yīng)該了解它所操作對象的內(nèi)部情形。

        即類? C? 的方法? f? 只應(yīng)該調(diào)用以下對象的方法:

        • C
        public?class?C{
        ??public?void?f1(){
        ????f2();
        ??}
        ??
        ??public?void?f2(){
        ??
        ??}
        }
        • 由? f? 創(chuàng)建的對象
        public?class?C1{
        ??public?void?f(){
        ????C2?c2?=?new?C2();
        ????c2.f();
        ??}
        }

        public?class?C2{
        ??public?void?f(){
        ????/*...*/
        ??}
        }
        • 作為參數(shù)傳給? f? 的對象
        public?class?C1{
        ??public?void?f(C2?c2){
        ????c2.f();
        ??}
        }

        public?class?C2{
        ??public?void?f(){
        ????/*...*/
        ??}
        }
        • 由? C? 的實體變量持有的對象
        public?class?C1{
        ??private?List?names?=?new?ArrayList();
        ??
        ??public?void?f(String?name){
        ????names.add(name);
        ??}
        }

        方法不應(yīng)調(diào)用由任何函數(shù)返回的對象的方法。

        下列代碼違反了得墨忒耳定律,因為它調(diào)用了 getOptions() 函數(shù)返回的對象的 getScratchDir() 函數(shù),又調(diào)用了 getScratchDir() 函數(shù)返回的對象的 getAbsolutePath() 函數(shù)。

        final?String?outputDir?=?ctxt.getOptions().getScartchDir().getAbsolutePath();

        這類代碼常被稱作火車失事,因為涉及到多個函數(shù)的級聯(lián)調(diào)用,一旦出了問題,不知問題出在哪。最好做類似如下的切分:

        Options?opts?=?ctxt.getOptions();
        File?scratchDir?=?opts.getScratchDir();
        final?String?outputDir?=?scratchDir.getAbsolutePath();

        優(yōu)化后的代碼是否違反了得墨忒耳定律呢?

        這要看 ctxt、Options、ScratchDir 是對象還是數(shù)據(jù)結(jié)構(gòu),如果是對象,就違反了得墨忒耳定律,因為模塊知道它要操作的所有對象的內(nèi)部情形(為什么這么說呢?看優(yōu)化后的代碼,模塊知道?ctxt 對象包含有多個選項,每個選項都有一個臨時目錄,而每個臨時目錄都有一個絕對路徑。模塊對于它要操作的這三個對象,每個對象內(nèi)部有啥,了解地清清楚楚)。

        如果是數(shù)據(jù)結(jié)構(gòu),由于數(shù)據(jù)結(jié)構(gòu)只包含數(shù)據(jù),沒有什么行為,則他們自然會暴露其內(nèi)部數(shù)據(jù)結(jié)構(gòu),得墨忒耳定律也就失效了。

        如果是數(shù)據(jù)結(jié)構(gòu),就應(yīng)該這樣寫代碼:

        final?String?outputDir?=?ctxt.options.scratchDir.absolutePath;

        如果是對象,這段代碼違反了得墨忒耳定律,那如何優(yōu)化讓其不違反這個定律呢?我們可以將這段代碼的邏輯全部抽取到 ctxt 的某個函數(shù)中,此時 ctxt 隱藏了內(nèi)部結(jié)構(gòu),防止當前函數(shù)因瀏覽它不該知道的對象而違反得墨忒耳定律

        BufferedOutputStream?bos?=?ctxt.createScratchFileStream(classFileName);

        4、數(shù)據(jù)傳送對象

        DTO(數(shù)據(jù)傳送對象)是一種只有公共變量,沒有函數(shù)(這個函數(shù)是指除 get、set 之外的函數(shù))的類。常見的數(shù)據(jù)傳送對象還有 Bean,這種結(jié)構(gòu)有賦值器和取值器操作私有變量。

        見下面的示例 1 和示例 2 ,二者都是 DTO,區(qū)別就是示例 1 只有數(shù)據(jù),由于數(shù)據(jù)權(quán)限是 public ,我們可以直接讀取數(shù)據(jù)或者給數(shù)據(jù)賦值,示例 2 使用 private 隱藏數(shù)據(jù),然后使用取值器 get 和賦值器 set 操作這些私有變量。

        示例 1

        public?class?Address{
        ??public?String?street;
        ??public?String?city;
        ??public?String?state;
        ??public?String?province;
        ??
        ??public?Address(String?street,?String?city,?String?state,?String?province){
        ????this.street?=?street;
        ????this.city?=?city;
        ????this.state?=?state;
        ????this.province?=?province;
        ??}
        }

        示例 2

        public?class?Address{
        ????private?String?street;
        ????private?String?city;
        ????private?String?state;
        ????private?String?province;
        ????
        ????public?Address(String?street,?String?city,?String?state,?String?province){
        ????????this.street?=?street;
        ????????this.city?=?city;
        ????????this.state?=?state;
        ????????this.province?=?province;
        ????}
        ????
        ????public?void?setStreet(String?street){
        ????????this.street?=?street;
        ????}
        ????
        ????public?String?getStreet(){
        ????????return?stree;
        ????}
        ????
        ????public?void?setCity(String?city){
        ????????this.city?=?city;
        ????}
        ????
        ????public?String?getCity(){
        ????????return?city;
        ????}
        ????
        ????public?void?setState(String?state){
        ????????this.state?=?state;
        ????}
        ????
        ????public?String?getState(){
        ????????return?state;
        ????}
        ????
        ????public?void?setProvince(String?province){
        ????????this.province?=?province;
        ????}
        ????
        ????public?String?getProvince(){
        ????????return?province;
        ????}
        }

        DTO 是非常有用的結(jié)構(gòu),尤其是在與數(shù)據(jù)庫通信,或解析套接字傳遞信息之類的場景之中。

        數(shù)據(jù)傳送對象應(yīng)該是簡單的數(shù)據(jù)結(jié)構(gòu),不應(yīng)該包含業(yè)務(wù)邏輯如果對象有較多需要處理的業(yè)務(wù)邏輯,應(yīng)當新建類來包含業(yè)務(wù)邏輯、隱藏內(nèi)部數(shù)據(jù)進行處理。

        舉個栗子來解釋一下,對于 Address 我們有查找地址 find() 和保存地址 save() 的需求,如果把這兩個業(yè)務(wù)邏輯寫入 Address 類中,Address 就不能說是一個數(shù)據(jù)傳送對象了。

        反例

        public?class?Address{
        ????private?String?street;
        ????private?String?city;
        ????private?String?state;
        ????private?String?province;
        ????
        ????public?Address(String?street,?String?city,?String?state,?String?province){
        ????????this.street?=?street;
        ????????this.city?=?city;
        ????????this.state?=?state;
        ????????this.province?=?province;
        ????}
        ????
        ????//?所有屬性的?set、get?方法
        ????
        ????public?Address?find(){
        ????/*...*/
        ????return?address;
        ??}
        ??
        ??public?void?set(String?street,?String?city,?String?state,?String?province){
        ????Address?address?=?new?Address();
        ????address.setStreet(street);
        ????address.setCity(city);
        ????address.setState(state);
        ????address.setProvince(province);
        ??}
        }

        想要保證 Address 是一個數(shù)據(jù)傳送對象,那么這兩個業(yè)務(wù)邏輯就不應(yīng)該寫到 Address 類中,我們可以這樣操作,Adderss 類依舊保持上述示例 2 的樣子,然后創(chuàng)建一個新的類,在這個新的類中編寫這兩個業(yè)務(wù)邏輯。

        正例

        public?class?AddressOperator{
        ??public?Address?find(){
        ????/*...*/
        ????return?address;
        ??}
        ??
        ??public?void?set(String?street,?String?city,?String?state,?String?province){
        ????Address?address?=?new?Address();
        ????address.setStreet(street);
        ????address.setCity(city);
        ????address.setState(state);
        ????address.setProvince(province);
        ??}
        }

        5、小結(jié)

        對象曝露行為,隱藏數(shù)據(jù)。便于添加新對象類型而無需修改既有行為,同時也難以在既有對象中添加新的行為。
        數(shù)據(jù)結(jié)構(gòu)曝露數(shù)據(jù),沒有明顯的行為,便于向既有數(shù)據(jù)結(jié)構(gòu)添加新的行為,同時也難以向既有函數(shù)添加新的數(shù)據(jù)結(jié)構(gòu)。

        在任何系統(tǒng)中,我們有時會希望能夠靈活地添加新數(shù)據(jù)類型,所以更喜歡在這部分使用對象和面向?qū)ο蟠a。另外一些時候,我們希望能靈活地添加新行為,這時我們更喜歡使用數(shù)據(jù)結(jié)構(gòu)和過程式代碼。優(yōu)秀的軟件開發(fā)者能夠根據(jù)手邊工作的性質(zhì)靈活地選擇其中一種手段。


        瀏覽 98
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 黄色一极视频 | 亚洲精品成AV人片天堂无码 | 老子要把你cao烂h调教 | 亚洲精品国产精华液 | 手机成人免费视频 |