1. 程式人生 > >設計模式六大原則(詳細) 設計模式六大原則

設計模式六大原則(詳細) 設計模式六大原則

設計模式六大原則

轉自https://www.cnblogs.com/shijingjing07/p/6227728.html

1.設計模式的目的
設計模式是為了更好的程式碼重用性,可讀性,可靠性,可維護性。

2.常用的六大設計模式
1)單一職責原則
2)里氏替換原則
3)依賴倒轉原則
4)介面隔離原則
5)迪米特法則
6)開閉原則

3.單一職責原則
該原則是針對類來說的,即一個類應該只負責一項職責。
如類T負責兩個不同職責:職責P1,職責P2。當職責P1需求變更而改變T時,可能造成職責P2發生故障,所以需要將類T的粒度分解為T1,T2。
示例如下:
用一個類秒數動物呼吸這個場景

複製程式碼
class Animal {
    public void breathe(string animal)
    {
        Console.WriteLine(animal+"呼吸空氣");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe("魚");
        Console.ReadLine();
    }
}
複製程式碼

輸出結果:

我們發現不是所有動物都是呼吸空氣的,比如魚就是呼吸水的,根據單一職責原則,我們將Animal類細分為陸生動物類和水生動物類,如下所示:

複製程式碼
class Terrestrial
{
    public void breathe(string animal)
    {
        Console.WriteLine(animal+"呼吸空氣");
    }
}
class Aquatic
{
    public void breathe(string animal)
    {
        Console.WriteLine(animal + "呼吸水");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("豬");
        Aquatic aquatic = new Aquatic();
        aquatic.breathe("魚");
        Console.ReadLine();
    }
}
複製程式碼

我們發現這樣修改的花銷很大,既要將原來的類分解,又要修改客戶端。而直接修改Animal類雖然違背了單一職責原則,但花銷小的多,如下所示:

複製程式碼
class Animal
{
    public void breathe(string animal)
    {
        if ("魚".Equals(animal))
        {
            Console.WriteLine(animal + "呼吸水");
        }
        else {
            Console.WriteLine(animal + "呼吸空氣");
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe("魚");
        Console.ReadLine();
    }
}
複製程式碼

可以看到,這種修改方式簡單的多。但卻存在隱患,一天需要將魚分為淡水魚,海水魚,又需要修改Animal類的breathe方法。可能給“豬牛羊”等相關功能帶來風險,這種修改直接在程式碼級別違背了單一職責原則,雖然修改起來最簡單,但隱患最大。還有一種修改方式:

複製程式碼
class Animal
{
    public void breathe(string animal)
    {
         Console.WriteLine(animal + "呼吸空氣");
    }
    public void breathe2(string animal)
    {
        Console.WriteLine(animal + "呼吸水");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe2("魚");
        Console.ReadLine();
    }
} 
複製程式碼

這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然違背了單一職責原則,但在方法級別上卻是符合單一職責原則的。那麼在實際程式設計中,採用哪一種呢?我的原則是,只有邏輯足夠簡單,才可以在程式碼級違反單一職責原則;只有類中方法數量足夠少,才可以在方法級別違反單一職責原則。

遵循單一職責的優點:
1)降低類的複雜度,一個類只負責一項職責。
2)提高類的可讀性,可維護性
3)降低變更引起的風險。

4.里氏替換原則
該原則是在1988年,由麻省理工學院的以為姓裡的女士提出的。
如果對每個型別為T1的物件o1,都有型別為T2的物件o2,使得以T1定義的所有程式P在所有的物件o1都代換成o2時,程式P的行為沒有發生變化,那麼型別T2是型別T1的子型別。
換句話說,所有引用基類的地方必須能透明地使用其子類的物件。

由定義可知,在使用繼承時,遵循里氏替換原則,在子類中儘量不要重寫和過載父類的方法。
繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對抽象方法而言),實際上是在設定一系列的規範和契約,雖然它不強制要求所有的子類必須遵循這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
繼承作為面向物件三大特性之一,在給程式設計帶來巨大遍歷的同時,也帶來了弊端。比如使用繼承會給程式帶來侵入性,程式的可移植性降低,增加物件間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能產生故障。
舉例說明繼承的風險,我們需要完成一個兩數相減的功能,由類A來負責。

