成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

違反這些設計原則,系統就等著“腐爛”

開發
雖然在日常開發的時候項目進度比較緊張,我們很多時候也不去深度設計代碼實現,但是我們在寫代碼的時候保證心中有一桿秤其實還是必要的。

在燦爛的陽光下,龍年重磅來臨。讓我們放下過去的困惑和猶豫,張開懷抱,迎接嶄新的希望和機遇。祝大家龍年快樂,事業有成!老貓在此給大家拜年了。

老貓的設計模式專欄已經偷偷發車了。不甘愿做crud boy?看了好幾遍的設計模式還記不住?那就不要刻意記了,跟上老貓的步伐,在一個個有趣的職場故事中領悟設計模式的精髓吧。還等什么?趕緊上車吧。

故事

這段時間以來,小貓按照之前的系統梳理方案【系統梳理大法&代碼梳理大法】一直在整理著文檔。

系統中涉及的業務以及模型也基本了然于胸,但是這代碼寫的真的是...

小貓也終于知道了為什么每天都有客訴,為什么每天都要去調用curl語句去訂正生產的數據,為什么每天都在Hotfix...

整理了一下,大概出于這些原因,業務流程復雜暫且不議,光從技術角度來看,整個代碼體系臃腫不堪,出問題之后定位困難,后面接手的幾任開發為了解決問題都是“曲線救國”,不從正面去解決問題,為了解決一時的客訴問題而去解決問題,于是定義了各種新的修復流程去解決問題,這么一來,軟件系統“無序”總量一直在增加,整個系統體系其實在初版之后就已經在“腐爛”了,如此?且拋開運維穩定性不談,就系統本身穩定性而言,能好?

所以那兩次事故還真不能怪小貓【事故1,事故2

整個系統,除了堆業務還是堆業務,但凡有點軟件設計原則,系統也不會寫成這樣了。

關于設計原則

大家在產品提出需求之后,一般都會去設計數據模型,還有系統流程。但是各位有沒有深度去設計一下代碼的實現呢?還是說上手就直接照著流程圖開始擼業務了?估計有很多的小伙伴由于各種原因不會去考慮代碼設計,其實老貓很多時候也一樣。主要原因比如:項目催得緊,哪有時間考慮那么多,功能先做出來,剩下的等到后面慢慢優化。然而隨著時間的推移,我們會發現我們一直很忙,說好的把以前的代碼重構好一點,哪有時間!于是,就這樣“技術債”越來越多,就像滾雪球一樣,整個系統逐漸“腐爛”到了根。最終坑的可能是自己,也有可能是“下一個他”。

雖然在日常開發的時候項目進度比較緊張,我們很多時候也不去深度設計代碼實現,但是我們在寫代碼的時候保證心中有一桿秤其實還是必要的。

那咱們就結合各種案來聊聊“這桿秤”————軟件設計原則。

下面我們通過各種小例子來協助大家理解軟件設計原則,案例是老貓構想的,有的時候不要太過較真,主要目的是講清楚原則。另外后文中也會有相關的類圖表示實體之間的關系,如果大家對類圖不太熟悉的,也可以看一下這里【類圖傳送門

開閉原則

開閉原則,英文(Open-Closed Principle,簡稱:OCP)。只要指一個軟件實體(例如,類,模塊和函數),應該對擴展開放,對修改關閉。其重點強調的是抽象構建框架,實現擴展細節,從而提升軟件系統的可復用性以及可維護性。

概念是抽象,但是案例是具體的,所以咱們直接看案例,通過案例去理解可能更容易。

由于小貓最近在維護商城類業務,所以咱們就從商品折價售賣這個案例出發。業務是這樣的,商城需要對商品進行做打折活動,目前針對不同品類的商品可能打折的力度不一樣,例如生活用品和汽車用品的打折情況不同。創建一個基礎商品接口:

public interface IProduct {
    String getSpuCode(); //獲取商品編號
    String getSpuName(); //獲取商品名稱
    BigDecimal getPrice(); //獲取商品價格
}

基礎商品實現該接口,于是我們就有了如下代碼:

