1.1 單一職責原則
單一職責原則:Singel Responsibility Principle,SRP
單一職責原則的定義:應(yīng)該有且僅有一個原因引起類的變更。
書中提到一個例子:對電話的抽象。

繼續(xù)細化,向下拆分,向上抽象
電話通話時可以抽象出4個過程:撥號、通話、回應(yīng)、掛機。
在舉著個例子的時候,作者提到大部分人可能都會說這個沒什么問題,動作定義比較清晰。其實如果更深層的了解電話的結(jié)構(gòu),應(yīng)該可以對電話類進行一個更完整的抽象。
比如:電話的通話過程,是否是自始至終都是這四個階段,在發(fā)展過程中會不會增加或者減少。如果以階段為維度來進行抽象,是否會出現(xiàn)經(jīng)常變更的情況。如果換一個維度,如:從更底層職責的角度來進行抽象,會抽象出“協(xié)議管理”和“數(shù)據(jù)傳輸” 兩個角度,無論中間階段發(fā)生什么變化,這兩個職責,是一個電話必須擁有的。
所以我們在對業(yè)務(wù)模型進行抽象定義時,也需要盡量的事無巨細的了解業(yè)務(wù)的模型,然后再做分析。
回歸原則定義
原則定義:應(yīng)該有且僅有一個原因引起類的變更。
上面的接口并不是“只有一個原因引起變化”的。IPhone 不只是只有一個職責,它包含兩個職責:
- 協(xié)議管理:dial() 和hangup() 方法負責撥號接通和掛機
- 數(shù)據(jù)傳送:chat() 實現(xiàn)數(shù)據(jù)傳送,把話轉(zhuǎn)換成信號在雙方之間傳遞
這兩個職責都會引起這個接口或者實現(xiàn)類的變化:
日常習慣
比如一個用戶信息接口:

這個接口有一個“修改用戶信息”的方法。這個方法太過于籠統(tǒng),一個方法承擔了多個職責,這樣的接口雖然對上層來說只提供了一個接口,但是它的職責并不是單一的。這樣做需要在接口文檔上做額外的注釋,說明這個修改接口都可以修改哪些信息,操作參數(shù)什么情況需要傳什么樣的值。在《代碼整潔之道》中建議過:提供好的注釋,不如將代碼寫的別人一看就明白,無需注釋。這里也一樣,一個好的接口的定義,不需要文檔中做長篇大論的調(diào)用說明。與其做一堆說明,不如在定義接口的時候,定義的清晰易懂。