複製程式碼
class A{
    public int func1(int a,int b){
        return a-b;
    }
}
public class Client{
    public static void main(string[] args){
        A a=new A();
        System.out.println("100-50="+a.func1(100,50));
        System.out.println("100-80="+a.func1(100,80));
    }
}
複製程式碼

執行結果:

100-50=50
100-80=20

後來,我們需要增加一個新的功能:完成兩數相加,然後再與100求和,由類B來負責。

複製程式碼
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)+100;
    }
}
public class Client{
    public static void main(string[] args){
        B a=new B();
        System.out.println("100-50="+b.func1(100,50));
        System.out.println("100-80="+b.func1(100,80));
        System.out.println("100+20+100="+b.func2(100,20));
    }
}
複製程式碼

執行結果:

100-50=150
100-80=180
100+20+100=220

我們發現原來執行正常的相減功能發生了錯誤。原因就是類B無意中重寫了父類的方法,造成原有功能出現錯誤。在實際程式設計中,我們常常會通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個繼承體系的複用性會比較差。特別是執行多型比較頻繁的時候,如果非要重寫父類的方法,通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴,聚合,組合等關係代替。

5.依賴倒轉原則
高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。
類A直接依賴類B,如果要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。此時,類A一般是高層模組,負責複雜的業務邏輯,類B和類C是低層模組,負責基本的原子操作;修改A會給程式帶來風險。
將類A修改未依賴介面I,類B和類C各自實現介面I,類A通過介面I間接與類B或類C發生聯絡,則會大大降低修改類A的記機率。
依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建的架構比以細節為基礎的架構要穩定的多。在java中,抽象指的是介面或抽象類,細節就是具體的實現類,使用介面或抽象類的目的是制定好規範,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
依賴倒置的中心思想是面向介面程式設計。

程式碼示例如下:

複製程式碼
class Book {
    public string getContent() {
        return "很久很久以前。。。。。";
    }
}
class Mother {
    public void narrate(Book book)
    {
        Console.WriteLine(book.getContent());
    }
}
class Program
{
    static void Main(string[] args)
    {
        Mother monther = new Mother();
        monther.narrate(new Book());
        Console.ReadLine();
    }
}
複製程式碼

執行結果:

如果讀的物件是報紙,雜誌,卻發現客戶端不適用了。
我們引入一個抽象的介面IReader,代表讀物

interface IReader{
    public string getContent();
}

這樣Mother類與介面IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,他們各自都去實現IReader介面,這樣就符合依賴倒置原則了,修改程式碼如下:

複製程式碼
interface IReader {
         string getContent();
    }
    class Newspaper: IReader
    {
    public string getContent()
    {
        return "切爾西豪取12連勝";
    }
}
class Book:IReader
{

    public string getContent()
{
    return "很久很久以前。。。。";
}
}
class Mother
{
    public void narrate(IReader reader)
    {
        Console.WriteLine(reader.getContent());
    }
}
class Program
{
    static void Main(string[] args)
    {
        Mother monther = new Mother();
        monther.narrate(new Book());
        monther.narrate(new Newspaper());
        Console.ReadLine();
    }
}
複製程式碼

執行結果:

採用依賴倒置原則給多人並行開發帶來極大的便利,比如上列中Mother類與Book類直接耦合,Mother必須等Book類編碼完成後才可以進行編碼,因為Mother類依賴於Book類。修改後的程式可以同時開工,互不影響。
依賴關係的傳遞有三種方式,介面傳遞,構造方法傳遞和setter方法傳遞。
介面傳遞:

複製程式碼
interface IDriver{
    public void drive(ICar car);
}
public class Driver:IDriver{
    public void drive(ICar car){
        car.run();
    }
}
複製程式碼

構造方法傳遞:

複製程式碼
interface IDriver{
    public void drive();
}
public class Driver implements IDriver{
    public ICar car;
    public Driver(ICar _car){
        this.car=_car;
    }
    public void drive(){
        this.car.run();
    }
}
複製程式碼

