提高代碼質量的經驗總結「如何提高代碼質量和擴展性」

chcbup 技術瑣話

如何提高代碼的擴展性

一、架構的高可用?(不是本文的重點)

如何提高代碼的擴展性

(1)【客戶端層】到【反向代理層】的高可用,是通過反向代理層的冗餘來實現的。以nginx為例:有兩台nginx,一台對線上提供服務,另一台冗餘以保證高可用,常見的實踐是keepalived存活探測,相同virtual IP提供服務。當nginx掛了的時候,keepalived能夠探測到,會自動的進行故障轉移,將流量自動遷移到shadow-nginx,由於使用的是相同的virtual IP,這個切換過程對調用方是透明的。

(2)【反向代理層】到【站點層】的高可用,是通過站點層的冗餘來實現的。假設反向代理層是nginx,nginx.conf里能夠配置多個web後端,並且nginx能夠探測到多個後端的存活性。當web-server掛了的時候,nginx能夠探測到,會自動的進行故障轉移,將流量自動遷移到其他的web-server,整個過程由nginx自動完成,對調用方是透明的。

(3)【站點層】到【服務層】的高可用,是通過服務層的冗餘來實現的。「服務連接池」會建立與下游服務多個連接,每次請求會「隨機」選取連接來訪問下游服務。當service掛了的時候,service-connection-pool能夠探測到,會自動的進行故障轉移,將流量自動遷移到其他的service,整個過程由連接池自動完成,對調用方是透明的(所以說RPC-client中的服務連接池是很重要的基礎組件)。

(4)【服務層】到【緩存層】的高可用,是通過緩存數據的冗餘來實現的。

緩存層的數據冗餘又有幾種方式:第一種是利用客戶端的封裝,service對cache進行雙讀或者雙寫。緩存層也可以通過支持主從同步的緩存集群來解決緩存層的高可用問題。當redis主掛了的時候,sentinel能夠探測到,會通知調用方訪問新的redis,整個過程由sentinel和redis集群配合完成,對調用方是透明的。

(5)【服務層】到【資料庫讀】的高可用,是通過讀庫的冗餘來實現的。

既然冗餘了讀庫,一般來說就至少有2個從庫,「資料庫連接池」會建立與讀庫多個連接,每次請求會路由到這些讀庫。當讀庫掛了的時候,db-connection-pool能夠探測到,會自動的進行故障轉移,將流量自動遷移到其他的讀庫,整個過程由連接池自動完成,對調用方是透明的(所以說DAO中的資料庫連接池是很重要的基礎組件)。

(6)【服務層】到【資料庫寫】的高可用,是通過寫庫的冗餘來實現的。

以mysql為例,可以設置兩個mysql雙主同步,一台對線上提供服務,另一台冗餘以保證高可用,常見的實踐是keepalived存活探測,相同virtual IP提供服務。自動故障轉移:當寫庫掛了的時候,keepalived能夠探測到,會自動的進行故障轉移,將流量自動遷移到shadow-db-master,由於使用的是相同的virtual IP,這個切換過程對調用方是透明的。

如何提高代碼的擴展性

X軸和Z軸已經趨於成熟。以後的發展方向必定是業務功能的發展,代碼的高可用。

二、軟體項目的變化(為什麼要提高代碼的可擴展性【背景】)

如何提高代碼的擴展性
如何提高代碼的擴展性
如何提高代碼的擴展性

軟體設計的唯一產出物—代碼

面向對象的目的是模塊化

三、什麼是高內聚、低耦合

如何提高代碼的擴展性

模塊就是從系統層次去分成不同的部分,每個部分就是一個模塊!分而治之, 將大型系統的複雜問題,分成不同的小模塊,去處理問題!

耦合:主要是講模塊與模塊之間的聯繫

如何提高代碼的擴展性

