當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]我們繼續(xù)學(xué)習(xí)架構(gòu)師技能,今天是本系列的第二篇,希望大家持續(xù)關(guān)注。


寫在前面

我們繼續(xù)學(xué)習(xí)架構(gòu)師技能,今天是本系列的第二篇,希望大家持續(xù)關(guān)注。

可能你不是科班出生,甚至大學(xué)都沒(méi)念,沒(méi)背景沒(méi)關(guān)系。我們只要每天進(jìn)步一點(diǎn)點(diǎn),一個(gè)月、兩個(gè)月、半年、一年......。

規(guī)劃性的學(xué)習(xí)一年半載后,你會(huì)覺(jué)得開始的你是多么的無(wú)知,如若不信,你可試試看!

只要你肯努力,遲早彎道超車!

坐穩(wěn)了,開始發(fā)車

網(wǎng)上都流行那么個(gè)段子:搞死一個(gè)程序員,不用動(dòng)刀或槍,只需要修改幾次需求。

開玩笑,確定嗎?

我也是程序員,也和相關(guān)人員互懟過(guò),不過(guò)都只是動(dòng)口不動(dòng)手。確實(shí),很多時(shí)候,相關(guān)人員提的需求確實(shí)有點(diǎn)過(guò)分,因?yàn)橐紤]到工期以及現(xiàn)有資源,有句話叫做“你給我做夠資源和時(shí)間,我可以給你造飛機(jī)火箭!”。

話又說(shuō)回來(lái),看這篇文章的相信都是程序員,咱們都是一路人。但,我想說(shuō)其實(shí)很多時(shí)候,我們也存在問(wèn)題。

比如:系統(tǒng)設(shè)計(jì)的可擴(kuò)展性、可維護(hù)性等,大家是否真的有認(rèn)真想過(guò),甚至部分人連軟件設(shè)計(jì)的七大原則都還搞不清楚。

不過(guò),很多人也是想好好設(shè)計(jì)的,但是無(wú)賴,給你工期不夠,否則,你需要加班,甚至加班估計(jì)夠嗆,從而也就導(dǎo)致系統(tǒng)逐漸變的非常難以維護(hù),臃腫,同樣的功能有n套代碼,到最后就是推翻了出個(gè)新版本(長(zhǎng)痛不如短痛)。

但,牛逼的人,只要工期不是很離譜,他們寫代碼永遠(yuǎn)是看起來(lái)非常舒服、還給你預(yù)留了很多擴(kuò)展口子、封裝了很多公用的工具類、抽象出了很多模型等等。

綜上,個(gè)人建議同行朋友,尤其是三年左右的,這時(shí)候,知識(shí)的廣度,深度都要有所涉及,同時(shí),系統(tǒng)設(shè)計(jì)或者某個(gè)模塊的設(shè)計(jì)也是體現(xiàn)你的能力的點(diǎn)(領(lǐng)導(dǎo)交給你的某個(gè)模塊,其實(shí)也可以理解為一個(gè)系統(tǒng),所以還是要認(rèn)真對(duì)待,大項(xiàng)目也是有多個(gè)模塊組成的)。希望大家一定多體會(huì)&&領(lǐng)會(huì)軟件設(shè)計(jì)的七大原則。

牛人們的總結(jié)

Robert C.Martin

一個(gè)可維護(hù)性(Maintainability)較低的軟件設(shè)計(jì),通常由于以下4個(gè)原因造成

1、過(guò)于僵化(Rigidity):設(shè)計(jì)難以修改

2、過(guò)于脆弱(Fragility):設(shè)計(jì)易遭到破壞(需要修改的時(shí)候,容易牽一發(fā)而動(dòng)全身,不該受到影響的代碼也被迫的破壞掉)

3、牢固性(Immobility):復(fù)用率低(當(dāng)想使用一些功能時(shí)會(huì)發(fā)現(xiàn)里面的某些代碼不是他想要的,想把這些代碼去掉時(shí),發(fā)現(xiàn)沒(méi)辦法去掉,原因是代碼耦合度太高了)

4、粘度過(guò)高(Viscosity):難以做正確事情(維護(hù)的過(guò)程中想進(jìn)行修改某些代碼,但是發(fā)現(xiàn)沒(méi)有辦法進(jìn)行修改,原因就是粘度太高)

PeterCoad

一個(gè)好的系統(tǒng)設(shè)計(jì)應(yīng)該具備如下三個(gè)特性:

  • 可擴(kuò)展性(Extendibility)
  • 靈活性(Flexibility)
  • 可插入性(Pluggability)

