天天看點

6大設計原則詳解(二)

4. 接口隔離原則(ISP)

(1)概念

接口隔離原則的定義是:建立單一的接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。

每個子產品應該是單一的接口,提供給幾個子產品就應該有幾個接口,而不是建立一個龐大臃腫的借口來容納所有用戶端通路。

與單一職責原則不同:比如一個接口的職責可能包含10個方法,這10個方法都放在一個接口中,并且提供給多個子產品通路。各個子產品按照規則的權限來通路,在系統外通過文檔限制“不使用的方法不要通路”。按照單一職責原則是允許的,按照接口隔離原則是不允許的,因為ISP要求盡量使用多個專門的接口,而不是一個龐大臃腫的接口。

(2)舉例

老師類和學生類實作工作的接口類:

6大設計原則詳解(二)
實作代碼如下:

//工作接口類
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個接口,如下:

6大設計原則詳解(二)
//老師接口類
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:一個類隻能和朋友類交流

老師讓班長清點全班人數的類圖如下:

6大設計原則詳解(二)
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類(陌生類)的通路。改進後的類圖如下:

6大設計原則詳解(二)
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:類與類之間的交流也是有距離的

模拟軟體安裝的向導:第一步,第二步(根據第一步判斷是否進行),第三步(根據第二步判斷是否進行)...,其類圖如下:

6大設計原則詳解(二)
//安裝向導類
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中。對設計進行重構後的類圖如下:

6大設計原則詳解(二)
//安裝向導類
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折),再後來書店增賣計算機類書籍(比小說類書籍多一個屬性“類别”)。

書店剛開始賣小說類書籍的類圖如下:

6大設計原則詳解(二)
//書籍接口
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()方法實作打折處理。改進後的類圖如下:

6大設計原則詳解(二)

修改後隻需要增加一個子類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接口即可,其類圖如下:

6大設計原則詳解(二)

增加兩個類後還需在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)

在面向對象的設計中,複用已有的設計或實作有兩種方法:繼承和聚合/組合。

而繼承有一些明顯的缺點:繼承破壞了封裝--基類的實作細節暴露給了子類;基類發生改變,子類随着發生改變;子類繼承基類的方法是靜态的,不能在運作時發生改變,是以沒有足夠的靈活性。

組合/聚合原則的定義是:在一個新的對象裡使用一些已有的對象,使之成為新對象的一部分。新對象通過調用已有對象的方法來達到複用的目的。

教學管理系統部分資料庫通路類設計如下圖:

6大設計原則詳解(二)

如果需要更換資料庫連接配接方式,如原來采用JDBC連接配接資料庫,現在需要采用資料庫連接配接池進行連接配接。或者StudentDAO采用JDBC連接配接,TeacherDAO采用資料庫連接配接池連接配接。此時則需要增加一個新的DBUtil類,并修改StudentDAO類和TeacherDAO類的源代碼,違反了開閉原則。

現使用組合/聚合原則對其進行重構如下:

6大設計原則詳解(二)

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

6大設計原則詳解(二)

當要複用代碼時首先想到使用組合/聚合的方式,其次才是使用繼承的方法。

隻有“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

繼續閱讀