setter方式傳遞:

複製程式碼
interface IDriver{
    public void setCar(ICar car);
    public void drive();
}
public class Driver:IDriver{
    PRIVATE ICar car;
    public void setCar(ICar car){
        this.car=car;
    }
    public void drive(){
        this.car.run();
    }
}
複製程式碼

在實際程式設計中,一般需要做到如下3點:
低層模組儘量都要有抽象類或介面,或者兩者都有。
變數的宣告型別儘量是抽象類或介面。
使用繼承時遵循里氏替換原則

6.介面隔離原則
客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。
類A通過介面I依賴類B,類C通過介面I依賴類D,如果介面I對於類A和類C來說不是最小介面,則類B和類D必須去實現他們不需要的方法。
將臃腫的介面I拆分為獨立的幾個介面,類A和類C分別與他們需要的介面建立依賴關係。也就是採用介面隔離原則。
舉例說明介面隔離原則:

這個圖的意思是:類A依賴介面I中的方法1,方法2,方法3,類B是對類A依賴的實現;類C依賴介面I中的方法1,方法4,方法5,類D是對類C依賴的實現。對於類B和類D來說,雖然存在用不到的方法(紅色標記所示),但由於實現了介面I,所以也必須要實現這些用不到的方法。程式碼如下:

複製程式碼
interface I{
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
}
class A{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method2();
    }
    public void depend3(I i){
        i.method3();
    }
}
class C{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method4();
    }
    public void depend3(I i){
        i.method5();
    }
}
class B:I{
    public void method1(){
        Console.WriteLine("類B實現介面I的方法1");
    }
    public void method2(){
        Console.WriteLine("類B實現介面I的方法2");
    }
    public void method3(){
        Console.WriteLine("類B實現介面I的方法3");
    }
    public void method4(){}
    public void method5(){}
}
class D:I{
    public void method1(){
        Console.WriteLine("類B實現介面I的方法1");
    }
    public void method2(){}
    public void method3(){}
    public void method4(){
        Console.WriteLine("類B實現介面I的方法4");
    }
    public void method5(){
        Console.WriteLine("類B實現介面I的方法5");
    }
}
class Program
{
    static void Main(string[] args)
    {
        A a=new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());
        
        C c=new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
        Console.ReadLine();
    }
}
複製程式碼

可以看到,介面中出現的方法,不管對依賴於它的類有沒有作用,實現類中都必須去實現這些方法。於是我們將原介面I拆分為三個介面:

程式碼如下所示:

複製程式碼
interface I1{
    void method1();
}
interface I2{
    void method2();
    void method3();
}
interface I3{
    void method4();
    void method5();
}
class A{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I2 i){
        i.method2();
    }
    public void depend3(I2 i){
        i.method3();
    }
}
class C{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I3 i){
        i.method4();
    }
    public void depend3(I3 i){
        i.method5();
    }
}
class B:I1,I2{
    public void method1(){
        Console.WriteLine("類B實現介面I1的方法1");
    }
    public void method2(){
        Console.WriteLine("類B實現介面I2的方法2");
    }
    public void method3(){
        Console.WriteLine("類B實現介面I2的方法3");
    }
}
class D:I1,I3{
    public void method1(){
        Console.WriteLine("類B實現介面I的方法1");
    }
    public void method4(){
        Console.WriteLine("類B實現介面I的方法4");
    }
    public void method5(){
        Console.WriteLine("類B實現介面I的方法5");
    }
}
class Program
{
    static void Main(string[] args)
    {
        A a=new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());
        
        C c=new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
        Console.ReadLine();
    }
}
複製程式碼

說到這裡,可能會覺得介面隔離原則和之前的單一職責原則很相似,其實不然。一,單一職責注重職責,而介面隔離原則注重對介面依賴的隔離;二,單一職責是約束類,其次是方法,針對的是程式中的實現和細節;而介面隔離原則約束的是介面,針對的是抽象,程式整體框架的構建。