這樣定義會對上層更友好一點,將修改用戶信息拆解為多個方法,每個方法只負責一件事,別人一看就知道,那個方法改的是什么,這個接口每個方法都能修改什么,清晰完整。
實際開發(fā)
雖然一直說要按照SRP的原則去進行設(shè)計,但畢竟理論是理論,實踐是實踐。在實際開發(fā)過程中,有很多因素導致最終無法達到按照SRP原則的最終效果,比如開會討論業(yè)務(wù)模型中職責的劃分;又比如deadline比較緊急,沒有足夠的時間進行討論和設(shè)計。一個行業(yè)的驅(qū)動最終還是業(yè)務(wù),代碼只是實現(xiàn)業(yè)務(wù)的工具,是輪子。一個功能,最低要求就是先能跑起來,完成功能。只是在一開始實現(xiàn)的時候,盡可能的去往 SRP 上靠,讀者的建議是:
對于單一職責原則, 我的建議是接口一定要做到單一職責, 類的設(shè)計盡量做到只有一個原因引起變化。
1.2 里氏替換原則
里氏替換原則原則是在繼承方面上的一個要求。它是針對繼承的弊端而出現(xiàn)的一個原則。
繼承的優(yōu)點:
- 共享代碼,提供代碼的重用性
- 提高代碼的擴展性
- 提高產(chǎn)品或者項目的開放性
繼承的缺點:
- 繼承是侵入性的,只要繼承,就必須擁有父類的所有屬性和方法
- 增強了耦合性。當父類的內(nèi)容修改時,需要考慮子類的修改,可能會造成大段代碼需要重構(gòu)
定義
Liskov Substitution Principle, LSP
所有引用父類的地方必須能透明的使用其子類的對象。
通俗的講,只要父類出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會產(chǎn)生任何錯誤或者異常,使用者可能根本不需要知道是父類還是子類。但是反過來就不行,有子類的地方,無法用父類進行替換。
LSP 對良好的繼承定義了一個規(guī)范,這個規(guī)范包含四層含義:
1. 子類必須完全實現(xiàn)父類的方法
書中的例子是“士兵和槍”的例子。當定義“ToyGun”時,由于 ToyGun 無法殺人,程序無法正常運行。原因是 ToyGun 無法完整的實現(xiàn) shoot 功能。
在具體應(yīng)用場景中就要考慮下面這個問題了: 子類是否能夠完整地實現(xiàn)父類的業(yè)務(wù), 否則就會出現(xiàn)像上面的拿槍殺敵人時卻發(fā)現(xiàn)是把玩具槍的笑話
如果子類不能完整地實現(xiàn)父類的方法, 或者父類的某些方法在子類中已經(jīng)發(fā)
生“畸變”, 則建議斷開父子繼承關(guān)系, 采用依賴、 聚集、 組合等關(guān)系代替承。
注意:在類中調(diào)用其他類時務(wù)必要使用父類或接口, 如果不能使用父類或接口, 則說明類的設(shè)計已經(jīng)違背了LSP原則。
2. 子類可以有自己的個性
書中給出的例子是“狙擊手使用狙擊槍殺人”,表達的意思是:如果實例類型為子類,則子類無法強轉(zhuǎn)成父類類型進行調(diào)用。
3. 覆蓋或?qū)崿F(xiàn)父類的方法是輸入?yún)?shù)可以被放大
書中舉出了一個例子,這個例子會導致“子類在沒有覆寫父類的方法的前提下,子類方法被執(zhí)行了”。
這個例子中子類對于方法的定義就有問題:
public class Father {
public Collection doSomething(Map map) {
System.out.println("父類被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
//縮小輸入?yún)?shù)范圍
public Collection doSomething(HashMap map) {
System.out.println("子類被執(zhí)行...");
return map.values();
}
}
public class Client {
public static void invoker() {
//有父類的地方就有子類
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
子類的doSomething方法,并不是覆蓋,而是對從父類繼承過來的doSomething方法的重載。在 Client 執(zhí)行時,如果使用子類去替換,實際執(zhí)行的將會是子類的 doSomething 方法。從而導致了“子類在沒有覆寫父類的方法的前提下,子類方法被執(zhí)行了”。
注意,這里是有一個前提:“子類在沒有覆寫父類的方法的前提下”
如果是子類在復寫了父類的方法下,使用子類去替換父類,調(diào)用的實際是子類的方法,這樣是ok的。但是上面卻是沒有復寫到父類的方法。沒有復寫,而且輸入?yún)?shù)的范圍比父類的方法大,就會出現(xiàn)問題。
正確的例子是:
public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父類被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
//縮小輸入?yún)?shù)范圍
public Collection doSomething(Map map) {
System.out.println("子類被執(zhí)行...");
return map.values();
}
}
public class Client {
public static void invoker() {
//有父類的地方就有子類
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
這樣執(zhí)行,子類也沒有復寫到父類的方法,但是在Client中用子類去替換父類,實際執(zhí)行的還是父類方法。
最根本的原因,就是 子類在定義同名方法時,輸入?yún)?shù)的范圍比父類更大。
4. 覆蓋或?qū)崿F(xiàn)父類的方法是輸出結(jié)果可以被縮小
書中分了兩種情況:
- 子類覆蓋:返回值范圍要小于等于父類的方法
- 方法重載:這個無所謂,因為不會調(diào)用到該方法
這個也比較好理解,目的是:
讓上層在調(diào)用目標方法后,在使用方法的返回值時不會出現(xiàn)不存在方法的現(xiàn)象。如果返回值是父類,而實際返回值類型是子類,這樣沒什么問題;如果反過來,就可能會出現(xiàn)問題,上層在調(diào)用返回值中的方法,有可能是子類獨有的方法,而返回值類型是父類,會出現(xiàn)調(diào)用失敗的現(xiàn)象。
總結(jié):
遵守了這四個規(guī)范,也就相當于遵守了 LSP 原則
1.3 依賴倒置原則
依賴倒置原則的表現(xiàn):
- 模塊間的依賴通過抽象發(fā)生,實現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的;
- 接口或抽象類不依賴于實現(xiàn)類;
- 實現(xiàn)類依賴接口或抽象類。
書中用“司機開車”的例子來說明。