面向?qū)ο笤O(shè)計(jì)原則和設(shè)計(jì)模式也是對(duì)系統(tǒng)進(jìn)行合理重構(gòu),重構(gòu)是在不改變軟件現(xiàn)有功能的基礎(chǔ)上,通過(guò)調(diào)整代碼改善軟件的質(zhì)量、性能,使其程序的設(shè)計(jì)模式和架構(gòu)更趨合理性,提高軟件的擴(kuò)展性和維護(hù)性。

軟件設(shè)計(jì)七大原則

  • 開閉原則
  • 依賴倒置原則
  • 單一職責(zé)原則
  • 接口隔離原則
  • 迪米特法則
  • 里氏替換原則
  • 合成復(fù)用原則

關(guān)于這個(gè)七大設(shè)計(jì)原則,相信大部分人也聽說(shuō)過(guò),甚至很多朋友都學(xué)習(xí)過(guò),但是始終沒(méi)有掌握,希望通過(guò)本文的分享,不敢說(shuō)你一定能掌握,但是至少掌握部分。

本文主要內(nèi)容就是軟件設(shè)計(jì)的七大原則,重點(diǎn)在于用代碼來(lái)演示。

PS:七種原則并不是孤立存在的,他們相互依賴,相互補(bǔ)充。

開閉原則

開閉原則(Open Closed Principle,OCP)由勃蘭特·梅耶提出,他在 1988 年的著作《面向?qū)ο筌浖?gòu)造》中提出:

軟件實(shí)體應(yīng)當(dāng)對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉

這就是開閉原則的經(jīng)典定義。

在現(xiàn)實(shí)生活中,開閉原則也有體現(xiàn)。比如,很多互聯(lián)網(wǎng)公司都實(shí)行彈性制作息時(shí)間,規(guī)定每天工作8小時(shí)。意思就是,對(duì)于每天工作8小時(shí)這個(gè)規(guī)定是關(guān)閉的,但是什么時(shí)候來(lái)、什么時(shí)候走是開放的。早來(lái)早走,晚來(lái)晚走。

作用

開閉原則是面向?qū)ο蟪绦蛟O(shè)計(jì)的終極目標(biāo),它使軟件實(shí)體擁有一定的適應(yīng)性和靈活性的同時(shí)具備穩(wěn)定性和延續(xù)性。具體來(lái)說(shuō),其作用如下。

  1. 對(duì)軟件測(cè)試的影響:軟件遵守開閉原則的話,軟件測(cè)試時(shí)只需要對(duì)擴(kuò)展的代碼進(jìn)行測(cè)試就可以了,因?yàn)樵械臏y(cè)試代碼仍然能夠正常運(yùn)行。
  2. 可以提高代碼的可復(fù)用性:粒度越小,被復(fù)用的可能性就越大;在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,根據(jù)原子和抽象編程可以提高代碼的可復(fù)用性。
  3. 可以提高軟件的可維護(hù)性:遵守開閉原則的軟件,其穩(wěn)定性高和延續(xù)性強(qiáng),從而易于擴(kuò)展和維護(hù)。

實(shí)際案例

報(bào)名一個(gè)網(wǎng)上課程,課程有價(jià)格、id、名稱。

//課程接口類 public interface ICourse { String getCourseName(); Integer getCourseId(); BigDecimal getCoursePrice();
} //整個(gè)課程生態(tài)有Java架構(gòu)、大數(shù)據(jù)、人工智能、前端、軟件測(cè)試等。 //我們創(chuàng)建一個(gè)Java架構(gòu)課程的類JavaCourse。 public class JavaCourse implements ICourse { @Override public String getCourseName() { return "JAVA課程";
    } @Override public Integer getCourseId() { return 1;
    } @Override public BigDecimal getCoursePrice() { return new BigDecimal("599");
    }
} public class OpenCloseDemo { public static void main(String[] args) {
        ICourse course = new JavaCourse();
        System.out.println("課程ID=" + course.getCourseId());
        System.out.println("課程名稱=" + course.getCourseName());
        System.out.println("課程價(jià)格=" + course.getCoursePrice());
    }
}

運(yùn)行OpenCloseDemo的main方法,結(jié)果:

課程ID=1
課程名稱=JAVA課程
課程價(jià)格=599

現(xiàn)在要給Java架構(gòu)課程做活動(dòng),價(jià)格優(yōu)惠,比如雙11、618等節(jié)日搞促銷活動(dòng)。如果修改JavaCourse中的getPrice()方法,則會(huì)存在一定風(fēng)險(xiǎn),可能影響其他地方的調(diào)用結(jié)果。

如何在不修改原有代碼的前提下,實(shí)現(xiàn)價(jià)格優(yōu)惠這個(gè)功能呢?我們?cè)賹懸粋€(gè)處理優(yōu)惠邏輯的類——JavaDiscountCourse類(可以思考一下為什么要叫JavaDiscountCourse,而不叫DiscountCourse)。