7.迪米特法則
一個物件應該對其他物件保持最少的瞭解。
類與類關係越密切,耦合度越大。
迪米特法則又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都儘量將邏輯封裝在類的內部。對外除了提供的public 方法,不對外洩露任何資訊。
迪米特法則還有個更簡單的定義:只與直接的朋友通訊。
什麼是直接的朋友:每個物件都會與其他物件由耦合關係,只要兩個物件之間有耦合關係,我們就說這兩個物件之間是朋友關係。耦合的方式很多,依賴,關聯,組合,聚合等。其中,我們稱出現成員變數,方法引數,方法返回值中的類為直接的朋友,而出現在區域性變數中的類不是直接的朋友。也就是說,陌生的類最好不要以區域性變數的形式出現在類的內部。
舉例額說明如下,有一個集團公司,下屬單位有分公司和直屬部門,現要求打印出所有下屬單位的員工ID。

複製程式碼
class Employee{
    private string id;
    public void setId(string id){
        this.id=id;
    }
    public string getId(){
        return id;
    }
}
class SubEmployee{
    private string id;
    public void setId(string id){
        this.id=id;
    }
    public string getId(){
        return id;
    }
}
class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list=new ArrayList(SubEmployee);
        for(int i=0;i<100;i++){
            SubEmployee emp=new SubEmployee();
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
}
class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list=new ArrayList<Employee>();
        for(int i=0;i<30;i++)
        {
            Employee emp=new Employee();
            emp.setId("總公司"+i);
            list.add(emp);
        }
        return list;
    }
    publi void printAllEmployee(SubCompanyManager sub){
        List<SubEmployee> list1=sub.getAllEmployee();
        foreach(SubEmployee e in list1){
            Console.WriteLine(e.getId());
        }
        List<Employee> list2=this.getAllEmployee();
        foreach(Employee e in list2){
            Console.WriteLine(e.getId());
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        CompanyManager e=new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
        Console.ReadLine();
    }
}
複製程式碼

這個設計的問題在於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());
        }
    }
}
複製程式碼

迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關係。

8.開閉原則
一個軟體實體如類,模組和函式應該對擴充套件開放,對修改關閉。用抽象構建框架,用實現擴充套件細節。
當軟體需要變化時,儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來實現變化。
當我們遵循前面介紹的5大原則,以及使用23中設計模式的目的就是遵循開閉原則。

1.設計模式的目的
設計模式是為了更好的程式碼重用性,可讀性,可靠性,可維護性。

2.常用的六大設計模式
1)單一職責原則
2)里氏替換原則
3)依賴倒轉原則
4)介面隔離原則
5)迪米特法則
6)開閉原則

3.單一職責原則
該原則是針對類來說的,即一個類應該只負責一項職責。
如類T負責兩個不同職責:職責P1,職責P2。當職責P1需求變更而改變T時,可能造成職責P2發生故障,所以需要將類T的粒度分解為T1,T2。
示例如下:
用一個類秒數動物呼吸這個場景

複製程式碼
class Animal {
    public void breathe(string animal)
    {
        Console.WriteLine(animal+"呼吸空氣");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe("魚");
        Console.ReadLine();
    }
}
複製程式碼

輸出結果:

我們發現不是所有動物都是呼吸空氣的,比如魚就是呼吸水的,根據單一職責原則,我們將Animal類細分為陸生動物類和水生動物類,如下所示:

複製程式碼
class Terrestrial
{
    public void breathe(string animal)
    {
        Console.WriteLine(animal+"呼吸空氣");
    }
}
class Aquatic
{
    public void breathe(string animal)
    {
        Console.WriteLine(animal + "呼吸水");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("豬");
        Aquatic aquatic = new Aquatic();
        aquatic.breathe("魚");
        Console.ReadLine();
    }
}
複製程式碼

我們發現這樣修改的花銷很大,既要將原來的類分解,又要修改客戶端。而直接修改Animal類雖然違背了單一職責原則,但花銷小的多,如下所示:

複製程式碼
class Animal
{
    public void breathe(string animal)
    {
        if ("魚".Equals(animal))
        {
            Console.WriteLine(animal + "呼吸水");
        }
        else {
            Console.WriteLine(animal + "呼吸空氣");
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe("魚");
        Console.ReadLine();
    }
}
複製程式碼

可以看到,這種修改方式簡單的多。但卻存在隱患,一天需要將魚分為淡水魚,海水魚,又需要修改Animal類的breathe方法。可能給“豬牛羊”等相關功能帶來風險,這種修改直接在程式碼級別違背了單一職責原則,雖然修改起來最簡單,但隱患最大。還有一種修改方式:

複製程式碼
class Animal
{
    public void breathe(string animal)
    {
         Console.WriteLine(animal + "呼吸空氣");
    }
    public void breathe2(string animal)
    {
        Console.WriteLine(animal + "呼吸水");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe2("魚");
        Console.ReadLine();
    }
} 
複製程式碼

這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然違背了單一職責原則,但在方法級別上卻是符合單一職責原則的。那麼在實際程式設計中,採用哪一種呢?我的原則是,只有邏輯足夠簡單,才可以在程式碼級違反單一職責原則;只有類中方法數量足夠少,才可以在方法級別違反單一職責原則。

遵循單一職責的優點:
1)降低類的複雜度,一個類只負責一項職責。
2)提高類的可讀性,可維護性
3)降低變更引起的風險。

4.里氏替換原則
該原則是在1988年,由麻省理工學院的以為姓裡的女士提出的。
如果對每個型別為T1的物件o1,都有型別為T2的物件o2,使得以T1定義的所有程式P在所有的物件o1都代換成o2時,程式P的行為沒有發生變化,那麼型別T2是型別T1的子型別。
換句話說,所有引用基類的地方必須能透明地使用其子類的物件。

由定義可知,在使用繼承時,遵循里氏替換原則,在子類中儘量不要重寫和過載父類的方法。
繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對抽象方法而言),實際上是在設定一系列的規範和契約,雖然它不強制要求所有的子類必須遵循這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
繼承作為面向物件三大特性之一,在給程式設計帶來巨大遍歷的同時,也帶來了弊端。比如使用繼承會給程式帶來侵入性,程式的可移植性降低,增加物件間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能產生故障。
舉例說明繼承的風險,我們需要完成一個兩數相減的功能,由類A來負責。

複製程式碼
class A{
    public int func1(int a,int b){
        return a-b;
    }
}
public class Client{
    public static void main(string[] args){
        A a=new A();
        System.out.println("100-50="+a.func1(100,50));
        System.out.println("100-80="+a.func1(100,80));
    }
}
複製程式碼

執行結果:

100-50=50
100-80=20

後來,我們需要增加一個新的功能:完成兩數相加,然後再與100求和,由類B來負責。

複製程式碼
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)+100;
    }
}
public class Client{
    public static void main(string[] args){
        B a=new B();
        System.out.println("100-50="+b.func1(100,50));
        System.out.println("100-80="+b.func1(100,80));
        System.out.println("100+20+100="+b.func2(100,20));
    }
}
複製程式碼

執行結果:

100-50=150
100-80=180
100+20+100=220

我們發現原來執行正常的相減功能發生了錯誤。原因就是類B無意中重寫了父類的方法,造成原有功能出現錯誤。在實際程式設計中,我們常常會通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個繼承體系的複用性會比較差。特別是執行多型比較頻繁的時候,如果非要重寫父類的方法,通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴,聚合,組合等關係代替。

5.依賴倒轉原則
高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。
類A直接依賴類B,如果要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。此時,類A一般是高層模組,負責複雜的業務邏輯,類B和類C是低層模組,負責基本的原子操作;修改A會給程式帶來風險。
將類A修改未依賴介面I,類B和類C各自實現介面I,類A通過介面I間接與類B或類C發生聯絡,則會大大降低修改類A的記機率。
依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建的架構比以細節為基礎的架構要穩定的多。在java中,抽象指的是介面或抽象類,細節就是具體的實現類,使用介面或抽象類的目的是制定好規範,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。
依賴倒置的中心思想是面向介面程式設計。

程式碼示例如下:

複製程式碼
class Book {
    public string getContent() {
        return "很久很久以前。。。。。";
    }
}
class Mother {
    public void narrate(Book book)
    {
        Console.WriteLine(book.getContent());
    }
}
class Program
{
    static void Main(string[] args)
    {
        Mother monther = new Mother();
        monther.narrate(new Book());
        Console.ReadLine();
    }
}
複製程式碼