public class Benz {
//汽車肯定會跑
public void run() {
System.out.println("奔馳汽車開始運行...");
}
}
public class Client {
public static void main (String[] args) {
Driver zhangSan = new Driver();
Benz benz = new Benz();
//張三開奔馳車
zhangSan.drive(benz);
}
}
Driver 和 Benz 類都是實現(xiàn)類,Driver 強依賴 Benz 類。
如果將來需要司機去開 BMW,程序?qū)o法完成。此處進行結(jié)構(gòu)優(yōu)化,對實現(xiàn)類進行抽象,解除強依賴關(guān)系。

public interface IDriver {
//是司機就應(yīng)該會駕駛汽車
public void drive(ICar car);
}
public class Driver implements IDriver{
//司機的主要職責就是駕駛汽車
public void drive(ICar car){
car.run();
}
}
public interface ICar {
//是汽車就應(yīng)該能跑
public void run();
}
public class Benz implements ICar{
//汽車肯定會跑
public void run(){
System.out.println("奔馳汽車開始運行...");
}
}
public class BMW implements ICar{
//寶馬車當然也可以開動了
public void run(){
System.out.println("寶馬汽車開始運行...");
}
}
public class Client {
public static void main (String[] args) {
IDriver zhangSan = new Driver();
ICar benz = new Benz();
//張三開奔馳車
zhangSan.drive(benz);
//IDriver zhangSan = new Driver();
//ICar bmw = new BMW();
//張三開奔馳車
//zhangSan.drive(bmw);
}
}
總結(jié)一下依賴倒置的好處:
- 在新增加低層模塊時, 只修改了業(yè)務(wù)場景類, 也就是高層模塊, 對其他低層模塊如Driver類不需要做任何修改, 業(yè)務(wù)就可以運行, 把“變更”引起的風險擴散降到最低
- 兩個類之間有依賴關(guān)系, 只要制定出兩者之間的接口( 或抽象類) 就可以獨立開發(fā)了, 而且項目之間的單元測試也可以獨立地運行
最佳實踐
我們怎么在項目中使用這個規(guī)則呢:
- 每個類盡量都有接口或抽象類, 或者抽象類和接口兩者都具備
- 變量的表面類型盡量是接口或者是抽象類
- 任何類都不應(yīng)該從具體類派生
- 盡量不要覆寫基類的方法
- 結(jié)合里氏替換原則使用
1.4 接口隔離原則
定義
接口:
- 類接口
- 實例接口
隔離:
- 客戶端不應(yīng)該依賴它不需要的接口
- 類之間的依賴關(guān)系應(yīng)該建立在最小的接口之上
接口隔離原則概括為一句話:
建立單一接口,接口盡量細化,同時接口中方法盡量少。
例子
書中給出的例子是“美女類”的例子

接口定義了一個美女:
public interface IPettyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
//要有氣質(zhì)
public void greatTemperament();
}
接口存在的問題是:審美會隨著時間的改變而改變。
接口IPettyGirl的設(shè)計是有缺陷的, 過于龐大了, 容納了一些可變的因素。而我們卻把這些特質(zhì)都封裝了起來, 放到了一個接口中, 封裝過度了。
把原IPettyGirl接口拆分為兩個接口, 一種是外形美的美女IGoodBodyGirl, 這類美女的特點就是臉蛋和身材極棒, 超一流, 但是沒有審美素質(zhì), 比如隨地吐痰, 文化程度比較低; 另外一種是氣質(zhì)美的美女IGreatTemperamentGirl, 談吐和修養(yǎng)都非常高。把一個比較臃腫的接口拆分成了兩個專門的接口, 靈活性提高了, 可維護性也增加了, 不管以后是要外形美的美女還是氣質(zhì)美的美女都可以輕松地通過PettyGirl定義。