于是我們就這么干,增加一個(gè)java課程的打折類。

public class JavaDiscountCourse extends JavaCourse { public BigDecimal getDiscountCoursePrice(BigDecimal discount) { return super.getCoursePrice().multiply(discount);
    }
} public class OpenCloseDemo { public static void main(String[] args) {
        JavaCourse course = new JavaDiscountCourse();
        DiscountJavaCourse discountJavaCourse = (DiscountJavaCourse) course;
        System.out.println("課程ID=" + course.getCourseId());
        System.out.println("課程名稱=" + course.getCourseName());
        System.out.println("課程價(jià)格=" + course.getCoursePrice());
        BigDecimal discount = new BigDecimal(0.5);
        System.out.println("課程折后價(jià)=" + discountJavaCourse.getDiscountCoursePrice()discount;
    }
}

運(yùn)行結(jié)果:

課程ID=1
課程名稱=JAVA課程
課程價(jià)格=599
課程折后價(jià)=299.5

這樣的話,我們就沒(méi)必要?jiǎng)覬avaCourse這個(gè)類了,其他地方可能還在使用這個(gè)JavaCourse中的價(jià)格。

依賴倒置原則

定義

依賴倒置原則(Dependence Inversion Principle,DIP)指設(shè)計(jì)代碼結(jié)構(gòu)時(shí),高層模塊不應(yīng)該依賴底層模塊,二者都應(yīng)該依賴其抽象。抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。通過(guò)依賴倒置,可以降低類與類之間的耦合性,提高系統(tǒng)的穩(wěn)定性,提高代碼的可讀性和可維護(hù)性,并降低修改程序帶來(lái)的風(fēng)險(xiǎn)。

案例

我們來(lái)看一個(gè)案例,還是以課程為例,首先創(chuàng)建一個(gè)類Tian。

public class Tian { public void studyJavaCourse(){
        System.out.println("老田在學(xué)java課程");
    } public void studyCCourse(){
        System.out.println("老田在學(xué)C課程");
    }
}

然后編寫客戶端測(cè)試代碼并調(diào)用。

public static void main(String[] args) {
    Tian tian = new Tian();
    tian.studyJavaCourse();
    tian.studyCCourse();
}

隨著學(xué)習(xí)興趣的暴漲,老田還想學(xué)習(xí)AI課程。

這個(gè)時(shí)候,需要業(yè)務(wù)擴(kuò)展,代碼要從底層到高層(調(diào)用層)一次修改代碼。在Tian類中增加studyAICourse()的方法,在高層也要追加調(diào)用。如此一來(lái),在系統(tǒng)發(fā)布以后,實(shí)際上是非常不穩(wěn)定的,在修改代碼的同時(shí)會(huì)帶來(lái)意想不到的風(fēng)險(xiǎn)。因此我們優(yōu)化代碼,首先創(chuàng)建一個(gè)課程的抽象接口ICourse。

public interface ICourse { void study();
}

然后寫JavaCourse類。

public class JavaCourse implements ICourse { @Override public void study() {
        System.out.println("老田在學(xué)習(xí)java架構(gòu)師課程");
    }
}

再實(shí)現(xiàn)PythonCourse類。

public class PythonCourse implements ICourse { @Override public void study() {
        System.out.println("老田在學(xué)習(xí)Python課程");
    }
}

最后修改Tian類。

public class Tian { public void study(ICourse course) {
        course.study();
    }
}

來(lái)看客戶端測(cè)試代碼。

public static void main(String[] args) {
    Tian tian = new Tian();
    ICourse course = new JavaCourse();
    tian.study(course);
}

這時(shí)候再看代碼,老田的興趣無(wú)論怎么暴漲,對(duì)于新的課程,只需要新建一個(gè)類,通過(guò)傳參的方式告訴Tian,而不需要修改底層代碼。實(shí)際上,這是一種大家非常熟悉的方式,叫作依賴注入。

  • 構(gòu)造器注入方式
  • Setter注入方式。

下面來(lái)看構(gòu)造器注入方式。

public class Tian { private ICourse course; /** 構(gòu)造函數(shù)方式注入course **/ public Tian(ICourse course) { this.course = course;
    } public void study() {
        course.study();
    }
}

來(lái)看客戶端代碼,將JavaCourse對(duì)象作為Tian對(duì)象的構(gòu)造參數(shù)注入。

public static void main(String[] args) {
        Tian tian = new Tian(new JavaCourse());
        tian.study();
    }
}