執行結果:

如果讀的物件是報紙,雜誌,卻發現客戶端不適用了。
我們引入一個抽象的介面IReader,代表讀物

interface IReader{
    public string getContent();
}

這樣Mother類與介面IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,他們各自都去實現IReader介面,這樣就符合依賴倒置原則了,修改程式碼如下:

複製程式碼
interface IReader {
         string getContent();
    }
    class Newspaper: IReader
    {
    public string getContent()
    {
        return "切爾西豪取12連勝";
    }
}
class Book:IReader
{

    public string getContent()
{
    return "很久很久以前。。。。";
}
}
class Mother
{
    public void narrate(IReader reader)
    {
        Console.WriteLine(reader.getContent());
    }
}
class Program
{
    static void Main(string[] args)
    {
        Mother monther = new Mother();
        monther.narrate(new Book());
        monther.narrate(new Newspaper());
        Console.ReadLine();
    }
}
複製程式碼

執行結果:

採用依賴倒置原則給多人並行開發帶來極大的便利,比如上列中Mother類與Book類直接耦合,Mother必須等Book類編碼完成後才可以進行編碼,因為Mother類依賴於Book類。修改後的程式可以同時開工,互不影響。
依賴關係的傳遞有三種方式,介面傳遞,構造方法傳遞和setter方法傳遞。
介面傳遞:

複製程式碼
interface IDriver{
    public void drive(ICar car);
}
public class Driver:IDriver{
    public void drive(ICar car){
        car.run();
    }
}
複製程式碼

構造方法傳遞:

複製程式碼
interface IDriver{
    public void drive();
}
public class Driver implements IDriver{
    public ICar car;
    public Driver(ICar _car){
        this.car=_car;
    }
    public void drive(){
        this.car.run();
    }
}
複製程式碼

setter方式傳遞:

複製程式碼
interface IDriver{
    public void setCar(ICar car);
    public void drive();
}
public class Driver:IDriver{
    PRIVATE ICar car;
    public void setCar(ICar car){
        this.car=car;
    }
    public void drive(){
        this.car.run();
    }
}
複製程式碼

在實際程式設計中,一般需要做到如下3點:
低層模組儘量都要有抽象類或介面,或者兩者都有。
變數的宣告型別儘量是抽象類或介面。
使用繼承時遵循里氏替換原則

6.介面隔離原則
客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。
類A通過介面I依賴類B,類C通過介面I依賴類D,如果介面I對於類A和類C來說不是最小介面,則類B和類D必須去實現他們不需要的方法。
將臃腫的介面I拆分為獨立的幾個介面,類A和類C分別與他們需要的介面建立依賴關係。也就是採用介面隔離原則。
舉例說明介面隔離原則:

這個圖的意思是:類A依賴介面I中的方法1,方法2,方法3,類B是對類A依賴的實現;類C依賴介面I中的方法1,方法4,方法5,類D是對類C依賴的實現。對於類B和類D來說,雖然存在用不到的方法(紅色標記所示),但由於實現了介面I,所以也必須要實現這些用不到的方法。程式碼如下:

複製程式碼
interface I{
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
}
class A{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method2();
    }
    public void depend3(I i){
        i.method3();
    }
}
class C{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method4();
    }
    public void depend3(I i){
        i.method5();
    }
}
class B:I{
    public void method1(){
        Console.WriteLine("類B實現介面I的方法1");
    }
    public void method2(){
        Console.WriteLine("類B實現介面I的方法2");
    }
    public void method3(){
        Console.WriteLine("類B實現介面I的方法3");
    }
    public void method4(){}
    public void method5(){}
}
class D:I{
    public void method1(){
        Console.WriteLine("類B實現介面I的方法1");
    }
    public void method2(){}
    public void method3(){}
    public void method4(){
        Console.WriteLine("類B實現介面I的方法4");
    }
    public void method5(){
        Console.WriteLine("類B實現介面I的方法5");
    }
}
class Program
{
    static void Main(string[] args)
    {
        A a=new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());
        