/** 外表型美女 **/
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}
/** 氣質(zhì)型美女 **/
public interface IGreatTemperamentGirl {
//要有氣質(zhì)
public void greatTemperament();
}
/** 最標準的美女,擁有所有優(yōu)點 **/
public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
private String name;
//美女都有名字
public PettyGirl(String _name){
this.name=_name;
}
//臉蛋漂亮
public void goodLooking() {
System.out.println(this.name + "---臉蛋很漂亮!");
}
//氣質(zhì)要好
public void greatTemperament () {
System.out.println(this.name + "---氣質(zhì)非常好!");
}
//身材要好
public void niceFigure () {
System.out.println(this.name + "---身材非常棒!");
}
}
讓客戶端去依賴兩個專用的接口,比去依賴一個綜合的接口要更加靈活。
接口隔離原則的目的
接口隔離原則是對接口進行規(guī)范的約束
接口要盡量的小
這一點上面的例子已經(jīng)體現(xiàn)了,如果一個接口已經(jīng)存在了臃腫現(xiàn)象,它會影響一個正常的代碼結(jié)構(gòu),一些不需要實現(xiàn)的方法強制去實現(xiàn)。要對接口進行細化。
接口要高內(nèi)聚
高內(nèi)聚 就是提高接口、 類、 模塊的處理能力, 減少對外的交互。
具體到接口隔離原則就是, 要求在接口中盡量少公布public方法, 接口是對外的承諾, 承諾越少對系統(tǒng)的開發(fā)越有利, 變更的風險也就越少, 同時也有利于降低成本
定制服務(wù)
只提供訪問者需要的方法
接口設(shè)計是有限度的:
- 對接口的拆分也需要有度,根據(jù)接口隔離原則拆分接口時,首先必須滿足單一職責原則
- 接口的設(shè)計粒度越小, 系統(tǒng)越靈活, 這是不爭的事實。 但是, 靈活的同時也帶來了結(jié)構(gòu)的復雜化, 開發(fā)難度增加, 可維護性降低。
如何衡量原子接口或原子類的劃分:
- 一個接口只服務(wù)于一個子模塊或業(yè)務(wù)邏輯
- 通過業(yè)務(wù)邏輯壓縮接口中的public方法, 接口時常去回顧, 盡量讓接口達到“滿身筋骨肉”, 而不是“肥嘟嘟”的一大堆方法
- 已經(jīng)被污染了的接口, 盡量去修改, 若變更的風險較大, 則采用適配器模式進行轉(zhuǎn)化處理
- 了解環(huán)境, 拒絕盲從。 每個項目或產(chǎn)品都有特定的環(huán)境因素,深入了解業(yè)務(wù)邏輯
與單一職責原則的區(qū)別
接口隔離原則與單一職責的審視角度是不相同的, 單一職責要求的是類和接口職責單一, 注重的是職責, 這是業(yè)務(wù)邏輯上的劃分, 而接口隔離原則要求接口的方法盡量少。
1.5 迪米特法則
定義
原則
對外公開的范圍
一個類公開的public屬性或方法越多, 修改時涉及的面也就越大, 變更引起的風險擴散也就越大,因此, 在設(shè)計時需要反復衡量:
- 是否還可以再減少 public方法和屬性
- 是否可以修改為private、 package-private(包類型, 在類、 方法、 變量前不加訪問權(quán)限, 則默認為包類型) 、 protected等訪問權(quán)限
- 是否可以加上final關(guān)鍵字等
成員的歸屬
如果一個方法或者屬性放在本類中, 既不增加類間關(guān)系, 也對本類不產(chǎn)生負面影響, 那就放置在本類中
最佳實踐
迪米特法則的核心觀念就是類間解耦, 弱耦合,既做到讓結(jié)構(gòu)清晰, 又做到高內(nèi)聚低耦合。
1.6 開閉原則
定義
對擴展開放, 對修改關(guān)閉, 其含義是說一個軟件實體應(yīng)該通過擴展來實現(xiàn)變化, 而不是通過修改已有的代碼來實現(xiàn)變化
例子
書中用 “書店買書” 的例子來進行說明

