6大設計原則詳解(二)


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM