什么是設計模式?
設計模式(Design Pattern)是一套被反復使用、多數人知曉的、經過分類的、代碼設計經驗的總結。
使用設計模式的目的:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性,使代碼編寫真正工程化!
今天我們來聊一下設計模式的六大原則:
1、單一職責原則(Single responsibility principle,SRP)
2、里氏替換原則(Liskov Substitution Principle,LSP)
3、依賴倒置原則(Dependency Inversion Principle,DIP)
4、接口隔離原則(Interface Segregation Principle,ISP)
5、迪米特法則(Principle of Least Knowledge,PLK)
6、開閉原則(Open Closed Principle,OCP)
單一職責原則
定義:一個類只負責一項職責,不能存在多于一個導致類變更的原因;
問題由來:由類S負責兩個不同的職責S1、S2,當由于職責S2需求發生改變而需要修改S類時,有可能導致原本運行正常的職責S1發生故障;
解決方案:遵循單一職責原則,分別建立兩個類X1、X2,使X1負責S1的職責,X2負責S2的職責,這樣使職責S1和S2獨立,互不影響;
舉例說明:
有個工具類
public class Utils {
//方法1:求平方
public int function1(int a) {
return a * a;
}
//方法2:求某個值的平方后并除10
public int function2(int b) {
return function1(b) / 10;
}
}
有個需求,我要求10的平方并除以10
Utils utils = new Utils();
utils.function2(10);
但是現在我們有了另一個需求,求10的立方并除以10
這個時候我們就要修改Utils類里的function1方法,這顯然違背了單一職責原則,按照單一職責原則我們應該function1新建一個類util1負責 ,function2新建一個類util2負責,如果有了新需求(求立方)我們只需新建一個類util3創建function3方法即可,但是新建類花銷也是很大的,我們可以直接在Utils里新建一個function3方法來求立方也可以啊。這樣雖然也違背了單一職責原則,但在方法級別上卻是符合單一職責原則的,因為它并沒有動原來方法的代碼。
本文所舉的這個例子,它太簡單了,它只有一個方法,所以,無論是在代碼級別上違反單一職責原則,還是在方法級別上違反,都不會造成太大的影響。實際應用中的類都要復雜的多,一旦發生職責擴散而需要修改類時,除非這個類本身非常簡單,否則還是遵循單一職責原則的好。
單一職責原則的優點:
1、可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
2、提高類的可讀性,提高系統的可維護性;
3、變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
單一職責原則不只是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用單一職責原則。
里氏替換原則
定義:如果對每一個類型為 S1的對象 o1,都有類型為 S2 的對象o2,使得以 S1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時,程序 P 的行為沒有發生變化,那么類型 S2 是類型 S1 的子類型。
即:所有引用基類的地方必須能透明地使用其子類的對象。
問題由來:類A有一功能A1, A{A1},現在功能要擴展,擴展為類B(A的子類) 功能為原有A1和新功能B2,即B{A1,B2},這樣有可能會導致在實現新功能B2時導致原功能A1異常;
解決方案:當使用繼承時,遵循里氏替換原則,在B繼承A時,除添加新功能B2,盡量不要重新父類(A1)方法,也不要重載父類方法;
繼承包含這樣一層含義:父類中凡是已經實現好的方法,實際上是在設定一系列的規范和契約,如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
舉例說明:
現在有一個類A實現一個方法計算兩個數的和
public class A {
public int func1(int a, int b) {
return a + b;
}
}
A a = new A();
System.out.println(a.func1(20, 10));
運行結果:30
現在由于功能要擴展,我們要計算兩個數的和的兩倍,于是我們建立了一個新類B
public class B extends A {
public int func1(int a, int b) {
return a - b;
}
public int func2(int a, int b) {
return func1(a, b) * 2;
}
}
由于父類已經實現了兩個數相加,因為我們擴展的方法可以直接使用父類的方法
B b = new B();
System.out.println("20+10=" + b.func1(20, 10));
System.out.println("(20+10)*2=" + b.func2(20, 10));
運行結果:
20+10=10
(20+10)*2=20
可以看到,原本運行正常的func1出現了問題,原因就是B類在起名的時候無意中重寫了父類的方法,造成所有運行相加功能的代碼全部調用了B類復寫后的方法,造成原本運行正常的功能出現了錯誤。
里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。
它包含以下4層含義:
1、子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
2、子類中可以增加自己特有的方法。
3、當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
4、當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
依賴倒置原則
核心原則:
A:高層次的模塊不應該依賴于低層次的模塊,他們都應該依賴于抽象。
B:抽象不應該依賴于具體,具體應該依賴于抽象。
問題由來:類B繼承類A,如果將類B改為繼承類C,則必須通過修改類B的代碼來達成;這種情況下類B一般是高層模塊,負責復雜的業務邏輯,類A和C都是低層模塊,負責基本的原始操作,這時候如果修改類B,給程序帶來了不必要的風險;
解決方案:將類B依賴接口S,類A和C各自實現接口S,類B通過接口S與類A和C發生聯系,則大大降低修改類B的幾率;
依賴倒置原則基于這樣一個事實:相對于細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規范和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
依賴倒置原則的核心思想是面向接口編程;
舉例說明:
假如有一個吃水果的需求,有這樣幾個類:
public class Apple {
public String getApple() {
return "蘋果";
}
}
public class Me {
//吃蘋果
public void eat(Apple apple) {
System.out.println("我開始吃:" + apple.getApple());
}
}
Me me = new Me();
me.eat(new Apple());
運行結果:我開始吃蘋果
假如有一條,我想吃梨于是我們新建一個類
public class Pear {
public String getPear() {
return "梨";
}
}
public class Me {
//吃蘋果
public void eat(Apple apple) {
System.out.println("我開始吃:" + apple.getApple());
}
//吃梨
public void eat(Pear pear) {
System.out.println("我開始吃:" + pear.getPear());
}
}
Me me = new Me();
me.eat(new Apple());
me.eat(new Pear());
運行結果:
我開始吃蘋果
我開始吃梨
如果有一個我想吃橘子,想吃香蕉,什么都想吃呢,難道還要這樣一直加下去?當然不是,我們可以根據依賴倒置原則,面向接口編程;我們引入一個接口IFruits;
public interface IFruits {
String getFruit();
}
public class Apple implements IFruits {
@Override
public String getFruit() {
return "蘋果";
}
}
public class Pear implements IFruits {
@Override
public String getFruit() {
return "梨";
}
}
Me me = new Me();
me.eat(new Apple());
me.eat(new Pear());
這樣,無論我們以后想吃什么,都無需修改Me類,如有想吃的水果,只需新建一個類實現IFruits接口即可;
這就是依賴倒置原則的典型應用,在實際開發過程中,我們應該遵循以下三點:
1、低層模塊盡量都要有抽象類或接口,或者兩者都有。
2、變量的聲明類型盡量是抽象類或接口。
3、使用繼承時遵循里氏替換原則。
依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。
接口隔離原則
定義:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。
核心原則:
1、使用多個專門的接口比使用單一的總接口要好。
2、一個類對另外一個類的依賴性應當是建立在最小的接口上的。
3、一個接口代表一個角色,不應當將不同的角色都交給一個接口。沒有關系的接口合并在一起,形成一個臃腫的大接口,這是對角色和接口的污染。
4、“不應該強迫客戶依賴于它們不用的方法。接口屬于客戶,不屬于它所在的類層次結構?!边@個說得很明白了,再通俗點說,不要強迫客戶使用它們不用的方法,如果強迫用戶使用它們不使用的方法,那么這些客戶就會面臨由于這些不使用的方法的改變所帶來的改變。
問題由來:類a通過接口i依賴類A,類b通過接口i依賴類B,如果接口i對于a和b來說不是最小接口,則類A和B必須要去實現它們不需要的方法;
解決方案:將臃腫的類i拆分獨立的幾個接口,類a和b分別與他們需要的接口建立依賴關系,也就是采用接口隔離原則;
舉例說明:
//定義一個接口I
public interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
類A
public class A implements I {
@Override
public void method1() {
System.out.println("實現接口I的方法1");
}
@Override
public void method2() {
System.out.println("實現接口I的方法2");
}
@Override
public void method3() {
System.out.println("實現接口I的方法3");
}
@Override
public void method4() {
//不需要
}
@Override
public void method5() {
//不需要
}
}
類B
public class B implements I{
@Override
public void method1() {
//不需要
}
@Override
public void method2() {
//不需要
}
@Override
public void method3() {
//不需要
}
@Override
public void method4() {
System.out.println("實現接口I的方法4");
}
@Override
public void method5() {
System.out.println("實現接口I的方法5");
}
}
可以看到類A和B都實現了接口I,但是對于A和B來說,它們都實現了它們不需要的方法,I對于A和B來說并不是最小接口,因此我們應該遵循接口隔離原則,分別建立I1和I2定義A和B各自所需要的方法,如下:
public interface I1 {
public void method1();
public void method2();
public void method3();
}
public interface I2 {
public void method4();
public void method5();
}
public class A implements I1 {
@Override
public void method1() {
System.out.println("實現接口I1的方法1");
}
@Override
public void method2() {
System.out.println("實現接口I1的方法2");
}
@Override
public void method3() {
System.out.println("實現接口I1的方法3");
}
}
public class B implements I2 {
@Override
public void method4() {
System.out.println("實現接口I2的方法4");
}
@Override
public void method5() {
System.out.println("實現接口I2的方法5");
}
}
以上接口定義看起來就清晰了很多,類A和B分別依賴自己所需要的方法,不去依賴不需要的方法。 接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。
到這里可能有人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口接口,主要針對抽象,針對程序整體框架的構建。
使用接口隔離原則是要注意一下幾點:
1、接口盡量小,但是要有限度。對接口進行細化可以提高程序設計靈活性是不掙的事實,但是如果過小,則會造成接口數量過多,使設計復雜化。所以一定要適度。
2、為依賴接口的類定制服務,只暴露給調用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務,才能建立最小的依賴關系。
3、提高內聚,減少對外交互。使接口用最少的方法去完成最多的事情。
迪米特法則
定義:一個對象應該對其他對象保持最少的理解;
問題由來:類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
解決方案:降低類與類之間的耦合;
迪米特法則又叫最少知道原則,就是一個類對自己所依賴的類知道的越少越好。迪米特法則還有一個定義即:只與直接的朋友通信。什么是直接的朋友?
每個對象都會與其他對象有耦合關系,只要兩個對象之間有耦合關系,我們就說這兩個對象之間是朋友關系。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變量、方法參數、方法返回值中的類為直接的朋友,而出現在局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要作為局部變量的形式出現在類的內部。
舉例說明
一個集團公司都有自己的下屬子公司,現在我們想打印一下集團所有員工的ID包括子公司;
//總公司員工
public class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//子公司員工
public class SubEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
public class SubCompanyManager {
public List<SubEmployee> getAllEmployee() {
List<SubEmployee> list = new ArrayList<SubEmployee>();
for (int i = 0; i < 100; i++) {
SubEmployee emp = new SubEmployee();
//為分公司人員按順序分配一個ID
emp.setId("分公司" + i);
list.add(emp);
}
return list;
}
}
public class CompanyManager {
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 30; i++) {
Employee emp = new Employee();
//為總公司人員按順序分配一個ID
emp.setId("總公司" + i);
list.add(emp);
}
return list;
}
//打印公司所有員工ID
public void printAllEmployee(SubCompanyManager sub) {
List<SubEmployee> list1 = sub.getAllEmployee();
for (SubEmployee e : list1) {
System.out.println(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
現在這個設計的主要問題出在CompanyManager中,根據迪米特法則,只與直接的朋友發生通信,而SubEmployee類并不是CompanyManager類的直接朋友(以局部變量出現的耦合不屬于直接朋友),從邏輯上講總公司只與他的分公司耦合就行了,與分公司的員工并沒有任何聯系,這樣設計顯然是增加了不必要的耦合。按照迪米特法則,應該避免類中出現這樣非直接朋友關系的耦合。修改后的代碼如下:
class SubCompanyManager{
public List<SubEmployee> getAllEmployee(){
List<SubEmployee> list = new ArrayList<SubEmployee>();
for(int i=0; i<100; i++){
SubEmployee emp = new SubEmployee();
//為分公司人員按順序分配一個ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
public void printEmployee(){
List<SubEmployee> list = this.getAllEmployee();
for(SubEmployee e:list){
System.out.println(e.getId());
}
}
}
class CompanyManager{
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<Employee>();
for(int i=0; i<30; i++){
Employee emp = new Employee();
//為總公司人員按順序分配一個ID
emp.setId("總公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
sub.printEmployee();
List<Employee> list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
修改后,為分公司增加了打印人員ID的方法,總公司直接調用來打印,從而避免了與分公司的員工發生耦合。
迪米特法則的初衷是降低類之間的耦合,由于每個類都減少了不必要的依賴,因此的確可以降低耦合關系。但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發生聯系,例如本例中,總公司就是通過分公司這個“中介”來與分公司的員工發生聯系的。過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統復雜度變大。所以在采用迪米特法則時要反復權衡,既做到結構清晰,又要高內聚低耦合。
開閉原則
主要特征
1、對于擴展是開放的(Open for extension)。這意味著模塊的行為是可以擴展的。當應用的需求改變時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行為。也就是說,我們可以改變模塊的功能。
2、對于修改是關閉的(Closed for modification)。對模塊行為進行擴展時,不必改動模塊的源代碼或者二進制代碼。模塊的二進制可執行版本,無論是可鏈接的庫、DLL或者.EXE文件,都無需改動。
定義:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。
問題由來:軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,并且需要原有代碼經過重新測試。
解決方案:當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。
開閉原則是面向對象設計中最基礎的設計原則,它指導我們如何建立穩定靈活的系統。開閉原則可能是設計模式六項原則中定義最模糊的一個了,它只告訴我們對擴展開放,對修改關閉,可是到底如何才能做到對擴展開放,對修改關閉,并沒有明確的告訴我們。
在仔細思考以及仔細閱讀很多設計模式的文章后,終于對開閉原則有了一點認識。其實,我們遵循設計模式前面5大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要我們對前面5項原則遵守的好了,設計出的軟件自然是符合開閉原則的,這個開閉原則更像是前面五項原則遵守程度的“平均得分”,前面5項原則遵守的好,平均分自然就高,說明軟件設計開閉原則遵守的好;如果前面5項原則遵守的不好,則說明開閉原則遵守的不好。
開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。而軟件中易變的細節,我們用從抽象派生的實現類來進行擴展,當軟件需要發生變化時,我們只需要根據需求重新派生一個實現類來擴展就可以了。當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性才行。
最后說明一下如何去遵守這六個原則。對這六個原則的遵守并不是是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是一樣,制定這六個原則的目的并不是要我們刻板的遵守他們,而需要根據實際情況靈活運用。對他們的遵守程度只要在一個合理的范圍內,就算是良好的設計。
我自己的總結,其實設計模式強調的是一種思想,非邏輯處理,我們雖然明白了其明面上的定義,但具體的實現還要靠我們在項目中去運用,如何靈活的運用,做優秀的設計,寫出更好的代碼,才能真正掌握設計模式的精髓。