java設計模式之6大設計原則day01
一、 單一職責原則
1.1
單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP
1.2
There should never be more than one reason for a class to change
1.3 單一原則好處
- 類的複雜性降低,實現什麼職責都有清晰明確的定義
- 可讀性提高,複雜性降低,那當然可讀性提高了
- 可維護性提高,可讀性提高,那當然更容易維護
- 變更引起的風險降低,變更是必不可少的,如果介面的單一職責做得好,一個介面修
改只對相應的實現類有影響,對其他的介面無影響,這對系統的擴充套件性、維護性都有非常大的幫助。
1.4 注意
單一職責原則提出了一個編寫程式的標準,用“職責”或“變化原因”來衡量介面或
類設計得是否優良,但是“職責”和“變化原因”都是不可度量的,因專案而異,因環境而異
1.5 This is sometimes hard to see
二 、里氏替換原則
2.1 繼承
2.1.1 繼承帶來的好處
- 程式碼共享,減少建立類的工作量,每個子類都擁有父類的方法和屬性;
- 提高程式碼的重用性;
- 子類可以形似父類,但又異於父類
- 提高程式碼的可擴充套件性
- 提高產品或專案的開放性
2.1.2 繼承的缺點
- 繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法
- 降低程式碼的靈活性
- 增強了耦合性
2.1.3
Java使用extends關鍵字來實現繼承,它採用了單一繼承的規則
2.2 什麼是里氏替換原則
- :If for each object o1 of type S there is an object o2 of
type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果對每一個型別為S的物件o1,都有型別為T的物件o2,使得以T定義的所有程式P在所有的物件o1都代換成o2時,程式P的行為沒有發生變化,那麼型別S是型別T的子型別。) - :Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基類的地方必須能透明地使用其子類的物件。)
- 通俗點講,只要父類能出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者可能根本就不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。
2.3 里氏替換原則含義
2.3.1
子類必須完全實現父類的方法
如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係代替繼承子類可以有自己的個性
向下轉型(downcast)是不安全的,從里氏替換原則來看,就是有子類出現的地方父類未必就可以出現,會在執行期丟擲java.lang.ClassCastException
異常覆蓋或實現父類的方法時輸入引數可以被放大
Design by Contract(契約設計):先定義介面:方法名、引數、返回值,指定雙方開發協議,然後再各自實現。 子類的引數的範圍應該大於父類的引數範圍。比如下面程式碼所示:
/**
*Father類原始碼
*/
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父類被執行...");
return map.values();
}
}
/**
*子類原始碼
*/
public class Son extends Father {
//放大輸入引數型別
public Collection doSomething(Map map){
System.out.println("子類被執行...");
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();
}
}
/**
*這裡使用子類 結果是 父類被執行...
*/
public class Client {
public static void invoker(){
//父類存在的地方,子類就應該能夠存在
Son f =new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
這裡子類和父類方法名相同,但是不是覆寫(Override)父類的方法 。如果這裡加入@Override 會報錯,因為方法名雖然相同,但是引數不同,就不是覆寫,這裡是過載(Overload ) , 這個例子 父類方法的輸入引數是HashMap型別,子類的輸入引數是Map型別,也就是說子類的輸入引數型別的範圍擴大了,子類代替父類傳遞到呼叫者中,子類的方法永遠都不會被執行。
如果相反,父類的範圍比子類大的話 。那麼我們就會發生業務邏輯混亂。例子如下:
/**
*父類的前置條件較大
*/
public class Father {
public Collection doSomething(Map map){
System.out.println("父類被執行...");
return map.values();
}
}
/**
*子類的前置條件較小
*/
public class Son extends Father {
//縮小輸入引數範圍
public Collection doSomething(HashMap map){
System.out.println("子類被執行...");
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();
}
}
/**
*採用子類代替父類執行後 結果為 子類被執行...
*/
public class Client {
public static void invoker(){
//有父類的地方就有子類
Son f =new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
子類在沒有覆寫父類的方法的前提下,子類方法被執行了。
2.3.2 總結
子類中方法的前置條件必須與超類中被覆寫的方法的前置條件相同或者更寬鬆
2.4 覆寫或實現父類的方法時輸出結果可以被縮小
2.4.1 理解
父類的一個方法的返回值是一個型別T,子類的相同方法(過載或覆寫)的返回值為S,那麼里氏替換原則就要求S必須小於等於T,也就是說,要麼S和T是同一個型別,要麼S是T的子類
2.4.2 為什麼要這樣?
- 如果是覆寫,父類和子類的同名方法的輸入引數是完全相同的,兩個方法的範圍值S小於等於T , 這是覆寫的要求。
- 如果是過載,則要求輸入引數型別或者數量不同,在里氏替換原則要求下,就是子類的輸入引數寬於或等於父類的輸入引數,也就是說我們寫的這個方法是不會被呼叫的。這裡需要理解2.3 。
2.5 總結
在專案中,採用里氏替換原則時,儘量避免子類的“個性”,一旦子類有“個性”,這個子類和父類之間的關係就很難調和了,把子類當做父類使用,子類的“個性”被抹殺——委屈了點;把子類單獨作為一個業務來使用,則會讓程式碼間的耦合關係變得撲朔迷離——缺乏類替換的標準。