/**
 * @Author: 公眾號:程序員老貓
 * @Date: 2024/2/7 23:39
 */
public class Product implements IProduct {
    private String spuCode;
    private String spuName;
    private BigDecimal price;
    private Integer categoryTag;

    public Product(String spuCode, String spuName, BigDecimal price, Integer categoryTag) {
        this.spuCode = spuCode;
        this.spuName = spuName;
        this.price = price;
        this.categoryTag = categoryTag;
    }

    public Integer getCategoryTag() {
        return categoryTag;
    }

    @Override
    public String getSpuCode() {
        return spuCode;
    }

    @Override
    public String getSpuName() {
        return spuName;
    }

    @Override
    public BigDecimal getPrice() {
        return price;
    }
}

按照上面的業務,現在搞活動,咱們需要針對不同品類的商品進行促銷活動,例如生活用品需要進行折扣。當然我們有兩種方式實現這個功能,如果咱們不改變原有代碼,咱們可以如下實現。

public class DailyDiscountProduct extends Product {
    private static final BigDecimal daily_discount_factor = new BigDecimal(0.95);
    private static final Integer DAILY_PRODUCT = 1;

    public DailyDiscountProduct(String spuCode, String spuName, BigDecimal price) {
        super(spuCode, spuName, price, DAILY_PRODUCT);
    }

    public BigDecimal getOriginPrice() {
        return super.getPrice();
    }

    @Override
    public BigDecimal getPrice() {
        return super.getPrice().multiply(daily_discount_factor);
    }
}

上面我們看到直接打折的日常用品的商品繼承了標準商品,并且對其進行了價格重寫,這樣就完成了生活用品的打折。當然這種打折系數的話我們一般可以配置到數據庫中。

對汽車用品的打折其實也是一樣的實現。繼承之后重寫價格即可。咱們并不需要去基礎商品Product中根據不同的品類去更改商品的價格。

錯誤案例:

如果我們一味地在原始類別上去做邏輯應該就是如下這樣:

public class Product implements IProduct {
    private static final Integer DAILY_PRODUCT = 1;
    private static final BigDecimal daily_discount_factor = new BigDecimal(0.95);
    private String spuCode;
    private String spuName;
    private BigDecimal price;
    private Integer categoryTag;
    ....
    @Override
    public BigDecimal getPrice() {
      if(categotyTag.equals(DAILY_PRODUCT)){
        return price.multiply(daily_discount_factor);
      }
      return price;
    }
}

后續隨著業務的演化,后面如果提出對商品名稱也要定制,那么咱們可能還是會動當前的代碼,我們一直在改當前類,代碼越堆越多,越來越臃腫,這種實現方式就破壞了開閉原則。

咱們看一下開閉原則的類圖。如下:

開閉原則類圖

依賴倒置原則

依賴倒置原則,英文名(Dependence Inversion Principle,簡稱DIP),指的是高層模塊不應該依賴低層模塊,二者都應該依賴其抽象。通過依賴倒置,可以減少類和類之間的耦合性,從而提高系統的穩定性。這里主要強調的是,咱們寫代碼要面向接口編程,不要面向實現去編程。

定義看起來不夠具體,咱們來看一下下面這樣一個業務。針對不同的大客戶,我們定制了很多商城,有些商城是專門售賣電器的,有些商城是專門售賣生活用品的。有個大客,由于對方是電器供應商,所以他們想售賣自己的電器設備,于是,我們就有了下面的業務。

//定義了一個電器設備商城,并且支持特有的電器設備下單流程
public class ElectricalShop {
    public String doOrder(){
        return "電器商城下單";
    }
}
//用戶進行下單購買電器設備
public class Consumer extends ElectricalShop {
    public void shopping() {
        super.doOrder();
    }
}

我們看到,當客戶可選擇的只有一種商城的時候,這種實現方式確實好像沒有什么問題,但是現在需求變了,馬上要過年了,大客戶不想僅僅給他們的客戶提供電器設備,他們還想賣海鮮產品,這樣,以前的這種下單模式好像會有點問題,因為以前我們直接繼承了ElectricalShop,這樣寫的話,業務可拓展性就太差了,所以我們就需要抽象出一個接口,然后客戶在下單的時候可以選擇不同的商城進行下單。于是改造之后,咱們就有了如下代碼:

//抽象出一個更高維度的商城接口
public interface Shop {
    String doOrder();
}
//電器商城實現該接口實現自有下單流程
public class ElectricalShop implements Shop {
    public String doOrder(){
        return "電器商城下單";
    }
}
//海鮮商城實現該接口實現自有下單流程
public class SeaFoodShop implements Shop{
    @Override
    public String doOrder() {
        return "售賣一些海鮮產品";
    }
}
//消費者注入不同的商城商品信息
public class Consumer {
    private Shop shop;
    public Consumer(Shop shop) {
        this.shop = shop;
    }
    public String shopping() {
        return shop.doOrder();
    }
}
//消費者在不同商城隨意切換下單測試
public class ConsumerTest {
    public static void main(String[] args) {
        //電器商城下單
        Consumer consumer = new Consumer(new ElectricalShop());
        System.out.println(consumer.shopping());
        //海鮮商城下單
        Consumer consumer2 = new Consumer(new SeaFoodShop());
        System.out.println(consumer2.shopping());
    }
}

上面這樣改造之后,原本繼承詳細商城實現的Consumer類,現在直接將更高維度的商城接口注入到了類中,這樣相信后面再多幾個新的商城的下單流程都可以很方便地就完成拓展。

這其實也就是依賴倒置原則帶來的好處,咱們最終來看一下類圖。

DIP

單一職責原則

單一職責原則,英文名(SimpleResponsibility Pinciple,簡稱SRP)指的是不要存在多余一個導致類變更的原因。這句話看起來還是比較抽象的,老貓個人的理解是單一職責原則重點是區分業務邊界,做到合理地劃分業務,根據產品的需求不斷去重新規劃設計當前的類信息。關于單一職責老貓其實之前已經和大家分享過了,在此不多贅述,大家可以進入這個傳送門【單一職責原則】。

接口隔離原則

接口隔離原則(Interface Segregation Principle,簡稱ISP)指的是指盡量提供專門的接口,而非使用一個混合的復雜接口對外提供服務。

聊到接口隔離原則,其實這種原則和單一職責原則有點類似,但是又不同:

  • 聯系:接口隔離原則和單一職責原則都是為了提高代碼的可維護性和可拓展性以及可重用性,其核心的思想都是“高內聚低耦合”。
  • 區別:針對性不同,接口隔離原則針對的是接口,而單一職責原則針對的是類。

下面,咱們用一個業務例子來說明一下吧。我們用簡單的動物行為這樣一個例子來說明一下,動物從大的方面有能飛的,能吃,能跑,有的也會游泳等等。如果我們定義一個比較大的接口就是這樣的。

public interface IAnimal {
    void eat();
    void fly();
    void swim();
    void run();
    ...
}

我們用貓咪實現了該方法,于是就有了。

public class Cat implements IAnimal{
    @Override
    public void eat() {
        System.out.println("老貓喜歡吃小魚干");
    }
    @Override
    public void fly() {
    }
    @Override
    public void swim() {
    }
    @Override
    public void run() {
        System.out.println("老貓還喜歡奔跑");
    }
}

我們很容易就能發現,如果老貓不是“超人貓”的話,老貓就沒辦法飛翔以及游泳,所以當前的類就有兩個空著的方法。同樣的如果有一只百靈鳥,那么實現Animal接口之后,百靈鳥的游泳方法也是空著的。那么這種實現我們發現只會讓代碼變得很臃腫,所以,我們發現IAnimal這個接口的定義太大了,我們需要根據不同的行為進行二次拆分。拆分之后的結果如下:

//所有的動物都會吃東西
public interface IAnimal {
    void eat();
}
//專注飛翔的接口
public interface IFlyAnimal {
    void fly();
}
//專注游泳的接口
public interface ISwimAnimal {
    void swim();
}

那如果現在有一只鴨子和百靈鳥,咱們分別去實現的時候如下:

public class Duck implements IAnimal,ISwimAnimal{
    @Override
    public void eat() {
        System.out.println("鴨子吃食");
    }

    @Override
    public void swim() {
        System.out.println("鴨子在河里游泳");
    }
}

public class Lark implements IAnimal,IFlyAnimal{
    @Override
    public void eat() {
        System.out.println("百靈鳥吃食");
    }

    @Override
    public void fly() {
        System.out.println("百靈鳥會飛");
    }
}

我們可以看到,這樣在我們具體的實現類中就不會存在空方法的情況,代碼隨著業務的發展也不會變得過于臃腫。咱們看一下最終的類圖。

ISP

迪米特原則

迪米特原則(Law of Demeter,簡稱 LoD),指的是一個對象應該對其他對象保持最少的了解,如果上面這個原則名稱不容易記,其實這種設計原則還有另外一個名稱,叫做最少知道原則(Least Knowledge Principle,簡稱LKP)。其實主要強調的也是降低類和類之間的耦合度,白話“不要和陌生人說話”,或者也可以理解成“讓專業的人去做專業的事情”,出現在成員變量,方法輸入、輸出參數中的類都可以稱為成員朋友類,而出現在方法體內部的類不屬于朋友類。

通過具體場景的例子來看一下。由于小貓接手了商城類的業務,目前他對業務的實現細節應該是最清楚的,所以領導在向老板匯報相關SKU銷售情況的時候總是會找到小貓去統計各個品類的sku的銷售額以及銷售量。于是就有了領導下命令,小貓去做統計的業務流程。

//sku商品
public class Sku {
    private BigDecimal price;
    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }
}

//小貓統計總sku數量以及總銷售金額
public class Kitty {
    public void doSkuCheck(List<Sku> skuList) {
        BigDecimal totalSaleAmount =
                skuList.stream().map(sku -> sku.getPrice()).reduce(BigDecimal::add).get();
        System.out.println("總sku數量:" + skuList.size() + "sku總銷售金額:" + totalSaleAmount);
    }
}

//領導讓小貓去統計各個品類的商品
public class Leader {
    public void checkSku(Kitty kitty) {
        //模擬領導指定的各個品類
        List<Sku> difCategorySkuList = new ArrayList<>();
        kitty.doSkuCheck(difCategorySkuList);
    }
}

//測試類
public class LodTest {
    public static void main(String[] args) {
        Leader leader = new Leader();
        Kitty kitty = new Kitty();
        leader.checkSku(kitty);
    }
}

從上面的例子來看,領導其實并沒有參與統計的任何事情,他只是指定了品類讓小貓去統計。從而降低了類和類之間的耦合。即“讓專門的人做專門的事”

我們看一下最終的類圖。

LOD

里氏替換原則

里氏替換原則(Liskov Substitution Principle,英文簡稱:LSP),它由芭芭拉·利斯科夫(Barbara Liskov)在1988年提出。里氏替換原則的含義是:如果一個程序中所有使用基類的地方都可以用其子類來替換,而程序的行為沒有發生變化,那么這個子類就遵守了里氏替換原則。換句話說,一個子類應該可以完全替代它的父類,并且保持程序的正確性和一致性。

上述的定義還是比較抽象的,老貓試著重新理解一下:

  • 子類可以實現父類的抽象方法,但是不能覆蓋父類的抽象方法。
  • 子類可以增加自己特有的方法。
  • 當子類的方法重載父類的方法的時,方法的前置條件(即方法的輸入/入參)要比父類方法的輸入參數更加寬松。
  • 當子類的方法實現父類的方法時,方法的后置條件比父類更嚴格或者和父類一樣。

里氏替換原則準確來說是上述提到的開閉原則的實現方式,但是它克服了繼承中重寫父類造成的可復用性變差的缺點。它是動作正確性的保證。即類的擴展不會給已有的系統引入新的錯誤,降低了代碼出錯的可能性。

下面咱們用里式替換原則比較經典的例子來說明“鴕鳥不是鳥”。我們看一下咱們印象中的鳥類:

class Bird {
    double flySpeed; 
 
    //設置飛行速度
    public void setSpeed(double speed) {
        flySpeed = speed;
    }
 
