4. 接口隔離原則(ISP)
(1)概念
接口隔離原則的定義是:建立單一的接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。
每個模塊應該是單一的接口,提供給幾個模塊就應該有幾個接口,而不是建立一個龐大臃腫的借口來容納所有客戶端訪問。
與單一職責原則不同:比如一個接口的職責可能包含10個方法,這10個方法都放在一個接口中,並且提供給多個模塊訪問。各個模塊按照規則的權限來訪問,在系統外通過文檔約束“不使用的方法不要訪問”。按照單一職責原則是允許的,按照接口隔離原則是不允許的,因為ISP要求盡量使用多個專門的接口,而不是一個龐大臃腫的接口。
(2)舉例
老師類和學生類實現工作的接口類:

實現代碼如下:
//工作接口類 public interface DoWork { // 學生類要實現的方法 public void doHomeWork(); // 老師類要實現的方法 public void correctingHomework(int StudentID); // 老師類和學生類共同需要實現的方法 public void attendClass(); }
//老師類實現工作接口 public class Teacher implements DoWork { private int teacherID; @Override public void doHomeWork() { // 應該是學生類調用的方法,由於老師類實現了接口DoWork就必須實現接口所有的方法,這里只能為空 } @Override public void correctingHomework(int StudentID) { System.out.println("老師批改作業..."); } @Override public void attendClass() { System.out.println("老師開始上課..."); } }
//學生類實現工作接口 public class Student implements DoWork{ private int studentID; @Override public void doHomeWork() { System.out.println("學生做作業..."); } @Override public void correctingHomework(int StudentID) { // 應該是老師類調用的方法,由於學生類實現了接口DoWork就必須實現接口所有的方法,這里只能為空 } @Override public void attendClass() { System.out.println("學生開始上課..."); } }
老師類需要實現correctingHomework()方法和attendClass()方法,學生類需要實現doHomework()方法和attendClass()方法,但這兩個類都有不需要實現的方法在接口中。由於實現了接口必須要實現接口中所有的方法,這些不需要的方法的方法體只能為空,顯然這不是一種好的設計。
按照接口隔離原則,對該接口進行拆分成3個接口,如下:

實現代碼如下:
//老師接口類 public interface DoWorkT { // 批改作業 public void correctingHomework(int studentID); }
//老師、學生公共接口類 public interface DoWorkC { // 上課 public void attendClass(); }
//學生接口類 public interface DoWorkS { // 做作業 public void doHomeWork(); }
//老師類實現工作接口 public class Teacher implements DoWorkT ,DoWorkC{ private int teacherID; @Override public void correctingHomework(int StudentID) { System.out.println("老師批改作業..."); } @Override public void attendClass() { System.out.println("老師開始上課..."); } }
//學生類實現工作接口 public class Student implements DoWorkS, DoWorkC { private int studentID; @Override public void doHomeWork() { System.out.println("學生做作業..."); } @Override public void attendClass() { System.out.println("學生開始上課..."); } }
(3)總結
接口隔離原則包含4層含義:
接口盡量要小;
接口要高內聚(即提高接口、類、模塊的處理能力,減少對外的交互,也就是說要有一定的獨立處理能力);
定制服務(即單獨為一個個體提供優良的服務,比如為一個模塊單獨設計其接口);
接口設計是有限度的(接口的設計粒度越小,系統越靈活,但同時也帶來了結構的復雜化,導致開發難度增加);
ISP的難點在於接口設計的這個“度”沒有一個固化或可測量的標准,接口設計一定要注意適度,而這個“度”也只能根據實際情況和經驗來進行判斷。
5. 迪米特法則(LOD)
(1)概念
迪米特法則又稱最少知道原則,定義是:一個對象應該對其他對象有最少的理解,即一個類應該對自己需要耦合或需要調用的類知道的最少。
(2)舉例
例A:一個類只能和朋友類交流
老師讓班長清點全班人數的類圖如下:

實現代碼如下:
public class Teacher { // 老師下發命令讓班長清點學生人數 public void commond(Monitor monitor) { // 初始化學生數量 List<Student> students = new ArrayList<Student>(); for (int i = 0; i < 30; i++) { students.add(new Student()); } //通知班長開始清點人數 monitor.countStudents(students); } }
public class Monitor { // 清點學生人數 public void countStudents(List<Student> students) { System.out.println("學生數量是" + students.size()); } }
public class Student { }
//場景調用類 public class Scene { public static void main(String[] args) { Teacher teacher = new Teacher(); teacher.commond(new Monitor()); } }
朋友類是這樣定義的:出現在成員變量、方法的輸入參數中的類稱為朋友類,出現在方法體內的類不能稱為朋友類。
上例中的Teacher類與Student類不是朋友類,卻與一個陌生類Student有了交流,這是違反了LOD的。將List<Student>初始化操作移動到場景類中,同時在Monitor類中注入List<Student>,避免Teacher類對Student類(陌生類)的訪問。改進后的類圖如下:

實現代碼如下:
public class Teacher { public void commond(Monitor monitor) { // 通知班長開始清點人數 monitor.countStudents(); } }
public class Monitor { private List<Student> students; // 構造函數注入 public Monitor(List<Student> students) { this.students = students; } // 清點學生人數 public void countStudents() { System.out.println("學生數量是" + students.size()); } }
public class Student { }
//場景調用類 public class Scene { public static void main(String[] args) { // 初始化學生數量 List<Student> students = new ArrayList<Student>(); for (int i = 0; i < 30; i++) { students.add(new Student()); } // 老師下發命令讓班長清點學生人數 Teacher teacher = new Teacher(); teacher.commond(new Monitor(students)); } }
例B:類與類之間的交流也是有距離的
模擬軟件安裝的向導:第一步,第二步(根據第一步判斷是否進行),第三步(根據第二步判斷是否進行)...,其類圖如下:

實現代碼如下:
//安裝向導類 public class Wizard { // 產生隨機數模擬用戶的不同選擇 private Random rand = new Random(); // 第一步 public int first() { System.out.println("安裝第一步..."); // 返回0-99之間的隨機數 return rand.nextInt(100); } // 第二步 public int second() { System.out.println("安裝第二步..."); return rand.nextInt(100); } // 第三步 public int third() { System.out.println("安裝第三步..."); return rand.nextInt(100); } }
//安裝類 public class InstallSoftware { public void installWizard(Wizard wizard) { int first = wizard.first(); // 根據第一步返回的數值判斷是否執行第二步 if (first > 50) { int second = wizard.second(); if (second < 50) { int third = wizard.third(); } } } }
//場景調用類 public class Scene { public static void main(String[] args) { InstallSoftware install = new InstallSoftware(); install.installWizard(new Wizard()); } }
上例的Wizard類把太多的方法暴露給InstallSoftware類,耦合關系變得異常牢固。如果將Wizard類中的first方法的返回類型由int更改為boolean,隨之就需要更改InstallSoftware類了,從而把修改變更的風險擴散開了。根據LOD原則,將Wizard類中的3個public方法修改為private方法,對安裝過程封裝在一個對外開放的InstallWizard中。對設計進行重構后的類圖如下:

實現代碼如下:
//安裝向導類 public class Wizard { // 產生隨機數模擬用戶的不同選擇 private Random rand = new Random(); // 第一步 private int first() { System.out.println("安裝第一步..."); // 返回0-99之間的隨機數 return rand.nextInt(100); } // 第二步 private int second() { System.out.println("安裝第二步..."); return rand.nextInt(100); } // 第三步 private int third() { System.out.println("安裝第三步..."); return rand.nextInt(100); } //對私有方法進行封裝,只對外開放這一個方法 public void installWizard(){ int first = this.first(); // 根據第一步返回的數值判斷是否執行第二步 if (first > 50) { int second = this.second(); if (second < 50) { int third = this.third(); } } } }
//安裝類 public class InstallSoftware { public void installWizard(Wizard wizard) { // 直接調用 wizard.installWizard(); } }
//場景調用類 public class Scene { public static void main(String[] args) { InstallSoftware install = new InstallSoftware(); install.installWizard(new Wizard()); } }
通過這樣重構后,類之間的耦合關系變弱。Wizard類只對外公布了一個public方法,即使要修改first()的返回值,影響的也僅僅是Wizard一個類本身,其他類不受任何影響,這體現了該類的高內聚特性。
(3)總結
一個類不要訪問陌生類(非朋友類),這樣可以降低系統間的耦合,提高了系統的健壯性。
在設計類時應該盡量減少使用public的屬性和方法,考慮是否可以修改為private,default,protected等訪問權限,是否可以加上final等關鍵字。
一個類公開的public方法越多,修改時涉及的面也就越大,變更引起的風險擴散也就越大。
6. 開閉原則(OCP)
(1)概念
開閉原則的定義是:軟件實體(類、模塊、方法)應該對擴展開發,對修改關閉。
即當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。
(2)舉例
書店剛開始賣小說類書籍,后來要求小說類書籍打折處理(40元以上9折,其他8折),再后來書店增賣計算機類書籍(比小說類書籍多一個屬性“類別”)。
書店剛開始賣小說類書籍的類圖如下:

實現代碼如下:
//書籍接口 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; public NovelBook(String name, int price, String author) { this.name = name; this.price = price; this.author = author; } @Override public String getName() { return this.name; } @Override public int getPrice() { return this.price; } @Override public String getAuthor() { return this.author; } }
//書店售書類 public class BookStore { private static List<IBook> books = new ArrayList<IBook>(); // 靜態塊初始化數據,在類加載時執行一次,先於構造函數 // 實際項目中一般由持久層完成 static { // 在非金融類項目中對貨幣的處理一般取兩位精度 // 通常的設計方法是在運算過程中擴大100倍,在顯示時再縮小100倍,以減小精度帶來的誤差 books.add(new NovelBook("小說A", 3200, "作者A")); books.add(new NovelBook("小說B", 5600, "作者B")); books.add(new NovelBook("小說C", 3500, "作者C")); books.add(new NovelBook("小說D", 4300, "作者D")); } // 模擬書店賣書 public static void main(String[] args) { // 設置價格精度 NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); // 展示所有書籍信息 for (IBook book : books) { System.out.println("書籍名稱:" + book.getName() + "\t書籍作者" + book.getAuthor() + "\t書籍價格" + formatter.format(book.getPrice() / 100.0) + "元"); } } }
輸出結果如下:
書籍名稱:小說A 書籍作者作者A 書籍價格¥32.00元 書籍名稱:小說B 書籍作者作者B 書籍價格¥56.00元 書籍名稱:小說C 書籍作者作者C 書籍價格¥35.00元 書籍名稱:小說D 書籍作者作者D 書籍價格¥43.00元
后來要求小說類書籍打折處理(40元以上9折,其他8折)
如果通過修改接口,在接口上新增加一個方法getOffPrice()專門來處理打折書籍,所有實現類實現該方法。那么與IBook接口相關的類都需要修改。而且作為接口應該是穩定且可靠的,不應經常變化,否則接口作為契約的作用就失去效能了。因此,此方案行不通。
如果修改實現類NovelBook中的方法,直接在getPrice()中實現打折處理,也可以達到預期效果。但采購人員看到的價格是打折后的價格,而看不到原來的價格。
綜上,按照OCP原則,應該通過擴展實現變化,增加一個子類OffNovelBook,重寫getPrice()方法實現打折處理。改進后的類圖如下:

修改后只需要增加一個子類OffNovelBook,修改BookStore類中static靜態塊中初始化方法即可。
修改代碼如下:
//為實現小說打折處理增加的子類 public class OffNovelBook extends NovelBook { public OffNovelBook(String name, int price, String author) { super(name, price, author); } // 復寫小說價格 @Override public int getPrice() { // 獲取原價 int price = super.getPrice(); // 打折后的處理價 int offPrice = 0; // 如果價格大於40打9折 if (price > 4000) { offPrice = price * 90 / 100; } else { // 其他打8折 offPrice = price * 80 / 100; } return offPrice; } }
//書店售書類 public class BookStore { private static List<IBook> books = new ArrayList<IBook>(); // 靜態塊初始化數據,在類加載時執行一次,先於構造函數 // 實際項目中一般由持久層完成 static { // 在非金融類項目中對貨幣的處理一般取兩位精度 // 通常的設計方法是在運算過程中擴大100倍,在顯示時再縮小100倍,以減小精度帶來的誤差 books.add(new OffNovelBook("小說A", 3200, "作者A")); books.add(new OffNovelBook("小說B", 5600, "作者B")); books.add(new OffNovelBook("小說C", 3500, "作者C")); books.add(new OffNovelBook("小說D", 4300, "作者D")); // 打折處理后只需更改靜態塊部分即可 } // 模擬書店賣書 public static void main(String[] args) { // 設置價格精度 NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); // 展示所有書籍信息 for (IBook book : books) { System.out.println("書籍名稱:" + book.getName() + "\t書籍作者" + book.getAuthor() + "\t書籍價格" + formatter.format(book.getPrice() / 100.0) + "元"); } } }
打折后的輸出結果如下:
書籍名稱:小說A 書籍作者作者A 書籍價格¥25.60元 書籍名稱:小說B 書籍作者作者B 書籍價格¥50.40元 書籍名稱:小說C 書籍作者作者C 書籍價格¥28.00元 書籍名稱:小說D 書籍作者作者D 書籍價格¥38.70元
再后來書店增賣計算機類書籍(比小說類書籍多一個屬性“類別”)
增加一個IComputerBook接口繼承IBook接口,增加一個ComputerBook類實現IComputerBook接口即可,其類圖如下:

增加兩個類后還需在BookStore類的static靜態塊中增加初始化數據即可。
修改代碼如下:
//增加的計算機書籍接口類 public interface IComputerBook extends IBook { // 聲明計算機書籍特有的屬性-類別 public String getScope(); }
//增加的計算機書籍實現類 public class ComputerBook implements IComputerBook { private String name; private int price; private String author; private String scope; public ComputerBook(String name, int price, String author, String scope) { this.name = name; this.price = price; this.author = author; this.scope = scope; } @Override public String getName() { return this.name; } @Override public int getPrice() { return this.price; } @Override public String getAuthor() { return this.author; } @Override public String getScope() { return this.scope; } }
//書店售書類 public class BookStore { private static List<IBook> books = new ArrayList<IBook>(); // 靜態塊初始化數據,在類加載時執行一次,先於構造函數 // 實際項目中一般由持久層完成 static { // 在非金融類項目中對貨幣的處理一般取兩位精度 // 通常的設計方法是在運算過程中擴大100倍,在顯示時再縮小100倍,以減小精度帶來的誤差 books.add(new OffNovelBook("小說A", 3200, "作者A")); books.add(new OffNovelBook("小說B", 5600, "作者B")); books.add(new OffNovelBook("小說C", 3500, "作者C")); books.add(new OffNovelBook("小說D", 4300, "作者D")); // 打折處理后只需更改靜態塊部分即可 // 添加計算機類書籍 books.add(new ComputerBook("計算機E", 3800, "作者E", "編程")); books.add(new ComputerBook("計算機F", 5400, "作者F", "編程")); } // 模擬書店賣書 public static void main(String[] args) { // 設置價格精度 NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); // 展示所有書籍信息 for (IBook book : books) { System.out.println("書籍名稱:" + book.getName() + "\t書籍作者" + book.getAuthor() + "\t書籍價格" + formatter.format(book.getPrice() / 100.0) + "元"); } } }
增加計算機類書籍后的輸出結果如下:
書籍名稱:小說A 書籍作者作者A 書籍價格¥25.60元 書籍名稱:小說B 書籍作者作者B 書籍價格¥50.40元 書籍名稱:小說C 書籍作者作者C 書籍價格¥28.00元 書籍名稱:小說D 書籍作者作者D 書籍價格¥38.70元 書籍名稱:計算機E 書籍作者作者E 書籍價格¥38.00元 書籍名稱:計算機F 書籍作者作者F 書籍價格¥54.00元
(3)總結
在業務規則改變的情況下,高層模塊必須有部分改變以適應新業務,但這種改變是很少的,也防止了變化風險的擴散。
開閉原則對測試是非常有利的,只需要測試增加的類即可。若改動原有的代碼實現新功能則需要重新進行大量的測試工作(回歸測試等)。
開閉原則是面向對象設計中“可復用設計”的基石。
開閉原則是面向對象設計的終極目標,其他原則可以看做是開閉原則的實現方法。
(補充)組合/聚合原則(CARP)
(1)概念
在面向對象的設計中,復用已有的設計或實現有兩種方法:繼承和聚合/組合。
而繼承有一些明顯的缺點:繼承破壞了封裝--基類的實現細節暴露給了子類;基類發生改變,子類隨着發生改變;子類繼承基類的方法是靜態的,不能在運行時發生改變,因此沒有足夠的靈活性。
組合/聚合原則的定義是:在一個新的對象里使用一些已有的對象,使之成為新對象的一部分。新對象通過調用已有對象的方法來達到復用的目的。
(2)舉例
教學管理系統部分數據庫訪問類設計如下圖:

如果需要更換數據庫連接方式,如原來采用JDBC連接數據庫,現在需要采用數據庫連接池進行連接。或者StudentDAO采用JDBC連接,TeacherDAO采用數據庫連接池連接。此時則需要增加一個新的DBUtil類,並修改StudentDAO類和TeacherDAO類的源代碼,違反了開閉原則。
現使用組合/聚合原則對其進行重構如下:

此時若需要增加新的數據庫連接方式,再增加一個DBUtil的子類即可:

(3)總結
當要復用代碼時首先想到使用組合/聚合的方式,其次才是使用繼承的方法。
只有“Is-A”關系才符合繼承關系,“Has-A”關系應當使用聚合來描述(”Is-A”代表一個類是另外一個類的一種(包含關系),而“Has-A”代表一個類是另外一個類的一個部分(屬於關系))。
6大設計原則詳解(一):http://www.cnblogs.com/LangZXG/p/6242925.html
6大設計原則,與常見設計模式(概述):http://www.cnblogs.com/LangZXG/p/6204142.html
類圖基礎知識:http://www.cnblogs.com/LangZXG/p/6208716.html
注:轉載請注明出處 http://www.cnblogs.com/LangZXG/p/6242927.html
