「設(shè)計(jì)原則 一」

「設(shè)計(jì)原則 」

一、開閉原則

顧名思義,在軟件設(shè)計(jì)中應(yīng)當(dāng)遵循對(duì)擴(kuò)展開放,而對(duì)修改關(guān)閉。也即在實(shí)際開發(fā)過程中,當(dāng)需求變動(dòng)業(yè)務(wù)調(diào)整時(shí),在不改動(dòng)源碼的情況下可以擴(kuò)展以支撐新的功能;這也要求了在設(shè)計(jì)之初制定技術(shù)方案時(shí)應(yīng)有前瞻性。

  • 遵循開閉原則的好處:提高代碼的復(fù)用性、可維護(hù)性、有利于單元測(cè)試。
  • 實(shí)現(xiàn):在面向?qū)ο蟮脑O(shè)計(jì)中,通??梢酝ㄟ^定義接口或者抽象類來(lái)約束相同屬性或者一般通用的實(shí)現(xiàn)(抽象),這樣具體派生實(shí)現(xiàn)類可以將具體的實(shí)現(xiàn)封裝在內(nèi)部。即使業(yè)務(wù)變化,我們只需要相應(yīng)的派生出一個(gè)實(shí)現(xiàn)類就可以實(shí)現(xiàn)擴(kuò)展。不過在實(shí)際中,這種對(duì)業(yè)務(wù)的抽象能力要求還是比較高的。如果抽象的粒度太小,那么會(huì)伴隨著繁雜的實(shí)現(xiàn)類;如果粒度太大卻不利于擴(kuò)展。經(jīng)驗(yàn)的積累與思考很重要。
1.實(shí)際問題

商品價(jià)格變動(dòng)模擬,如打折促銷、漲價(jià)等

  • 定義頂層的商品接口(僅僅包含ID、名稱、價(jià)格)
public interface Product {
  long getId();

  long getPrice();

  String getName();
}
  • 新建水果中香蕉的實(shí)現(xiàn)類
public class Banana implements Product {
    private  long id;
    private  long price;
    private  String name;

    public Banana(long id, long price, String name) {
        this.id = id;
        this.price = price;
        this.name = name;
    }

    public void setId(long id) {
        this.id = id;
    }

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

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public long getId() {
        return this.id;
    }

    @Override
    public long getPrice() {
        return this.price;
    }

    @Override
    public String getName() {
        return this.name;
    }
}
  • 香蕉不易保存的特性決定了,如果庫(kù)存較多只能打折進(jìn)行處理。

如果直接修改Banana實(shí)現(xiàn)類中價(jià)格getPrice()勢(shì)必會(huì)對(duì)其他的地方的調(diào)用產(chǎn)生影響,違背了開閉原則。因此增加BananaDiscountImp折扣類,當(dāng)然這其實(shí)也是不合理的,僅僅作為舉例,如果都是這種,會(huì)增加很多不必要的實(shí)現(xiàn)類,使得項(xiàng)目膨脹冗雜。

public class BananaDiscountImp extends Banana {

    public BananaDiscountImp(long id, long price, String name) {
        super(id, price, name);
    }

    /**
     * 原始價(jià)格
     */
    @Override
    public long getPrice() {
        return super.getPrice();
    }

    /**
     * 折后價(jià)格
     * (需借助BigDecimal轉(zhuǎn)換,包括保留小數(shù)位等,80相當(dāng)于8折)
     */
    public long getOriginalPrice() {
        return getPrice() * 80L;
    }
}
二、里氏替換原則
  • 含義:通俗的講在繼承過程中子類可以對(duì)基類的功能進(jìn)行擴(kuò)展,但不能改變基類原有的功能。在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,繼承作為三大特性之一。雖然帶來(lái)了很大的便利性,但同時(shí)也增加了耦合性,侵入性。
  • 里氏替換原則實(shí)際上更是對(duì)繼承過程中的一種規(guī)范與約束:1.子類可以增加自身特有的方法;2.子類可以實(shí)現(xiàn)基類的抽象方法,但不能覆蓋基類的非抽象方法;3.當(dāng)子類重載基類的方法時(shí),方法的入?yún)?yīng)該比基類更寬松;4.當(dāng)子類實(shí)現(xiàn)基類的抽象方法時(shí),方法的返回值應(yīng)該比基類更嚴(yán)格;5.如果子類必須重寫基類的方法時(shí),應(yīng)該考慮替換當(dāng)前的繼承關(guān)系,同時(shí)繼承更加一般的基類,或者使用組合、聚合、依賴等其他方式替代。
1.實(shí)際問題

比較經(jīng)典的“正方形非長(zhǎng)方形問題”;另外我們知道鴕鳥是不會(huì)飛的,但是奔跑的速度很快,以鴕鳥為例。

  • 頂層的抽象動(dòng)物類
public class Animal {
  /**
   * 米每秒
   */
  private long moveSpeed;

  public long getMoveTime(long distance) {
    return distance / moveSpeed;
  }

  public void setMoveSpeed(long moveSpeed) {
    this.moveSpeed = moveSpeed;
  }
}
  • 較為一般的鳥類
public class Bird extends Animal {

    private long flySpeed;

    public void setFlySpeed(long flySpeed) {
        this.flySpeed = flySpeed;
    }

    public long getFlyTime(long distance) {
        return distance / flySpeed;
    }
}

在定義的過程,無(wú)非就是根據(jù)一些鳥類的特性,比如有羽毛,會(huì)飛,有喙等等;但是往往會(huì)存在特例。鴕鳥除了沒有的能力其他都是包含的,如果繼承Bird類,當(dāng)求導(dǎo)飛行速度時(shí)勢(shì)必會(huì)出現(xiàn)錯(cuò)誤,因?yàn)轼r鳥的飛行速度為0。

  • 具體到某一種鳥類-麻雀
public class Sparrow extends Bird {
    
    @Override
    public void setFlySpeed(long flySpeed) {
        super.setFlySpeed(flySpeed);
    }
}
  • 鴕鳥類(錯(cuò)誤的繼承)
public class Ostrich extends Bird {

    @Override
    public void setFlySpeed(long flySpeed) {
        //鴕鳥的飛行速度為零,重寫了
        flySpeed = 0;
        super.setFlySpeed(flySpeed);
    }
}

當(dāng)測(cè)試時(shí),肯定是會(huì)出現(xiàn)系統(tǒng)異常的情況,這里違背了里氏替換的原則-不能覆蓋基類的非抽象方法;從而導(dǎo)致了錯(cuò)誤的結(jié)果,此時(shí)應(yīng)該考慮取消繼承關(guān)系,改為更加通用的基類,也即繼承Animal,動(dòng)物都有移動(dòng)的速度。

  • 鴕鳥類繼承Animal
public class Ostrich extends Animal {

    public Ostrich() {}

    @Override
    public void setMoveSpeed(long moveSpeed) {
        super.setMoveSpeed(moveSpeed);
    }

  public static void main(String[] args) {
    //測(cè)試
      Animal ostrich = new Ostrich();
      ostrich.setMoveSpeed(90);
  }
}
  • 實(shí)際開發(fā)的過程中應(yīng)避免對(duì)濫用繼承,實(shí)現(xiàn)子類時(shí)遵循里氏替換的原則能夠幫助我們對(duì)子類更好地約束,建立起更健壯、易維護(hù)的系統(tǒng)。當(dāng)然不遵循程序也能跑,隨著項(xiàng)目的復(fù)雜度增加,出現(xiàn)問題的概率也大大增加。
三、依賴倒置原則

高層結(jié)構(gòu)的模塊不應(yīng)該依賴低層結(jié)構(gòu)的模塊,二者都應(yīng)該依賴其抽象。抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。

1.一般含義
  • 通俗的解釋,依賴倒置的核心思想-面向接口編程。面向接口編程的好處不言而喻,相對(duì)于實(shí)現(xiàn)細(xì)節(jié)的多變性,抽象的概念則穩(wěn)定的多,很多同學(xué)包括自己在實(shí)際開發(fā)中有時(shí)候也會(huì)陷入到實(shí)現(xiàn)的細(xì)節(jié)中,試想以具體的實(shí)現(xiàn)類來(lái)構(gòu)建系統(tǒng)自然是不夠穩(wěn)定的,同樣不利于擴(kuò)展。對(duì)于這種,首先考慮的是制定抽象的接口、抽象類層,以接口來(lái)約束和規(guī)范實(shí)現(xiàn),而不關(guān)心具體的實(shí)現(xiàn)細(xì)節(jié)。
2.作用
  • 既然都面向了接口,類與類之間的耦合度降低了(依賴倒置原則降低了類之間的耦合度)。
  • 耦合度低,提高了系統(tǒng)的穩(wěn)定性(穩(wěn)定性)。
  • 抽象的規(guī)范與約束作用,提高了代碼的可維護(hù)性,可讀性,當(dāng)然既然存在繼承,那么在設(shè)計(jì)與實(shí)現(xiàn)的過程中應(yīng)遵循里氏替換原則(可維護(hù)性、可讀性)。
3.如何設(shè)計(jì)
  • 面向接口-盡量使用使用接口或者抽象類,或者兩者都包含來(lái)代替類傳遞。
  • 對(duì)于變量的申明類型盡量使用接口或者抽象類,而不是具體的實(shí)現(xiàn)類。
  • 繼承遵循里氏替換原則
4.實(shí)際問題

以大學(xué)生學(xué)習(xí)課程為例

  • 定義課程的接口
/**
 * Created by Sai
 * on: 05/01/2022 23:54.
 */
public interface ICourse {
    void selected();
}
  • 具體課程類-物理課
/**
 * Created by Sai
 * on: 05/01/2022 23:58.
 */
public class PhysicsCourse implements ICourse {

    @Override
    public void selected() {
        System.out.println("物理課被選修了");
    }
}
  • 具體課程類-英語(yǔ)課
/**
 * Created by Sai
 * on: 06/01/2022 00:00.
 */
public class EnglishCourse implements ICourse {

    @Override
    public void selected() {
        System.out.println("英語(yǔ)課被選修了");
    }
}
  • 學(xué)生類
/**
 * Created by Sai
 * on: 06/01/2022 00:01.
 */
public class Student {
    //依賴注入
    private ICourse course;
  
    public Student() {}

    public ICourse getCourse() {
        return course;
    }

    public void setCourse(ICourse course) {
        this.course = course;
    }

    public void study() {
        if (null != course) {
            course.selected();
        }
    }

  public static void main(String[] args) {
    Student stu = new Student();
    stu.setCourse(new EnglishCourse());
    stu.study();
    stu.setCourse(new PhysicsCourse());
    stu.study();
  }
}

//英語(yǔ)課被選修了
//物理課被選修了
//Process finished with exit code 0
  • 前面提到依賴倒置的核心-面向接口編程,理解了面向接口編程的含義與運(yùn)用,依賴倒置原則自然而然就掌握了,當(dāng)然這離不開實(shí)踐過程中的積累與思考。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容