什麼是設計模式?
設計模式(Design Pattern) ,這個術語最初並不是出現在軟件設計中,而是被用於建築領域的設計中,後來隨着軟件工程的發展,設計模式思想被引入到軟件工程中。直到今天,狹義的設計模式還是人們廣爲流傳的 23 種經典設計模式。
設計模式(Design pattern)代表了計算機軟件工程最佳的實踐,是一套被反覆使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是爲了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。
在當今大型工程軟件體系的發展背景下,不管是設計模式也好,其他工程模式也罷,都是爲了解決問題而發明的有效方法。除了常見的的23種設計模式以外,還有 MVVM、MVC、Combinator 等,都是前輩們經過多年的摸爬滾打總結出來的,其有效性不容置疑。對於傳統簡單的程序開發,寫一個簡單的算法可能比引入某種設計模式更容易實現,但對於大型項目開發或框架設計,用設計模式來組織代碼纔是主要方向。
數據結構、算法、設計模式,只有牢固掌握這三項基本技能,纔有資格稱爲軟件工程師。
合理使用設計模式
曾有人這樣說:
設計模式是爲了封裝變化,讓各個模塊可以獨立變化。精準地使用設計模式的前提是你能夠精準的預測需求變更的走向。
我們都知道大部分人是做不到的,所以大部分人就算精通設計模式也多少會做錯點什麼東西。所以這其實不怪設計模式。所以說如何避免過度設計,這就要求你深入的理解你的程序所在的領域的知識,瞭解用戶使用你的軟件是爲了解決什麼問題,這樣你預測用戶的需求才會比以前更加準確,從而避免了你使用設計模式來封裝一些根本不會發生的變化,也避免了你忽視了未來會發生的變化從而發現你使用的模式根本不能適應需求的新走向。
所以,在你滿足了【知道所有設計模式爲什麼要被髮明出來】的前提之後,剩下的其實都跟編程沒關係,而跟你的領域知識和領域經驗有關係。
我認爲說的很對,合理使用設計模式應該基於對工程的完全理解。
設計模式的七大原則
俗話說得好,沒有規矩不成方圓,設計模式也要遵循一些準則。一般地,我們在設計軟件工程的時候需要遵循七項基本原則,分別是:
- 單一職責原則(Single Responsibility)
- 接口隔離原則(Interface Segregation)
- 依賴倒置原則(Dependence Inversion)
- 里氏替換原則(Liskov Substitution)
- 開閉原則(Open Close)
- 迪米特法則(Demeter)
- 合成複用原則(Composite Reuse)
設計模式包含了面向對象的精髓,懂了設計模式就懂了面向對象分析(OOA)和設計(OOD)的精要。
1. 單一職責原則
對於類來說,一個類應該只負責一項職責。
- 降低類的複雜度。
- 提高類的可讀性,可維護性。
- 降低變更引起的風險。
- 通常情況下,我們應當遵守單一職責原則,只有邏輯足夠簡單,纔可以在代碼級別違反單一職責原則;只有類中的方法足夠少,可以在方法級別違反單一職責原則。
2. 接口隔離原則
一個類不應該依賴它不需要的接口,即一個類對另一個類的依賴應該建立在最小的接口上。
3. 依賴倒置原則 (面向接口編程)
- 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象。
- 抽象不應該依賴細節,細節應該依賴抽象。
- 依賴倒轉(倒置)的中心思想是面向接口編程。
- 依賴倒轉原則是基於這樣的設計理念:相對於細節的多變性,抽象的東西要穩定的多。以抽象爲基礎搭建的架構比以細節爲基礎的架構要穩定的多。在 Java 中,抽象指的是接口或抽象類,細節就是具體的實現類。
- 使用接口或抽象類的目的是制定好規範,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
4. 里氏替換原則 (正確使用繼承)
里氏替換原則,主張使用“抽象(Abstraction)”和“多態(Polymorphism)”將設計中的靜態結構改爲動態結構,維持設計的封閉性。“抽象”是語言提供的功能。“多態”由繼承語義實現。
里氏替換原則的內容可以描述爲: “派生類(子類)對象可以在程式中代替其基類(超類)對象。”
- 如果對每個類型爲 T1 的對象 object1 ,都有類型爲 T2 的對象 object2 ,使得以 T1 定義的所有程序 P 在所有的對象 object1 都代換成 object2 時,程序 P 的行爲沒有發生變化,那麼類型 T2 是類型 T1 的子類型。換句話說,所有引用基類的地方必須能透明地使用其子類的對象。
- 在使用繼承時,遵循里氏替換原則,在子類中儘量不要重寫父類的方法。
- 里氏替換原則告訴我們,繼承實際上讓兩個類耦合性增強了,在適當的情況下,可以通過 聚合、組合、依賴 來解決問題。
5. 開閉原則 (編程中最基礎、最重要的設計原則)
- 一個軟件實體如類,模塊和函數應該對擴展開放,對修改關閉。用抽象構建框架,用實現擴展細節。
- 當軟件需要變化時,儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來實現變化。
- 編程中遵循其它原則,以及使用設計模式的目的就是遵循開閉原則。
違反開閉原則的示例代碼
GraphicEditor 類違反了開閉原則。
// 主方法
public class Ocp {
public static void main(String[] args) {
//使用看看存在的問題
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
//這是一個用於繪圖的類 [使用方]
class GraphicEditor {
//接收Shape對象,然後根據type,來繪製不同的圖形
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
else if (s.m_type == 3)
drawTriangle(s);
}
//繪製矩形
public void drawRectangle(Shape r) {
System.out.println(" 繪製矩形 ");
}
//繪製圓形
public void drawCircle(Shape r) {
System.out.println(" 繪製圓形 ");
}
//繪製三角形
public void drawTriangle(Shape r) {
System.out.println(" 繪製三角形 ");
}
}
//Shape類,基類
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
//新增畫三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
}
改進示例代碼
// 主方法
public class Ocp {
public static void main(String[] args) {
//使用看看存在的問題
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
graphicEditor.drawShape(new OtherGraphic());
}
}
//這是一個用於繪圖的類 [使用方]
class GraphicEditor {
//接收Shape對象,調用draw方法
public void drawShape(Shape s) {
s.draw();
}
}
//Shape類,基類
abstract class Shape {
int m_type;
public abstract void draw(); //抽象方法
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 繪製矩形 ");
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 繪製圓形 ");
}
}
//新增畫三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 繪製三角形 ");
}
}
//新增一個圖形
class OtherGraphic extends Shape {
OtherGraphic() {
super.m_type = 4;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 繪製其它圖形 ");
}
}
6. 迪米特法則 (降低類之間的耦合)
- 一個對象應該對其他對象保持最少的瞭解。
- 類與類關係越密切,耦合度越大。
- 迪米特法則(Demeter Principle)又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都儘量將邏輯封裝在類的內部。對外除了提供的 public 方法,不對外泄露任何信息。
- 迪米特法則還有個更簡單的定義:只與直接的朋友通信。
什麼是直接的朋友?
每個對象都會與其他對象有耦合關係,只要兩個對象之間有耦合關係, 我們就說這兩個對象之間是朋友關係。耦合的方式很多,依賴,關聯,組合,聚合 等。其中,我們稱出現成員變量,方法參數,方法返回值中的類爲直接的朋友,而出現在局部變量中的類不是直接的朋友。也就是說,陌生的類最好不要以局部變量的形式出現在類的內部。
違反迪米特法則的示例代碼
//主方法
public class Demeter1 {
public static void main(String[] args) {
//創建了一個 SchoolManager 對象
SchoolManager schoolManager = new SchoolManager();
//輸出學院的員工id 和 學校總部的員工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//學校總部員工類
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//學院的員工類
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理學院員工的管理類
class CollegeManager {
//返回學院的所有員工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<>();
//這裏我們增加了10個員工到 list
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("學院員工id= " + i);
list.add(emp);
}
return list;
}
}
//學校管理類
//分析 SchoolManager 類的直接朋友類有 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一個陌生類,這樣違背了 迪米特法則
class SchoolManager {
//返回學校總部的員工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<>();
//這裏我們增加了5個員工到 list
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("學校總部員工id= " + i);
list.add(emp);
}
return list;
}
//該方法完成輸出學校總部和學院員工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析問題
//1. 這裏的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部變量方式出現在 SchoolManager
//3. 違反了 迪米特法則
//獲取到學院員工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------學院員工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
//獲取到學校總部員工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------學校總部員工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
改進示例代碼
// 主方法
public class Demeter1 {
public static void main(String[] args) {
System.out.println("~~~使用迪米特法則的改進~~~");
//創建了一個 SchoolManager 對象
SchoolManager schoolManager = new SchoolManager();
//輸出學院的員工id 和 學校總部的員工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//學校總部員工類
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//學院的員工類
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理學院員工的管理類
class CollegeManager {
//返回學院的所有員工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<>();
//這裏我們增加了10個員工到 list
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("學院員工id= " + i);
list.add(emp);
}
return list;
}
//輸出學院員工的信息
public void printEmployee() {
//獲取到學院員工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------學院員工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
//學校管理類
//分析 SchoolManager 類的直接朋友類有 Employee、CollegeManager
class SchoolManager {
//返回學校總部的員工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<>();
//這裏我們增加了5個員工到 list
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("學校總部員工id= " + i);
list.add(emp);
}
return list;
}
//該方法完成輸出學校總部和學院員工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析問題
//1. 將輸出學院的員工方法,封裝到CollegeManager
sub.printEmployee();
//獲取到學校總部員工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------學校總部員工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
7. 合成複用原則
原則是儘量使用組合/聚合的方式,而不是使用繼承。
如果要使用繼承關係,則必須嚴格遵循里氏替換原則。合成複用原則和里氏替換原則相輔相成,兩者都是開閉原則的具體實現規範。
爲什麼不推薦優先使用繼承?
- 繼承複用破壞了類的封裝性。因爲繼承會將父類的實現細節暴露給子類,父類對子類是透明的,所以這種複用又稱爲“白箱”複用。而組合和聚合複用維持了類的封裝性。因爲成分對象的內部細節是新對象看不見的,所以這種複用又稱爲“黑箱”複用。
- 子類與父類的耦合度高。父類的實現的任何改變都會導致子類的實現發生變化,這不利於類的擴展與維護。
- 繼承限制了複用的靈活性。從父類繼承而來的實現是靜態的,在編譯時已經定義,所以在運行時不可能發生變化。而組合和聚合複用可以在運行時動態進行,新對象可以動態地引用與已有對象類型相同的對象,也就是說,在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分,新對象通過向這些對象的委派達到複用已有功能的目的。
設計模式的分類
設計模式有兩種分類方法,即 模式的目的 和 模式的作用範圍。
-
根據模式是用來完成什麼工作,這種方式可分爲 創建型模式、結構型模式 和 行爲型模式 3 種。
- 創建型模式:用於描述“怎樣創建對象”,它的主要特點是“將對象的創建與使用分離”。主要有單例、原型、工廠方法、抽象工廠、建造者等 5 種創建型模式。
- 結構型模式:用於描述如何將類或對象按某種佈局組成更大的結構。主要有代理、適配器、橋接、裝飾、外觀、享元、組合等 7 種結構型模式。
- 行爲型模式:用於描述類或對象之間怎樣相互協作共同完成單個對象都無法單獨完成的任務,以及怎樣分配職責。主要有模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄、解釋器等 11 種行爲型模式。
-
根據模式是主要用於類上還是主要用於對象上,這種方式可分爲 類模式 和 對象模式 兩種。
- 類模式:用於處理類與子類之間的關係,這些關係通過繼承來建立,是靜態的,在編譯時刻便確定下來了。工廠方法、(類)適配器、模板方法、解釋器屬於該模式。
- 對象模式:用於處理對象之間的關係,這些關係可以通過組合或聚合來實現,在運行時刻是可以變化的,更具動態性。除了以上 4 種,其他的都是對象模式。
總結
這 7 種設計原則是軟件設計模式必須儘量遵循的原則,各種原則要求的側重點不同。
- 開閉原則,要對擴展開放,對修改關閉
- 依賴倒置原則,要面向接口編程
- 接口隔離原則,接口要精簡設計
- 單一職責原則,實現類要職責單一
- 里氏替換原則,不要破壞繼承體系
- 迪米特法則,要降低類之間的耦合度
- 合成複用原則,優先使用組合或者聚合關係複用,少用繼承關係複用