一、面向對象的7大設計原則
- 開閉原則
- 對擴展開放,對更改關閉
- 類模塊應該是可擴展的,但是不可修改。
- 里氏代換原則
- 子類必須能夠替換它們的基類(IS-A)
- 繼承表達類型抽象
- 迪米特原則
- 要求一個對象應該對其他對象有最少的瞭解,所以迪米特法則又叫做最少知識原則(Least Knowledge Principle, LKP)
- 單一職責原則
- 一個類應該僅有一個引起它變化的原因
- 變化的方向隱含着類的責任
- 接口分離原則
- 不應該強迫客戶程序依賴它們不用的方法
- 接口應該小而完備
- 依賴倒置原則
- 高層模塊(穩定)不應該依賴於低層模塊(變化),二者都應該依賴於抽象(穩定)
- 抽象(穩定)不應該依賴於實現細節(變化),實現細節應該依賴於抽象(穩定)
- 組合/聚合複用原則
- 儘量使用組合/聚合,不要使用類繼承
- 在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分,新對象通過向這些對象的委派達到複用已有功能的目的。就是說要儘量的使用合成和聚合,而不是繼承關係達到複用的目的
二、一句話理解七大原則
- 開閉原則
/**
* 定義課程接口
*/
public interface ICourse {
String getName(); // 獲取課程名稱
Double getPrice(); // 獲取課程價格
Integer getType(); // 獲取課程類型
}
/**
* 英語課程接口實現
*/
public class EnglishCourse implements ICourse {
private String name;
private Double price;
private Integer type;
public EnglishCourse(String name, Double price, Integer type) {
this.name = name;
this.price = price;
this.type = type;
}
@Override
public String getName() {
return null;
}
@Override
public Double getPrice() {
return null;
}
@Override
public Integer getType() {
return null;
}
}
// 測試
public class Main {
public static void main(String[] args) {
ICourse course = new EnglishCourse("小學英語", 199D, "Mr.Zhang");
System.out.println(
"課程名字:"+course.getName() + " " +
"課程價格:"+course.getPrice() + " " +
"課程作者:"+course.getAuthor()
);
}
}
如果課程要打折,有一種方法是往接口中添加方法;
public interface ICourse {
// 獲取課程名稱
String getName();
// 獲取課程價格
Double getPrice();
// 獲取課程類型
String getAuthor();
// 新增:打折接口
Double getSalePrice();
}
這就違反了開閉原則,會導致所有的其他課程都需要實現打折接口,更好的方法是用擴展代替修改;
public class SaleEnglishCourse extends EnglishCourse {
public SaleEnglishCourse(String name, Double price, String author) {
super(name, price, author);
}
@Override
public Double getPrice() {
return super.getPrice() * 0.85;
}
}
- 里氏代換原則
里氏替換原則強調的是設計和實現要依賴於抽象而非具體;子類只能去擴展基類,而不是隱藏或者覆蓋基類,它包含以下4層含義- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法
- 子類可以有自己的個性
- 覆蓋或實現父類的方法時輸入參數可以被放大
- 覆寫或實現父類的方法時輸出結果可以被縮小
- 迪米特原則
通過老師要求班長告知班級人數爲例,講解迪米特原則。先來看一下違反迪米特法則的設計,代碼如下
public class Student {
private Integer id;
private String name;
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
}
public class Teacher {
public void call(Monitor monitor) {
List<Student> sts = new ArrayList<>();
for (int i = 0; i < 10; i++) {
sts.add(new Student(i + 1, "name" + i));
}
monitor.getSize(sts);
}
}
public class Monitor {
public void getSize(List list) {
System.out.println("班級人數:" + list.size());
}
}
從邏輯上講 Teacher 只與 Monitor 耦合就行了,與 Student 並沒有任何聯繫,這樣設計顯然是增加了不必要的耦合。按照迪米特原則,應該避免類中出現這樣非直接朋友關係的耦合。
public class Student {
private Integer id;
private String name;
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
}
public class Teacher {
public void call(Monitor monitor) {
monitor.getSize();
}
}
public class Monitor {
public void getSize() {
List<Student> sts = new ArrayList<>();
for (int i = 0; i < 10; i++) {
sts.add(new Student(i + 1, "name" + i));
}
System.out.println("班級人數" + sts.size());
}
}
- 單一職責原則
用一個類描述動物呼吸這個場景:
class Animal {
func breathe(animal: String) {
print("\(animal)呼吸空氣")
}
}
let animal = Animal()
animal.breathe(animal: "牛")
animal.breathe(animal: "羊")
animal.breathe(animal: "豬")
程序上線後,發現問題了,並不是所有的動物都呼吸空氣的,比如魚就是呼吸水的。修改時如果遵循單一職責原則,需要將 Animal 類細分爲陸生動物類 Terrestrial,水生動物 Aquatic,代碼如下:
class Terrestrial {
func breathe(animal: String) {
print("\(animal)呼吸空氣")
}
}
class Aquatic {
func breathe(animal: String) {
print("\(animal)呼吸水")
}
}
let terrestrial = Terrestrial()
terrestrial.breathe(animal: "牛")
terrestrial.breathe(animal: "羊")
terrestrial.breathe(animal: "豬")
let aquatic = Aquatic()
aquatic.breathe(animal: "魚")
違反單一職責的修改:
class Animal {
func breathe(animal: String) {
if (animal == "魚") {
print("\(animal)呼吸水")
} else {
print("\(animal)呼吸空氣")
}
}
}
let animal = Animal()
animal.breathe(animal: "牛")
animal.breathe(animal: "羊")
animal.breathe(animal: "豬")
animal.breathe(animal: "魚")
// 這種修改方式要簡單的多。但是卻存在着隱患:有一天需要將魚分爲呼吸淡水的魚和呼吸海水的魚,則又需要修改Animal類的breathe方法,而對原有代碼的修改會對調用 “豬” “牛” “羊” 等相關功能帶來風險,也許某一天你會發現程序運行的結果變爲“牛呼吸水”了。
- 接口分離原則
- 接口中的方法應該儘量少,不要使接口過於臃腫,不要有很多不相關的邏輯方法。
- 依賴倒置原則
場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照着書給孩子講故事了。代碼如下:
class Book {
func getContent() -> String{
return "很久很久以前有一個阿拉伯的故事……"
}
}
class Mother {
func narrate(book: Book) {
print("媽媽開始講故事");
print("\(book.getContent())")
}
}
let mother = Mother()
mother.narrate(book: Book())
運行良好,假如有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的代碼如下:
class NewsPaper {
func getContent() -> String {
return "70 週年慶..."
}
}
只是將書換成報紙,居然必須要修改Mother才能讀。假如以後需求換成雜誌呢?換成網頁呢?還要不斷地修改Mother,這顯然不是好的設計。原因就是Mother與Book之間的耦合性太高了,必須降低他們之間的耦合度纔行。我們引入一個抽象的接口IReader。讀物,只要是帶字的都屬於讀物:
protocol IReader {
func getContent() -> String
}
class Book: IReader {
func getContent() -> String{
return "很久很久以前有一個阿拉伯的故事……"
}
}
class NewsPaper: IReader{
func getContent() -> String {
return "70 週年慶..."
}
}
class Mother{
func narrate(reader: IReader) {
print("媽媽開始講故事");
print("\(reader.getContent())")
}
}
let mother = Mother()
mother.narrate(reader: Book())
mother.narrate(reader: NewsPaper())
這樣修改後,無論以後怎樣擴展,都不需要再修改 Mother 類了。
- 組合/聚合複用原則
- 類之間有三種基本關係,分別是:關聯(聚合和組合)、泛化(與繼承同一概念)、依賴。
- 聚合,用來表示整體與部分的關係或者 “擁有” 關係。其中,代表部分的對象可能會被代表多個整體的對象所擁有,但是並不一定會隨着整體對象的銷燬而銷燬,部分的生命週期可能會超越整體。好比班級和學生,班級銷燬或解散後學生還是存在的,學生可以繼續存在某個培訓機構或步入社會,生命週期不同於班級甚至大於班級。
- 合成,用來表示一種強得多的 “擁有” 關係。其中,部分和整體的生命週期是一致的,一個合成的新的對象完全擁有對其組成部分的支配權,包括創建和泯滅。好比人的各個器官組成人一樣,一旦某個器官衰竭,人也不復存在,這是一種 “強” 關聯。
- 類之間有三種基本關係,分別是:關聯(聚合和組合)、泛化(與繼承同一概念)、依賴。