「設(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í)踐過程中的積累與思考。