例如:如果模塊A直接操作了模塊B的數據,這種操作模塊與模塊之間就為強耦合,甚至可以認為這種情況之下基本算沒有分模塊!如果A只是通過數據與B模塊交互,這種我們稱之為弱耦合!微服務獨立的模塊,方便去維護,或者寫單元測試等等…如果木塊之間的依賴非常嚴重,將會非常不易於維護。

內聚:主要指的是模塊內部【東西聚合在一起形成了一個模塊】例如方法,變數,對象,或者是功能模塊。

如何提高代碼的擴展性

模塊內部的代碼, 相互之間的聯繫越強,內聚就越高, 模塊的獨立性就越好。一個模塊應該盡量的獨立,去完成獨立的功能!如果有代碼非得引入到獨立的模塊,建議拆分成多模塊!低內聚的代碼,不好維護,代碼也不夠健壯。

四、軟體設計的目的

如何提高代碼的擴展性

1、如何評價代碼的質量

最重要的是:靈活性;可擴展性;可維護性;可讀性。

如何提高代碼的擴展性

2、如何實現代碼的高質量?

遵循SOLID設計原則:(介面設計原則)參考依據高內聚、低耦合

單一職責原則:一個類值負責一個功能的職責

開閉原則:擴展開放,修改關閉。

里氏代換原則:使用父類的地方都能使用子類對象

依賴倒轉原則:針對介面編程,

介面隔離原則:針對不同部分用專門介面,不用總介面,需要哪些介面就用哪些介面

如何提高代碼的擴展性

你認為下圖的設計違反了哪一種設計原則?

如何提高代碼的擴展性

(1)違反了單一職責原則(SRP)

畫圖和計算面積並不是單一職責,計算幾何學應用程序只計算面積不畫圖,但是還要引入GUI。

應該有且僅有一個原因引起類的變更。簡單點說,一個類,最好只負責一件事,只有一個引起它變化的原因。也就是說引起類變化的原因只有一個。高內聚、低耦合是軟體設計追求的目標,而單一職責原則可以看做是高內聚、低耦合的引申,將職責定義為引起變化的原因,以提高內聚性,以此來減少引起變化的原因。職責過多,可能引起變化的原因就越多,這將是導致職責依賴,相互之間就產生影響,從而極大的損傷其內聚性和耦合度。單一職責通常意味著單一的功能,因此不要為類實現過多的功能點,以保證實體只有一個引起它變化的原因。

(2)開閉原則

對擴展開放。模塊對擴展開放,就意味著需求變化時,可以對模塊擴展,使其具有滿足那些改變的新行為。換句話說,模塊通過擴展的方式去應對需求的變化。

對修改關閉。模塊對修改關閉,表示當需求變化時,關閉對模塊源代碼的修改,當然這裡的「關閉」應該是儘可能不修改的意思,也就是說,應該盡量在不修改源代碼的基礎上面擴展組件。

一個開閉原則的簡單實例(懂則不用看)

如何提高代碼的擴展性

對拓展開放,對修改關閉:比如當某個業務增加,不是在原類增加方法,而是增加原類的實現類。

下面的例子是一個非常典型的開閉原則及其實現。非常簡單,但卻能夠很好的說明開閉原則。

假設有一個應用程序,能夠計算任意形狀面積。這是幾年前我在明尼蘇達州農作物保險公司遇到的一個非常簡單問題。app程序必須能夠計算出指定區域的農作物總的保險報價。正如你所知道的,農作物有各種形狀和大小,有可能是圓的,有可能是三角形的也可能是其他各種多邊形。

OK,讓我們回到我們之前的例子中….

作為一名優秀的程序員,我們將這個面積計算類命名為 AreaManager。這個 AreaManager是單一職責的類:計算形狀的總面積 。

假設我們現在有一塊矩形的農作物,我omen用一個Rectangle類來表示。相關類代碼如下:

public class Rectangle {
    private double length;
    private double height; 
    // getters/setters ... 
}