根據(jù)構(gòu)造器注入方式,當(dāng)調(diào)用時(shí),每次都要?jiǎng)?chuàng)建實(shí)例。

但,如果Tian是全局單例,則只能選擇Setter注入方式,繼續(xù)修改Tian類的代碼。

public class Tian { private ICourse course; public void setCourse(ICourse course) { this.course = course;
    } public void study() {
        course.study();
    }
}

來(lái)看客戶端代碼,調(diào)用Tian對(duì)象的setCourse()方法,將JavaCourse對(duì)象作為參數(shù)。

public static void main(String[] args) {
        Tian tian = new Tian();
        tian.setCourse(new JavaCourse());
        tian.study();

        tian.setCourse(new PythonCourse());
        tian.study();
    }
}

注:

以抽象為基準(zhǔn)比以細(xì)節(jié)為基準(zhǔn)搭建起來(lái)的架構(gòu)要穩(wěn)定得多,因此大家在拿到需求后,要面向接口編程,按照先頂層再細(xì)節(jié)的順序設(shè)計(jì)代碼結(jié)構(gòu)。

單一職責(zé)原則

定義

單一職責(zé)原則的定義單一職責(zé)原則(Simple Responsibility Principle,SRP)指不要存在一個(gè)以上導(dǎo)致類變更的原因。

假設(shè)有一個(gè)Class負(fù)責(zé)兩個(gè)職責(zé),一旦發(fā)生需求變更,修改其中一個(gè)職責(zé)的邏輯代碼,有可能會(huì)導(dǎo)致另一個(gè)職責(zé)的功能發(fā)生故障。這樣一來(lái),這個(gè)Class就存在兩個(gè)導(dǎo)致類變更的原因。

如何解決這個(gè)問(wèn)題呢?我們就要分別用兩個(gè)Class來(lái)實(shí)現(xiàn)兩個(gè)職責(zé),進(jìn)行解耦。后期需求變更維護(hù)互不影響。這樣的設(shè)計(jì),可以降低類的復(fù)雜度,提高類的可讀性,提高系統(tǒng)的可維護(hù)性,降低變更引起的風(fēng)險(xiǎn)。

總體來(lái)說(shuō)就是一個(gè)Class、Interface、Method只負(fù)責(zé)一項(xiàng)職責(zé)。

案例

我們來(lái)看代碼實(shí)例,還是用課程舉例,我們的課程有直播課和錄播課。直播課不能快進(jìn)和快退,錄播課可以任意地反復(fù)觀看,功能職責(zé)不一樣。首先創(chuàng)建一個(gè)Course類。

public class ICourse { public void study(String courseName){ if("直播課".equals(courseName)){
            System.out.println("不能快進(jìn)哦");
        }else{
            System.out.println("可以自定義播放速度,已經(jīng)來(lái)回播放");
        }
    }
}

然后看客戶端代碼,無(wú)論是直播課還是錄播課,都調(diào)用study()方法的邏輯。

public class Test1 { public static void main(String[] args) {
        Course course = new Course();
        course.study("直播課");
        course.study("看錄像");
    }
}

從上面代碼來(lái)看,Course類承擔(dān)了兩種處理邏輯。

假如,現(xiàn)在對(duì)課程進(jìn)行加密,那么直播課和錄播課的加密邏輯是不一樣的,必須修改代碼。

而修改代碼邏輯勢(shì)必會(huì)相互影響,容易帶來(lái)不可控的風(fēng)險(xiǎn)。

我們對(duì)職責(zé)進(jìn)行分離解耦,分別創(chuàng)建兩個(gè)類LiveCourse和ReplayCourse。

LiveCourse直播課程:

public class LiveCourse { public void study(String courseName){
        System.out.println("現(xiàn)場(chǎng)直播,無(wú)法修改播放速度");
    }
}

ReplayCourse重播或錄像課程:

public class ReplayCourse { public void study(String courseName){
        System.out.println("看錄像,可以隨便切換播放速度,以及來(lái)回播放");
    }
}

客戶端代碼如下,將直播課的處理邏輯調(diào)用LiveCourse類,錄播課的處理邏輯調(diào)用ReplayCourse類。

public class Test2 { public static void main(String[] args) {
        LiveCourse course = new LiveCourse();
        course.study("直播課");

        ReplayCourse replayCourse=new ReplayCourse();
        replayCourse.study("錄播課");
    }
}

當(dāng)業(yè)務(wù)繼續(xù)發(fā)展時(shí),要對(duì)課程做權(quán)限。沒(méi)有付費(fèi)的學(xué)員可以獲得課程的基本信息,已經(jīng)付費(fèi)的學(xué)員可以獲得視頻流,即學(xué)習(xí)權(quán)限。

那么對(duì)于控制課程層面,至少有兩個(gè)職責(zé)。我們可以把展示職責(zé)和管理職責(zé)分離開,都實(shí)現(xiàn)同一個(gè)抽象依賴。

設(shè)計(jì)一個(gè)頂層接口,創(chuàng)建ICourse接口。

public interface ICourse { //獲得課程的基本信息 String getCourseName(); //獲取視頻流 byte[] getCourseVioeo(); //學(xué)習(xí)課程 void studyCourse(); //退款 void refundCourse();
}

可以把這個(gè)接口拆成兩個(gè)接口,創(chuàng)建一個(gè)接口ICourseInfo和ICourseManager。ICourseInfo接口的代碼如下。

ICourseInfo接口的代碼如下:

public interface ICourseInfo { //獲取課程名稱 String getCourseName(); //獲取課程視頻流 byte [] getCourseVideo();
}

ICourseManager接口的代碼如下:

public interface ICourseManager { //學(xué)習(xí)課程 void studyCourse(); //退款 void refundCourse();
}

下面來(lái)看方法層面的單一職責(zé)設(shè)計(jì)。有時(shí)候,為了偷懶,通常會(huì)把一個(gè)方法寫成下面這樣。

public void modifyUserInfo(String userName, String address){
    userName = userName;
    address = address;
}

還可能寫成這樣:

private void modifyUserInfo(String userName, String... fields){
    userNmae = userName;
} private void modifyUserInfo(String userName, String address,boolean flag){ if(flag){ //.... }else{ //.... }
    userName = userName;
    address = address;
}

顯然,上面兩種寫法的modifyUserInfo()方法都承擔(dān)了多個(gè)職責(zé),既可以修改userName,也可以修改address,甚至更多,明顯不符合單一職責(zé)原則。那么我們做如下修改,把這個(gè)方法拆成兩個(gè)。

private void modifyUserName(String userName){
    userName = userName;
} private void modifyAddress(String address){
   address = address;
}

代碼在修改之后,開發(fā)起來(lái)簡(jiǎn)單,維護(hù)起來(lái)也容易。在實(shí)際項(xiàng)目中,代碼會(huì)存在依賴、組合、聚合關(guān)系,在項(xiàng)目開發(fā)過(guò)程中還受到項(xiàng)目的規(guī)模、周期、技術(shù)人員水平、對(duì)進(jìn)度把控的影響,導(dǎo)致很多類都不能滿足單一職責(zé)原則。但是,我們?cè)诰帉懘a的過(guò)程中,盡可能地讓接口和方法保持單一職責(zé),對(duì)項(xiàng)目后期的維護(hù)是有很大幫助的。

接口隔離原則

定義

接口隔離原則的定義接口隔離原則(Interface Segregation Principle,ISP)指用多個(gè)專門的接口,而不使用單一的總接口,客戶端不應(yīng)該依賴它不需要的接口。

這個(gè)原則指導(dǎo)我們?cè)谠O(shè)計(jì)接口時(shí),應(yīng)當(dāng)注意以下幾點(diǎn)。

(1)一個(gè)類對(duì)另一個(gè)類的依賴應(yīng)該建立在最小接口上。

(2)建立單一接口,不要建立龐大臃腫的接口。

(3)盡量細(xì)化接口,接口中的方法盡量少(不是越少越好,一定要適度)。

接口隔離原則符合“高聚合、低耦合”的設(shè)計(jì)思想,使得類具有很好的可讀性、可擴(kuò)展性和可維護(hù)性。在設(shè)計(jì)接口的時(shí)候,要多花時(shí)間思考,要考慮業(yè)務(wù)模型,包括還要對(duì)以后可能發(fā)生變更的地方做一些預(yù)判。所以,在實(shí)際開發(fā)中,我們對(duì)抽象、業(yè)務(wù)模型的理解是非常重要的。

案例

我們來(lái)寫一個(gè)動(dòng)物行為的抽象。

IAnimal接口的代碼如下:

public interface IAnimal{ //吃 void eat(); //飛 void fly(); //游泳 void swim();
}

Bird實(shí)現(xiàn)類代碼如如下:

public class Bird implements IAnimal{ //吃 @Override void eat(){ //小鳥吃東西 } //飛 @Override void fly(){ //小鳥在飛 } //游泳 void swim(){ //空著,因?yàn)樾▲B不游泳 }
}

Dog類實(shí)現(xiàn)的代碼如下。

public class Dog implements IAnimal{ //吃 @Override void eat(){ //小狗吃東西 } //飛 @Override void fly(){ //空著,因?yàn)樾」凡粫?huì)飛 } //游泳 void swim(){ //小狗在游泳 }
}