        C c=new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
        Console.ReadLine();
    }
}
複製程式碼

可以看到,介面中出現的方法,不管對依賴於它的類有沒有作用,實現類中都必須去實現這些方法。於是我們將原介面I拆分為三個介面:

程式碼如下所示:

複製程式碼
interface I1{
    void method1();
}
interface I2{
    void method2();
    void method3();
}
interface I3{
    void method4();
    void method5();
}
class A{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I2 i){
        i.method2();
    }
    public void depend3(I2 i){
        i.method3();
    }
}
class C{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I3 i){
        i.method4();
    }
    public void depend3(I3 i){
        i.method5();
    }
}
class B:I1,I2{
    public void method1(){
        Console.WriteLine("類B實現介面I1的方法1");
    }
    public void method2(){
        Console.WriteLine("類B實現介面I2的方法2");
    }
    public void method3(){
        Console.WriteLine("類B實現介面I2的方法3");
    }
}
class D:I1,I3{
    public void method1(){
        Console.WriteLine("類B實現介面I的方法1");
    }
    public void method4(){
        Console.WriteLine("類B實現介面I的方法4");
    }
    public void method5(){
        Console.WriteLine("類B實現介面I的方法5");
    }
}
class Program
{
    static void Main(string[] args)
    {
        A a=new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());
        
        C c=new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
        Console.ReadLine();
    }
}
複製程式碼

說到這裡,可能會覺得介面隔離原則和之前的單一職責原則很相似,其實不然。一,單一職責注重職責,而介面隔離原則注重對介面依賴的隔離;二,單一職責是約束類,其次是方法,針對的是程式中的實現和細節;而介面隔離原則約束的是介面,針對的是抽象,程式整體框架的構建。

7.迪米特法則
一個物件應該對其他物件保持最少的瞭解。
類與類關係越密切,耦合度越大。
迪米特法則又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都儘量將邏輯封裝在類的內部。對外除了提供的public 方法,不對外洩露任何資訊。
迪米特法則還有個更簡單的定義:只與直接的朋友通訊。
什麼是直接的朋友:每個物件都會與其他物件由耦合關係,只要兩個物件之間有耦合關係,我們就說這兩個物件之間是朋友關係。耦合的方式很多,依賴,關聯,組合,聚合等。其中,我們稱出現成員變數,方法引數,方法返回值中的類為直接的朋友,而出現在區域性變數中的類不是直接的朋友。也就是說,陌生的類最好不要以區域性變數的形式出現在類的內部。
舉例額說明如下,有一個集團公司,下屬單位有分公司和直屬部門,現要求打印出所有下屬單位的員工ID。

複製程式碼
class Employee{
    private string id;
    public void setId(string id){
        this.id=id;
    }
    public string getId(){
        return id;
    }
}
class SubEmployee{
    private string id;
    public void setId(string id){
        this.id=id;
    }
    public string getId(){
        return id;
    }
}
class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list=new ArrayList(SubEmployee);
        for(int i=0;i<100;i++){
            SubEmployee emp=new SubEmployee();
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
}
class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list=new ArrayList<Employee>();
        for(int i=0;i<30;i++)
        {
            Employee emp=new Employee();
            emp.setId("總公司"+i);
            list.add(emp);
        }
        return list;
    }
    publi void printAllEmployee(SubCompanyManager sub){
        List<SubEmployee> list1=sub.getAllEmployee();
        foreach(SubEmployee e in list1){
            Console.WriteLine(e.getId());
        }
        List<Employee> list2=this.getAllEmployee();
        foreach(Employee e in list2){
            Console.WriteLine(e.getId());
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        CompanyManager e=new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
        Console.ReadLine();
    }
}
複製程式碼

這個設計的問題在於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());
        }
    }
}
複製程式碼

迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關係。

8.開閉原則
一個軟體實體如類,模組和函式應該對擴充套件開放,對修改關閉。用抽象構建框架,用實現擴充套件細節。
當軟體需要變化時,儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來實現變化。
當我們遵循前面介紹的5大原則,以及使用23中設計模式的目的就是遵循開閉原則。