public class AreaManager {
    public double calculateArea(ArrayList<Rectangle>... shapes) {
        double area = 0;
        for (Rectangle rect : shapes) {
            area += (rect.getLength() * rect.getHeight()); 
        }
        return area;
    }
}


AreaManager類現在運行良好,直到幾周之後,我們又有一種新的形狀——圓形:
public class Circle {
    private double radius; 
    // getters/setters ...
}


由於有新的形狀需要考慮,我們必須修改我們的AreaManager類:
public class AreaManager {
    public double calculateArea(ArrayList<Object>... shapes) {
        double area = 0;
        for (Object shape : shapes) {
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle)shape;
                area += (rect.getLength() * rect.getHeight());                
            } else if (shape instanceof Circle) {
                Circle circle = (Circle)shape;
                area += (circle.getRadius() * cirlce.getRadius() * Math.PI;
            } else {
                throw new RuntimeException("Shape not supported");
            }            
        }
        return area;
    }
}


從這段代碼開始,我們察覺到了問題。
如果我們遇到一個三角形,或者其他形狀呢,這時候我們就必須一次又一次的修改AreaManager類。
這個類的設計就違背了開閉原則,沒有做到對修改的封閉性以及對擴展的開放性。我們必須避免這種事情的發生~
基於繼承的開閉原則的實現
AreaManager類的職責是計算各種形狀的面積,而每一種形狀都有其獨特的計算面積的方法,因此將面積的計算放入到各個形狀類中是特別合理的。
AreaManager類仍然需要知道所有的形狀,否則它就無法判斷所有的形狀類是否都包含了計算面積的方法。當然了,我們可以通過反射來實現。其實有一種更簡單的方式也可以實現——讓所有的形狀類都繼承一個介面:Shape(也可以是抽象類)
public interface Shape {
    double getArea(); 
}


每一個形狀類都實現這個介面(如果介面無法滿足你的需求,也可以通過繼承某個抽象類):
public class Rectangle implements Shape {
   private double length;
   private double height; 
   // getters/setters ... 


   @Override
   public double getArea() {
       return (length * height);
   }
}


public class Circle implements Shape {
   private double radius; 
   // getters/setters ...


   @Override
   public double getArea() {
       return (radius * radius * Math.PI);
   }
}


現在,我們可以通過這個抽象方法將AreaManager構造成一個符合開閉原則的類。
public class AreaManager {
    public double calculateArea(ArrayList<Shape> shapes) {
        double area = 0;
        for (Shape shape : shapes) {
            area += shape.getArea();
        }
        return area;
    }
}


通過這種方式, AreaManager類符合了對修改關閉,對擴展開放的要求。如果我們需要增加一種新形狀,比如:八邊形。新的類只需要繼承Shape介面即可,AreaManager根本不需要做任何的修改。




作者:4d3bf4cac28c
鏈接:https://www.jianshu.com/p/6c8a9611b38b
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

通過擴展去應對需求變化,就要求我們必須要面向介面編程,或者說面向抽象編程。所有參數類型、引用傳遞的對象必須使用抽象(介面或者抽象類)的方式定義,不能使用實現類的方式定義;通過抽象去界定擴展,比如我們定義了一個介面A的參數,那麼我們的擴展只能是介面A的實現類。總的來說,開閉原則提高系統的可維護性和代碼的重用性。

指導建議:使用者先定義介面

如何提高代碼的擴展性

(3)里氏替換原則

如何提高代碼的擴展性

如上圖釋義,一個軟體實體如果使用的是一個基類的話,那麼一定適用於其子類,而且它根本不能察覺出基類對象和子類對象的區別。

比如,假設有兩個類,一個是Base類,另一個是Child類,並且Child類是Base的子類。那麼一個方法如果可以接受一個基類對象b的話:method1(Base b)那麼它必然可以接受一個子類的對象method1(Child c).

里氏替換原則是繼承復用的基石。只有當衍生類可以替換掉基類,軟體單位的功能不會受到影響時,基類才能真正的被複用,而衍生類也才能夠在基類的基礎上增加新的行為。