由上面代碼可以看出,Bird類的swim()方法可能只能空著,Dog類的fly()方法顯然是不可能的。這時(shí)候,我們針對(duì)不同動(dòng)物的行為來(lái)設(shè)計(jì)不同的接口,分別設(shè)計(jì)IEatAnimal、IFlyAnimal和ISwimAnimal接口。

IEatAnimal接口的代碼如下:

public interface IEatAnimal{ //吃 void eat(); 
}

IFlyAnimal接口的代碼如下:

public interface IFlyAnimal{ //飛 void fly(); 
}

ISwimAnimal接口的代碼如下:

public interface ISwimAnimal{ //游泳 void swim(); 
}

Dog只實(shí)現(xiàn)IEatAnimal和ISwimAnimal接口。

public class Dog implements IEatAnimal、ISwimAnimal{ //吃 @Override void eat(){ //小狗吃東西 } //游泳 void swim(){ //小狗在游泳 }
}

迪米特法則

迪米特法則的定義

迪米特法則(Law of Demeter,LoD)又叫作最少知道原則(Least KnowledgePrinciple,LKP),指一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少的了解,盡量降低類與類之間的耦合。迪米特法則主要強(qiáng)調(diào)只和朋友交流,不和陌生人說(shuō)話。出現(xiàn)在成員變量、方法的輸入和輸出參數(shù)中的類都可以被稱為成員朋友類,而出現(xiàn)在方法體內(nèi)部的類不屬于朋友類。

案例

我們來(lái)設(shè)計(jì)一個(gè)權(quán)限系統(tǒng),TeamLeader需要查看目前發(fā)布到線上的課程數(shù)量。這時(shí)候,TeamLeader要讓Employee去進(jìn)行統(tǒng)計(jì),Employee再把統(tǒng)計(jì)結(jié)果告訴TeamLeader,來(lái)看代碼。

Course類的代碼如下:

public class Course{
   
}

Employee類的代碼如下:

public class Employee{ public void checkNumberOfCourse(ListcourseList){
        System.out.println("目前已經(jīng)發(fā)布的課程數(shù)量是:" + courseList.size());
    }
}

TeamLeader類的代碼如下:

public class TeamLeader{ public void commandCheckNumber(Employee employee){
        ListcourseList = new ArrayList(); for(int i=0;i<20;i++){
            courseList.add(new Course());
        }
        employee.checkNumberOfCourse(courseList);
    }
}

客戶端測(cè)試代碼如下,將Employee對(duì)象作為參數(shù)傳送給TeamLeader對(duì)象

public static void main(String [] args){
    TeamLeader teamLeader = new TeamLeader();
    Employee employee = new Employee();
    teamLeader.commandCheckNumber(employee);
}

寫到這里,其實(shí)功能都已經(jīng)實(shí)現(xiàn),代碼看上去也沒(méi)什么問(wèn)題。根據(jù)迪米特法則,TeamLeader只想要結(jié)果,不需要跟Course產(chǎn)生直接交流。而Employee統(tǒng)計(jì)需要引用Course對(duì)象,TeamLeader和Course并不是朋友,從如下圖所示的類圖就可以看出來(lái)。

改造

Employee類的代碼如下。

public class Employee{ public void checkNumberOfCourse(ListcourseList){
        ListcourseList = new ArrayList(); for(int i=0;i<20;i++){
            courseList.add(new Course());
        }
        System.out.println("目前已經(jīng)發(fā)布的課程數(shù)量是:" + courseList.size());
    }
}

TeamLeader類的代碼如下。

ublic class TeamLeader{ public void commandCheckNumber(Employee employee){ 
        employee.checkNumberOfCourse(courseList);
    }
}

學(xué)習(xí)軟件設(shè)計(jì)原則,千萬(wàn)不能形成強(qiáng)迫癥。當(dāng)碰到業(yè)務(wù)復(fù)雜的場(chǎng)景時(shí),需要隨機(jī)應(yīng)變。

里氏替換原則

定義

里氏替換原則(Liskov Substitution Principle,LSP)指如果對(duì)每一個(gè)類型為T1的對(duì)象O1,都有類型為T2的對(duì)象O2,使得以T1定義的所有程序P在所有對(duì)象O1都替換成O2時(shí),程序P的行為沒(méi)有發(fā)生變化,那么類型T2是類型T1的子類型。

定義看上去比較抽象,我們重新解釋一下,可以理解為一個(gè)軟件實(shí)體如果適用于一個(gè)父類,則一定適用于其子類,所有引用父類的地方必須能透明地使用其子類的對(duì)象,子類對(duì)象能夠替換父類對(duì)象,而程序邏輯不變。也可以理解為,子類可以擴(kuò)展父類的功能,但不能改變父類原有的功能。根據(jù)這個(gè)理解,我們對(duì)里氏替換原則的定義總結(jié)如下。

