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)
迪米特法則又稱最少知道原則,定義是:一個對象應該對其他對象有最少的了解,即一個類應該對自己需要耦合或需要調用的類知道的最少。
例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一個類本身,其他類不受任何影響,這展現了該類的高内聚特性。
一個類不要通路陌生類(非朋友類),這樣可以降低系統間的耦合,提高了系統的健壯性。
在設計類時應該盡量減少使用public的屬性和方法,考慮是否可以修改為private,default,protected等通路權限,是否可以加上final等關鍵字。
一個類公開的public方法越多,修改時涉及的面也就越大,變更引起的風險擴散也就越大。
6. 開閉原則(OCP)
開閉原則的定義是:軟體實體(類、子產品、方法)應該對擴充開發,對修改關閉。
即當軟體需要變化時,盡量通過擴充軟體實體的行為來實作變化,而不是通過修改已有的代碼來實作變化。
書店剛開始賣小說類書籍,後來要求小說類書籍打折處理(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元
在業務規則改變的情況下,高層子產品必須有部分改變以适應新業務,但這種改變是很少的,也防止了變化風險的擴散。
開閉原則對測試是非常有利的,隻需要測試增加的類即可。若改動原有的代碼實作新功能則需要重新進行大量的測試工作(回歸測試等)。
開閉原則是面向對象設計中“可複用設計”的基石。
開閉原則是面向對象設計的終極目标,其他原則可以看做是開閉原則的實作方法。
(補充)組合/聚合原則(CARP)
在面向對象的設計中,複用已有的設計或實作有兩種方法:繼承和聚合/組合。
而繼承有一些明顯的缺點:繼承破壞了封裝--基類的實作細節暴露給了子類;基類發生改變,子類随着發生改變;子類繼承基類的方法是靜态的,不能在運作時發生改變,是以沒有足夠的靈活性。
組合/聚合原則的定義是:在一個新的對象裡使用一些已有的對象,使之成為新對象的一部分。新對象通過調用已有對象的方法來達到複用的目的。
教學管理系統部分資料庫通路類設計如下圖:
如果需要更換資料庫連接配接方式,如原來采用JDBC連接配接資料庫,現在需要采用資料庫連接配接池進行連接配接。或者StudentDAO采用JDBC連接配接,TeacherDAO采用資料庫連接配接池連接配接。此時則需要增加一個新的DBUtil類,并修改StudentDAO類和TeacherDAO類的源代碼,違反了開閉原則。
現使用組合/聚合原則對其進行重構如下:
此時若需要增加新的資料庫連接配接方式,再增加一個DBUtil的子類即可:
當要複用代碼時首先想到使用組合/聚合的方式,其次才是使用繼承的方法。
隻有“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