問題由來:有一功能P1,由類A完成。現需要將功能P1進行擴展,擴展後的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。

解決方案:當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。

里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。它包含以下4層含義:

  • 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
  • 子類中可以增加自己特有的方法。
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。
  • 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

(4)依賴倒置原則

官方釋義:高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。

上面的定義主要包含兩次意思:

1)高層模塊不應該直接依賴於底層模塊的具體實現,而應該依賴於底層的抽象。換言之,模塊間的依賴是通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過介面或抽象類產生的。

2)介面和抽象類不應該依賴於實現類,而實現類依賴介面或抽象類。這一點其實不用多說,很好理解,「面向介面編程」思想正是這點的最好體現。

如何提高代碼的擴展性

相比傳統的軟體設計架構,比如我們常說的經典的三層架構,UI層依賴於BLL層,BLL層依賴於DAL層。由於每一層都是依賴於下層的實現,這樣當某一層的結構發生變化時,它的上層就不得不也要發生改變,比如我們DAL裡面邏輯發生了變化,可能會導致BLL和UI層都隨之發生變化,這種架構是非常荒謬的!好,這個時候如果我們換一種設計思路,高層模塊不直接依賴低層的實現,而是依賴於低層模塊的抽象,具體表現為我們增加一個IBLL層,裡面定義業務邏輯的介面,UI層依賴於IBLL層,BLL層實現IBLL裡面的介面,所以具體的業務邏輯則定義在BLL裡面,這個時候如果我們BLL裡面的邏輯發生變化,只要介面的行為不變,上層UI裡面就不用發生任何變化。

在三層架構裡面增加一個介面層能實現依賴倒置,它的目的就是降低層與層之間的耦合,使得設計更加靈活。從這點上來說,依賴倒置原則也是「松耦合」設計的很好體現。

依賴倒置原則(DIP)例子:

如何提高代碼的擴展性

未進行依賴倒置設計:司機只開一種車

如何提高代碼的擴展性

遵循依賴倒置原則設計:司機可以開多種車

(5)介面隔離原則

ISP:其一是不應該強行要求客戶端依賴於它們不用的介面;其二是類之間的依賴應該建立在最小的介面上面。簡單點說,客戶端需要什麼功能,就提供什麼介面,對於客戶端不需要的介面不應該強行要求其依賴;類之間的依賴應該建立在最小的介面上面,這裡最小的粒度取決於單一職責原則的劃分。

如果客戶端依賴了它們不需要的介面,那麼這些客戶端程序就面臨不需要的介面變更引起的客戶端變更的風險,這樣就會增加客戶端和介面之間的耦合程度,顯然與「高內聚、低耦合」的思想相矛盾。

類之間的依賴應該建立在最小的介面上面。何為最小的介面,即能夠滿足項目需求的相似功能作為一個介面,這樣設計主要就是為了「高內聚」。那麼我們如何設計最小的介面呢?那就要說說粒度的劃分了,粒度細化的程度取決於我們上一章講的的單一職責原則裡面介面劃分的粒度。從這一點來說,介面隔離和單一職責兩個原則有一定的相似性。

不同:

(1)單一職責原則更加偏向對業務的約束,介面隔離原則更加偏向設計架構的約束。

(2)從介面的細化程度來說,單一職責原則對介面的劃分更加精細,而介面隔離原則注重的是相同功能的介面的隔離。介面隔離裡面的最小介面有時可以是多個單一職責的公共介面。

(3)從原則約束的側重點來說,介面隔離原則更關注的是介面依賴程度的隔離,更加關注介面的「高內聚」;而單一職責原則更加註重的是介面職責的劃分。

如何提高代碼的擴展性

未遵循介面隔離原則的設計

如何提高代碼的擴展性

遵循介面隔離原則的設計

1、場景舉例分析