(1)子類可以實(shí)現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法。

(2)子類中可以增加自己特有的方法。

(3)當(dāng)子類的方法重載父類的方法時(shí),方法的前置條件(即方法的輸入?yún)?shù))要比父類的方法更寬松。

(4)當(dāng)子類的方法實(shí)現(xiàn)父類的方法時(shí)(重寫/重載或?qū)崿F(xiàn)抽象方法),方法的后置條件(即方法的輸出/返回值)要比父類的方法更嚴(yán)格或相等。

案例

在講開閉原則的時(shí)候,我們埋下了一個(gè)伏筆。我們?cè)讷@取折扣價(jià)格后重寫覆蓋了父類的getPrice()方法,增加了一個(gè)獲取源碼的方法getOriginPrice(),這顯然違背了里氏替換原則。我們修改一下代碼,不應(yīng)該覆蓋getPrice()方法,增加getDiscountPrice()方法。

public class JavaDiscountCourse extends JavaCourse{ public JavaDiscountCourse(Integer id, String name,Double price){ super(id,name,price);
    } public Double getDiscountPrice(){ return suer.getPrice()*0.5 }
}

使用里氏替換原則有以下優(yōu)點(diǎn):

  • 約束繼承泛濫,是開閉原則的一種體現(xiàn)。
  • 加強(qiáng)程序的健壯性,同時(shí)變更時(shí)可以做到非常好的兼容性,提高程序的維護(hù)性、可擴(kuò)展性,降低需求變更時(shí)引入的風(fēng)險(xiǎn)。

現(xiàn)在來(lái)描述一個(gè)經(jīng)典的業(yè)務(wù)場(chǎng)景,用正方形、矩形和四邊形的關(guān)系說(shuō)明里氏替換原則,我們都知道正方形是一個(gè)特殊的長(zhǎng)方形,那么可以創(chuàng)建一個(gè)長(zhǎng)方形的父類Rectangle類,代碼如下。

public class Rectangle{ private long height; private long width; //set get方法省略 }

創(chuàng)建正方形Square類繼承長(zhǎng)方形,代碼如下:

public class Square extends Rectangle{ private long length; //length的get set方法  @Override public long getHeight(){ return getLength();
    } @Override public void setHeight(long height){
        setLength(height);
    } @Override public long getWidth(){ return getLength();
    } @Override public void setWidth(long width){
        setLength(width);
    }
}

在測(cè)試類中,創(chuàng)建resize()方法。根據(jù)邏輯,長(zhǎng)方形的寬應(yīng)該大于等于高,我們讓高一直自增,直到高等于寬變成正方形,代碼如下。

public class Test { public static void resize(Rectangle rectangle) { while (rectangle.getWidth() >= rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width=" + rectangle.getWidth() + " height=" + rectangle.getHeight());
        }
        System.out.println("Resize end,width=" + rectangle.getWidth() + " height=" + rectangle.getHeight());
    } public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(20);
        rectangle.setHeight(10);
        resize(rectangle);
    }
}

運(yùn)行結(jié)果:

width=20 height=11
width=20 height=12
width=20 height=13
width=20 height=14
width=20 height=15
width=20 height=16
width=20 height=17
width=20 height=18
width=20 height=19
width=20 height=20
width=20 height=21
Resize end,width=20 height=21

由運(yùn)行結(jié)果可知,高比寬還大,這在長(zhǎng)方形中是一種非常正常的情況。再來(lái)看下面的代碼,把長(zhǎng)方形替換成它的子類正方形,修改客戶端測(cè)試代碼如下。

public class Test { public static void resize(Rectangle rectangle) { while (rectangle.getWidth() >= rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width=" + rectangle.getWidth() + "height=" + rectangle.getHeight());
        }
        System.out.println("Resize end,width=" + rectangle.getWidth() + "height=" + rectangle.getHeight());
    } public static void main(String[] args) {
        Square square = new Square();
        square.setWidth(20);
        square.setHeight(10);
        resize(square);
    }
}

此時(shí),運(yùn)行出現(xiàn)了死循環(huán),違背了里氏替換原則,在將父類替換為子類后,程序運(yùn)行結(jié)果沒(méi)有達(dá)到預(yù)期。因此,代碼設(shè)計(jì)是存在一定風(fēng)險(xiǎn)的。里氏替換原則只存在于父類與子類之間,約束繼承泛濫。

再來(lái)創(chuàng)建一個(gè)基于長(zhǎng)方形與正方形共同的抽象——四邊形QuardRangle接口,代碼如下。