    //計算飛行所需要的時間
    public double getFlyTime(double distance) {
        return (distance / flySpeed);
    }
}
//燕子
public class Swallow extends Bird{
}
//由于鴕鳥不能飛,所以我們將鴕鳥的速度設置為0
public class Ostrich extends Bird {
    public void setSpeed(double speed) {
        flySpeed = 0;
    }
}

光看這個實現的時候好像沒有問題,但是我們調用其方法計算其指定距離飛行時間的時候,那么這個時候就有問題了,如下:

public class TestMain {
    public static void main(String[] args) {
        double distance = 120;
        Ostrich ostrich = new Ostrich();
        System.out.println(ostrich.getFlyTime(distance));

        Swallow swallow = new Swallow();
        swallow.setSpeed(30);
        System.out.println(swallow.getFlyTime(distance));
    }
}

結果輸出:

Infinity
4.0

顯然鴕鳥出問題了:

  • 鴕鳥重寫了鳥類的 setSpeed(double speed) 方法,這違背了里氏替換原則。
  • 燕子和鴕鳥都是鳥類,但是父類抽取的共性有問題,鴕鳥的飛行不是正常鳥類的功能,需要特殊處理,應該抽取更加共性的功能。

于是我們進行對其進行優化,咱們取消鴕鳥原來的繼承關系,定義鳥和鴕鳥的更一般的父類,如動物類,它們都有奔跑的能力。鴕鳥的飛行速度雖然為 0,但奔跑速度不為 0,可以計算出其奔跑指定距離所要花費的時間。優化之后代碼如下:

//抽象出更高層次的動物類,定義內部的奔跑行為
public class Animal {
    double runSpeed;

    //設置奔跑速度
    public void setSpeed(double speed) {
        runSpeed = speed;
    }
    //計算奔跑所需要的時間
    public double getRunTime(double distance) {
        return (distance / runSpeed);
    }
}
//定義飛行的鳥類
public class Bird extends Animal {
    double flySpeed;
    //設置飛行速度
    public void setSpeed(double speed) {
        flySpeed = speed;
    }
    //計算飛行所需要的時間
    public double getFlyTime(double distance) {
        return (distance / flySpeed);
    }
}
//此時鴕鳥直接繼承動物接口
public class Ostrich extends Animal {
}
//燕子繼承普通的鳥類接口
public class Swallow extends Bird {
}

簡單測試一下:

public class TestMain {
    public static void main(String[] args) {
        double distance = 120;
        Ostrich ostrich = new Ostrich();
        ostrich.setSpeed(40);
        System.out.println(ostrich.getRunTime(distance));

        Swallow swallow = new Swallow();
        swallow.setSpeed(30);
        System.out.println(swallow.getFlyTime(distance));
    }
}

結果輸出:

3.0
4.0

優化之后,優點:

  • 代碼共享,減少創建類的工作量,每個子類都擁有父類的方法和屬性;
  • 提高代碼的重用性;
  • 提高代碼的可擴展性;
  • 提高產品或項目的開放性;

缺點:

  • 繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法;
  • 降低代碼的靈活性。子類必須擁有父類的屬性和方法,讓子類自由的世界中多了些約束;
  • 增強了耦合性。當父類的常量、變量和方法被修改時,需要考慮子類的修改,而且在缺乏規范的環境下,這種修改可能帶來非常糟糕的結果————大段的代碼需要重構。

最終我們看一下類圖:

老貓覺得里氏替換原則是最難把握好的,所以到后續咱們再進行深入設計模式回歸的時候再做深入探究。

合成復用原則

合成復用原則(Composite/Aggregate Reuse Principle,英文簡稱CARP)是指咱們盡量要使用對象組合而不是繼承關系達到軟件復用的目的。這樣的話系統就可以變得更加靈活,同時也降低了類和類之間的耦合度。

看個例子,當我們剛學java的時候都是從jdbc開始學起來的。所以對于DBConnection我們并不陌生。那當我們實現基本產品Dao層的時候,我們就有了如下寫法:

public class DBConnection {
    public String getConnection(){
        return "獲取數據庫鏈接";
    }
}
//基礎產品dao層
public class ProductDao {
    private DBConnection dbConnection;

    public ProductDao(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void saveProduct(){
        String conn = dbConnection.getConnection();
        System.out.println("使用"+conn+"新增商品");
    }
}

上述就是最簡單的合成服用原則應用場景。但是這里有個問題,DBConnection目前只支持mysql一種連接DB的方式,顯然不合理,有很多企業其實還需要支持Oracle數據庫連接,所以為了符合之前說到的開閉原則,我們讓DBConnection交給子類去實現。于是我們可以將其定義成抽象方法。

public abstract class DBConnection {
    public abstract String getConnection();
}
//mysql鏈接
public class MySqlConnection extends DBConnection{
    @Override
    public String getConnection() {
        return "獲取mysql鏈接";
    }
}
//oracle鏈接
public class OracleConnection extends DBConnection{
    @Override
    public String getConnection() {
        return "獲取Oracle鏈接方式";
    }
}

最終的實現方式我們一起看一下類圖。

CARP

總結

之前看過一個故事,一棟樓的破敗往往從一扇破窗戶開始,慢慢腐朽。其實代碼的腐爛其實也是一樣,往往是一段拓展性極差的代碼開始。所以這要求我們研發人員還是得心中有桿“設計原則”的秤,咱們可能不會去做刻意的代碼設計,但是相信有這么一桿原則的秤,代碼也不至于會寫得太爛。

當然我們也不要刻意去追求設計原則,要權衡具體的場景做出合理的取舍。設計原則是設計模式的基礎,相信大家在了解完設計原則之后對后續的設計模式會有更加深刻的理解。

責任編輯:趙寧寧 來源: 程序員老貓
相關推薦

2010-07-16 11:12:40

云計算爭議

2022-12-26 18:53:00

MQ宕機倉儲服務

2020-11-10 07:08:08

API程序外接口

2016-11-28 09:06:45

前端系統開發

2021-10-17 22:24:57

芯片數據技術

2025-06-03 08:05:00

設計原則開發代碼

2024-08-16 14:01:00

2016-03-29 09:59:11

JavaScriptAPI設計

2011-11-09 13:59:27

代碼腐爛

2021-07-05 06:51:43

流程代碼結構

2013-04-17 10:46:54

面向對象

2012-05-08 10:14:45

設計原則

2012-03-15 11:15:13

Java設計模式

2013-06-09 11:04:07

設計扁平化設計平面化設計

2010-10-11 11:25:26

MySQL主鍵

2012-06-07 10:11:01

面向對象設計原則Java

2017-06-19 14:21:01

JavaScriptAPI設計原則

2011-06-01 10:58:57

2012-03-05 13:58:34

設計模式里氏置換

2012-03-07 10:40:19

Java設計模式
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚州精品天堂中文字幕 | 日本手机看片 | 免费在线看a | 国产高潮好爽受不了了夜夜做 | 国产精品精品久久久 | 国产男人的天堂 | 成人av一区二区三区 | 国产一级一级 | 天天艹| 2019天天干夜夜操 | 久久久久久久久久久久久久国产 | 国产激情综合五月久久 | 久久中文字幕一区 | 91精品久久久久久久久中文字幕 | 亚洲美女视频 | 久久aⅴ乱码一区二区三区 91综合网 | 亚洲国产aⅴ精品 | 人人干免费 | 国产成人免费在线观看 | 成人免费视频网站在线看 | 天堂一区二区三区四区 | 伊人手机在线视频 | 久久精品在线播放 | 精品国产99| 久久久国产精品 | 成人深夜小视频 | 久久综合狠狠综合久久 | 日韩在线中文字幕 | 欧美精品一二区 | 夜夜撸av| 日本亚洲一区二区 | 国产aⅴ精品 | 亚洲精品99 | 日韩在线播放一区 | 欧美日韩一二三区 | 成人免费久久 | 91视视频在线观看入口直接观看 | 一区二区在线观看免费视频 | 91av在线免费观看 | 亚洲精品乱码久久久久久蜜桃 | 亚洲成人在线网 |