/* 書籍接口 */
public interface IBook {
//書籍有名稱
public String getName();
//書籍有售價
public int getPrice();
//書籍有作者
public String getAuthor();
}
/* 小說類 */
public class NovelBook implements IBook {
//書籍名稱
private String name;
//書籍的價格
private int price;
//書籍的作者
private String author;
//通過構(gòu)造函數(shù)傳遞書籍數(shù)據(jù)
public NovelBook(String _name,int _price,String _author){
this.name = _name;
this.price = _price;
this.author = _author;
}
//作者是誰
public String getAuthor() {
return this.author;
}
//書籍叫什么名字
public String getName() {
return this.name;
}
//獲得書籍的價格
public int getPrice() {
return this.price;
}
}
/* 模擬業(yè)務(wù)流程類 */
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//static靜態(tài)模塊初始化數(shù)據(jù), 實際項目中一般是由持久層完成
static{
bookList.add(new NovelBook("天龍八部",3200,"金庸"));
bookList.add(new NovelBook("巴黎圣母院",5600,"雨果"));
bookList.add(new NovelBook("悲慘世界",3500,"雨果"));
bookList.add(new NovelBook("金瓶梅",4300,"蘭陵笑笑生"));
}
//模擬書店買書
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println("-----------書店賣出去的書籍記錄如下: -----------");
for(IBook book:bookList){
System.out.println("書籍名稱: " + book.getName()+"\t書籍作者: "
book.getAuthor()+"\t書籍價格: "+ formatter.format (book.getPrice()/100.0)+"元");
}
}
}
此時需求增加,需要對打折的書籍的價格進行特殊調(diào)整。
- 打折行為只會出現(xiàn)在打折書籍中,并不存在于所有書籍。所以不能改動 IBook 接口;
- 例如采購書籍人員也是要看價格的, 由于該方法已經(jīng)實現(xiàn)了打折處理價格, 因此采購人員看到的也是打折后的價格, 會因信息不對稱而出現(xiàn)決策失誤的情況。 因此, 該方案也不是一個最優(yōu)的方案。(說來慚愧,書上的這一段我沒咋明白作者想表達的意思...)
此時需要構(gòu)造一個新的類作為 NovelBook 的子類

public class OffNovelBook extends NovelBook {
public OffNovelBook(String _name,int _price,String _author){
super(_name,_price,_author);
}
//覆寫銷售價格
@Override
public int getPrice () {
//原價
int selfPrice = super.getPrice();
int offPrice = 0;
if (selfPrice > 4000) { //原價大于40元, 則打9折
offPrice = selfPrice * 90 /100;
} else {
offPrice = selfPrice * 80 /100;
}
return offPrice;
}
}
/* 業(yè)務(wù)流程類 */
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//static靜態(tài)模塊初始化數(shù)據(jù), 實際項目中一般是由持久層完成
static{
bookList.add(new OffNovelBook("天龍八部",3200,"金庸"));
bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果"));
bookList.add(new OffNovelBook("悲慘世界",3500,"雨果"));
bookList.add(new OffNovelBook("金瓶梅",4300,"蘭陵笑笑生"));
}
//模擬書店買書
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println("-----------書店賣出去的書籍記錄如下: -----------");
for(IBook book:bookList){
System.out.println("書籍名稱: " + book.getName()+"\t書籍作者: "
}
}
}
在定義了新的子類之后,輸入的圖書列表對象可能存在正常的 NovelBook,也會有 OffNovelBook,無論存在什么,業(yè)務(wù)主流程還是無需改動的。關(guān)鍵點在于
在 BookStore 類中,也可以將 bookList 看做是一種外界輸入,參數(shù)的類型為接口類型,main 方法中也是使用的是接口類型對象進行操作。
開閉原則的意義
- 主業(yè)務(wù)流程不會改動的太頻繁
- 單測用例不需要頻繁改動
- 提高復用性
- 提高可維護性