public interface QuardRangle { long getHeight(); long getWidth();
}

修改長(zhǎng)方形Rectangle類的代碼如下。

public class Rectangle implements QuardRangle{ private long height; private long width; @Override public long getHeight() { return height;
    } public void setHeight(long height) { this.height = height;
    } @Override public long getWidth() { return width;
    } public void setWidth(long width) { this.width = width;
    }
}

修改正方形Square類的代碼如下。

public class Square implements QuardRangle{ private long length; public long getLength() { return length;
    } public void setLength(long length) { this.length = length;
    } @Override public long getHeight(){ return length;
    } @Override public long getWidth(){ return length;
    }

}

此時(shí),如果把resize()方法的參數(shù)換成四邊形QuardRangle類,方法內(nèi)部就會(huì)報(bào)錯(cuò)。因?yàn)檎叫我呀?jīng)沒(méi)有了setWidth()和setHeight()方法,所以,為了約束繼承泛濫,resize()方法的參數(shù)只能用長(zhǎng)方形Rectangle類。

合成復(fù)用原則

定義

合成復(fù)用原則(Composite/Aggregate Reuse Principle,CARP)指盡量使用對(duì)象組合(has-a)或?qū)ο缶酆希╟ontanis-a)的方式實(shí)現(xiàn)代碼復(fù)用,而不是用繼承關(guān)系達(dá)到代碼復(fù)用的目的。

合成復(fù)用原則可以使系統(tǒng)更加靈活,降低類與類之間的耦合度,一個(gè)類的變化對(duì)其他類造成的影響相對(duì)較小。繼承,又被稱為白箱復(fù)用,相當(dāng)于把所有實(shí)現(xiàn)細(xì)節(jié)暴露給子類。

組合/聚合又被稱為黑箱復(fù)用,對(duì)類以外的對(duì)象是無(wú)法獲取實(shí)現(xiàn)細(xì)節(jié)的。我們要根據(jù)具體的業(yè)務(wù)場(chǎng)景來(lái)做代碼設(shè)計(jì),其實(shí)也都需要遵循面向?qū)ο缶幊蹋∣bject OrientedProgramming,OOP)模型。

案例

還是以數(shù)據(jù)庫(kù)操作為例,首先創(chuàng)建DBConnection類。

public class DBConnection{ public String getConnection(){ return "數(shù)據(jù)庫(kù)連接";
    }
}

創(chuàng)建ProductDao類。

public class ProdcutDao{ private DBConnection dbConnection; public void setDBConnection(DBConnection dbConnection){ this.dbConnection=dbConnection;
    } public void addProduct(){
        String conn=dbConnection.getConnection();
        System.out.println("使用" + conn + "連接數(shù)據(jù)庫(kù)");
    }
}

這是一種非常典型的合成復(fù)用原則應(yīng)用場(chǎng)景。但是,對(duì)于目前的設(shè)計(jì)來(lái)說(shuō),DBConnection還不是一種抽象,不便于系統(tǒng)擴(kuò)展。目前的系統(tǒng)支持MySQL數(shù)據(jù)庫(kù)連接,假設(shè)業(yè)務(wù)發(fā)生變化,數(shù)據(jù)庫(kù)操作層要支持Oracle數(shù)據(jù)庫(kù)。

當(dāng)然,我們可以在DBConnection中增加對(duì)Oracle數(shù)據(jù)庫(kù)支持的方法,但是這違背了開閉原則。其實(shí),可以不必修改Dao的代碼,將DBConnection修改為abstract,代碼如下。

public abstract class DBConnection{ public abstract String getConnection();
}

然后將MySQL的邏輯抽離。

public class MySQLConnection extends DBConnection{ @Override public String getConnection(){ return "MySQL 數(shù)據(jù)庫(kù)連接";
    }
}

再創(chuàng)建Oracle支持的邏輯。

public class OracleConnection extends DBConnection{ @Override public String getConnection(){ return "Oracle 數(shù)據(jù)庫(kù)連接";
    }
}

總結(jié)

學(xué)習(xí)設(shè)計(jì)原則是學(xué)習(xí)設(shè)計(jì)模式的基礎(chǔ)。在實(shí)際開發(fā)過(guò)程中,并不是一定要求所有代碼都遵循設(shè)計(jì)原則,而是要綜合考慮人力、時(shí)間、成本、質(zhì)量,不刻意追求完美,要在適當(dāng)?shù)膱?chǎng)景遵循設(shè)計(jì)原則。這體現(xiàn)的是一種平衡取舍,可以幫助我們?cè)O(shè)計(jì)出更加優(yōu)雅的代碼結(jié)構(gòu)。





免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