二、場景示例
下面就以我們傳統行業的訂單操作為例來說明下介面隔離的必要性。
1、胖介面
軟體設計最初,我們的想法是相同功能的方法放在同一個介面裡面,如下,所有訂單的操作都放在訂單介面IOrder裡面。理論上來說,這貌似沒錯。我們來看看如何設計。


   public interface IOrder
    {
        //訂單申請操作
        void Apply(object order);


        //訂單審核操作
        void Approve(object order);


        //訂單結束操作
        void End(object order);


    }


剛開始只有銷售訂單,我們只需要實現這個介面就好了。


    public class SaleOrder:IOrder
    {
        public void Apply(object order)
        {
            throw new NotImplementedException();
        }


        public void Approve(object order)
        {
            throw new NotImplementedException();
        }


        public void End(object order)
        {
            throw new NotImplementedException();
        }
    }


後來,隨著系統的不斷擴展,我們需要加入生產訂單,生產訂單也有一些單獨的介面方法,比如:排產、凍結、導入、導出等操作。於是我們向訂單的介面裡面繼續加入這些方法。於是訂單的介面變成這樣:


    public interface IOrder
    {
        //訂單申請操作
        void Apply(object order);


        //訂單審核操作
        void Approve(object order);


        //訂單結束操作
        void End(object order);


        //訂單下發操作
        void PlantProduct(object order);


     //訂單凍結操作
        void Hold(object order);


        //訂單刪除操作
        void Delete(object order);


        //訂單導入操作
        void Import();


        //訂單導出操作
        void Export();
    }


我們生產訂單的實現類如下


    //生產訂單實現類
    public class ProduceOrder : IOrder
    {
        /// <summary>
        /// 對於生產訂單來說無用的介面
        /// </summary>
        /// <param name="order"></param>
        public void Apply(object order)
        {
            throw new NotImplementedException();
        }


        /// <summary>
        /// 對於生產訂單來說無用的介面
        /// </summary>
        /// <param name="order"></param>
        public void Approve(object order)
        {
            throw new NotImplementedException();
        }


        /// <summary>
        /// 對於生產訂單來說無用的介面
        /// </summary>
        /// <param name="order"></param>
        public void End(object order)
        {
            throw new NotImplementedException();
        }


        public void PlantProduct(object order)
        {
            Console.WriteLine("訂單下發排產");
        }


     public void Hold(object order)
        {
            Console.WriteLine("訂單凍結");
        }


        public void Delete(object order)
        {
            Console.WriteLine("訂單刪除");
        }


        public void Import()
        {
            Console.WriteLine("訂單導入");
        }


        public void Export()
        {
            Console.WriteLine("訂單導出");
        }
    }


銷售訂單的實現類也要相應做修改


    //銷售訂單實現類
    public class SaleOrder:IOrder
    {
        public void Apply(object order)
        {
            Console.WriteLine("訂單申請");
        }


        public void Approve(object order)
        {
            Console.WriteLine("訂單審核處理");
        }


        public void End(object order)
        {
            Console.WriteLine("訂單結束");
        }


        #region 對於銷售訂單無用的介面方法
        public void PlantProduct(object order)
        {
            throw new NotImplementedException();
        }


     public void Hold(object order)
        {
            throw new NotImplementedException();
        }


        public void Delete(object order)
        {
            throw new NotImplementedException();
        }


        public void Import()
        {
            throw new NotImplementedException();
        }


        public void Export()
        {
            throw new NotImplementedException();
        } 
        #endregion
    }


需求做完了,上線正常運行。貌似問題也不大。系統運行一段時間之後,新的需求變更來了,要求生成訂單需要一個訂單撤銷排產的功能,那麼我們的介面是不是就得增加一個訂單撤排的介面方法CancelProduct。於是乎介面變成這樣:


public interface IOrder
    {
        //訂單申請操作
        void Apply(object order);


        //訂單審核操作
        void Approve(object order);


        //訂單結束操作
        void End(object order);


        //訂單下發操作
        void PlantProduct(object order);


        //訂單撤排操作
        void CancelProduct(object order);


        //訂單凍結操作
        void Hold(object order);


        //訂單刪除操作
        void Delete(object order);


        //訂單導入操作
        void Import();


        //訂單導出操作
        void Export();
    }


這個時候問題就來了,我們的生產訂單只要實現這個撤銷的介面貌似就OK了,但是我們的銷售訂單呢,本來銷售訂單這一塊我們不想做任何的變更,可是由於我們IOrder介面裡面增加了一個方法,銷售訂單的實現類是不是也必須要實現一個無效的介面方法?這就是我們常說的「胖介面」導致的問題。由於介面過「胖」,每一個實現類依賴了它們不需要的介面,使得層與層之間的耦合度增加,結果導致了不需要的介面發生變化時,實現類也不得不相應的發生改變。這裡就凸顯了我們介面隔離原則的必要性,下面我們就來看看如何通過介面隔離來解決上述問題。
2、介面隔離
我們將IOrder介面分成兩個介面來設計


    //刪除訂單介面
    public interface IProductOrder
    {
        //訂單下發操作
        void PlantProduct(object order);


        //訂單撤排操作
        void CancelProduct(object order);


        //訂單凍結操作
        void Hold(object order);


        //訂單刪除操作
        void Delete(object order);


        //訂單導入操作
        void Import();


        //訂單導出操作
        void Export();
    }


    //銷售訂單介面
    public interface ISaleOrder
    {
        //訂單申請操作
        void Apply(object order);


        //訂單審核操作
        void Approve(object order);


        //訂單結束操作
        void End(object order);
    }


對應的實現類只需要實現自己需要的介面即可


    //生產訂單實現類
    public class ProduceOrder : IProductOrder
    {
        public void PlantProduct(object order)
        {
            Console.WriteLine("訂單下發排產");
        }


        public void CancelProduct(object order)
        {
            Console.WriteLine("訂單撤排");
        }


        public void Hold(object order)
        {
            Console.WriteLine("訂單凍結");
        }


        public void Delete(object order)
        {
            Console.WriteLine("訂單刪除");
        }


        public void Import()
        {
            Console.WriteLine("訂單導入");
        }


        public void Export()
        {
            Console.WriteLine("訂單導出");
        }
    }


    //銷售訂單實現類
    public class SaleOrder : ISaleOrder
    {


        public void Apply(object order)
        {
            Console.WriteLine("訂單申請");
        }


        public void Approve(object order)
        {
            Console.WriteLine("訂單審核處理");
        }


        public void End(object order)
        {
            Console.WriteLine("訂單結束");
        }
    }

這樣設計就能完美解決上述「胖介面」導致的問題,如果需要增加訂單操作,只需要在對應的介面和實現類上面修改即可,這樣就不存在依賴不需要介面的情況。通過這種設計,降低了單個介面的複雜度,使得介面的「內聚性」更高,「耦合性」更低。由此可以看出介面隔離原則的必要性。

另:

有稱六大設計原則:+迪米特法則。

迪米特法則的定義是:只與你的直接朋友交談,不跟「陌生人」說話(Talk only to your immediate friends and not to strangers)。其含義是:如果兩個軟體實體無須直接通信,那麼就不應當發生直接的相互調用,可以通過第三方轉發該調用。其目的是降低類之間的耦合度,提高模塊的相對獨立性。

舉例:明星由於全身心投入藝術,所以許多日常事務由經紀人負責處理,如與粉絲的見面會,與媒體公司的業務洽淡等。這裡的經紀人是明星的朋友,而粉絲和媒體公司是陌生人,所以適合使用迪米特法則,其類圖如圖 所示。

如何提高代碼的擴展性

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/282162.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-21 13:22
下一篇 2024-12-21 13:22

相關推薦

發表回復

登錄後才能評論