六大設計原則(C#)

爲何要有設計原則,我以爲一張圖片就能夠解釋這一切

1、單一職責原則(SRP)

對於一個類而言,應該只有一個發生變化的緣由。(單一職責不只僅是指類)編程

若是一個模塊須要修改,它確定是有緣由的,除此緣由以外,若是遇到了其餘狀況,還須要對此模塊作出修改的話,那麼就說這個模塊就兼具多個職責。舉個栗子:ide

此時咱們有個動物類Animal,有個Move()會移動的方法函數

public class Animal
{
    //動物移動的方法
    public void Move(String name)
    {
        Console.WriteLine($"動物{name}跑");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal a = new Animal();
        a.Move("");
        Console.ReadKey();
    }
}

此時若是傳入一個魚進去就不太合適了,由於魚是不會跑只會遊的ui

a.Move("");

此時咱們須要兼顧兩個職責,第一個就是普通動物移動的方法,第二個就是魚類的移動方法。咱們修改一下,讓這一切變得合理一些spa

第一種設計

public class Animal
{
    //動物移動的方法
    public void Move(String name)
    {
        if (name == "") {
            Console.WriteLine($"動物{name}跑");
        }
        else if (name==""){
            Console.WriteLine($"動物{name}遊");
        }
        
    }
}

這種的話其實就是讓Move方法兼具普通動物和魚類移動兩個職責(若是你的設計之初就是讓Move知足全部動物的移動,此時Move方法仍是兼具一個職責)code

第二種對象

public class Animal
{
    //普通動物移動的方法
    public void RunMove(String name)
    {
        Console.WriteLine($"動物{name}跑");
    }
    //魚類移動的方法
    public void FishMove(String name)
    {
        Console.WriteLine($"動物{name}遊");
    }
}

此時RunMove和FishMove方法的職責是單一的,只管普通動物和魚類的移動,可是Animal類確是兼具了普通動物和魚類移動兩個職責(若是你的設計之初就是讓Animal類知足全部動物的移動,此時Animal仍是兼具一個職責)blog

第三種繼承

public class RunAnimal
{
    //普通動物移動的方法
    public void Move(String name)
    {
        Console.WriteLine($"動物{name}跑");
    }
}
public class FishAnimal
{
    //魚類移動的方法
    public void Move(String name)
    {
        Console.WriteLine($"動物{name}遊");
    }
}

class Program
{
    static void Main(string[] args)
    {
        RunAnimal a = new RunAnimal();
        a.Move("");
        FishAnimal f = new FishAnimal();
        f.Move("");
        Console.ReadKey();
    }
}

此時的話RunAnimal類、FishAnimal類和Move方法的職責都是單一的,只作一件事。就拿RunAnimal的Move方法來講,只有普通動物的移動須要作出改變了,纔會對Move方法作出修改。

單一職責原則的優勢就是高內聚,使得模塊看起來有目的性,結構簡單,修改當前模塊對於其餘模塊的影響很低。缺點就是若是過分的單一,過分的細分,就會產生出不少模塊,無形之中增長了系統的複雜程度。

2、開閉原則(OCP)

軟件中的對象(類,模塊,函數等等)應該對於擴展時開放的,可是對於修改是封閉的

一個模塊寫好了,可是若是還想要修改功能,不要對模塊自己進行修改,可能會引發很大的連鎖反應,破壞現有的程序,應該經過擴展來進行實現。經過擴展來實現的前提,就是一開始把模塊抽象出來,而抽象出來的東西要可以預測到足夠多的可能,由於一旦肯定後,該抽象就不能在發生改變。舉個栗子:

如今有個Dog類,Food食物類,還有個Feed類 ,根據傳入食物餵養Dog類動物

public class Dog
{
    public void eat(DogFood f)
    {
        Console.WriteLine("狗吃" + f.Value);
    }
}
public class Feed
{
    //開始餵食
    public void StartFeed(List<Dog> d, DogFood f)
    {
        foreach (Dog item in d)
        {
            item.eat(f);
        }
    }
}

public class DogFood
{
    public String Value
    {
        get
        {
            return "狗糧";
        }
    }
}

若是有一天,咱們引入了新的種類Tiger老虎類,和新的食物Meat肉類,此時要修改Feed餵食類了,這就違反了開閉原則,只作擴展不作修改,若是要讓Feed類符合開閉原則,咱們須要對Dog類和Food類作出一個抽象,抽象出Eat和Food抽象類或者接口,這裏我就抽象出兩個接口IEat和IFood:

//全部須要進食的動物都要實現此接口
public interface IEat
{
    //此時食物也應該使用接口而不是具體類來接收
    //不然只能接收單一的食物,增長食物的話仍是須要修改
    void eat(IFood food);
}
//全部食物須要實現此接口
public interface IFood
{
    String Value { get; }
}

此時,IEat和IFood是被固定死了,不作修改,這就須要設計之初可以預測到足夠多的可能,若是須要添加新的功能(動物或食物),只須要實現對應的接口就好了。

修改原有Dog類和DogFood類實現對應的接口:

public class Dog:IEat
{

    public void eat(IFood food)
    {
        Console.WriteLine("狗吃" + food.Value);
    }
}
public class DogFood:IFood
{

    public String Value
    {
        get
        {
            return "狗糧";
        }
    }
}

修改Feed餵食類,使用接口來接收和使用,使其知足開閉原則:

public class Feed
{
    //使用接口接收,後續能夠傳入實現該接口的子類,由於用到了協變,就須要使用IEnumerable來接受
    public void StartFeed(IEnumerable<IEat> d, IFood f)
    {
        foreach (IEat item in d)
        {
            item.eat(f);
        }
    }
}

這樣的話,若是要添加新的功能,就不須要對Feed進行修改,而是添加新的類:

public class Tiger : IEat
{
    public void eat(IFood food)
    {
        Console.WriteLine("老虎吃" + food.Value);
    }
}

public class Meat : IFood
{
    public string Value
    {
        get
        {
            return "";
        }
    }
}

調用:

static void Main(string[] args)
{
    //餵食
    Feed f = new Feed();
    //
    List<Dog> dog = new List<Dog>(){
        new Dog(),
        new Dog()
    };
    //狗的食物
    DogFood df = new DogFood();
    //開始餵食
    f.StartFeed(dog,df);

    //老虎
    List<Tiger> t = new List<Tiger>() {
        new Tiger()
    };
    //
    Meat m = new Meat();
    //開始餵食
    f.StartFeed(t,m);


    Console.ReadKey();
}

 遵循開閉原則的好處是擴展模塊時,無需對已有的模塊進行修改,只須要添加新的模塊就行,避免了程序修改所形成的風險,抽象化就是開閉原則的關鍵。

3、依賴倒置原則(DIP)

依賴倒置原則主程序要依賴於抽象接口,不要依賴於具體實現。高層模塊不該該依賴底層模塊,兩個都應該以來抽象。抽象不該該依賴細節,細節應該依賴抽象。

依賴:依賴其實就是耦合性,若是A類依賴B類,那麼當B類消失或修改時,對A類會有很大的影響,能夠說是A類的存在徹底就是爲B類服務,就是說A類依賴B類。

高層模塊不該該依賴底層模塊,兩個都應該依賴抽象:在上一個例子中,做爲高層模塊Feed餵食類依賴底層模塊Dog類和DogFood類(高層和底層就是調用時的關係,由於Feed調用Dog,因此Feed是高層模塊),這樣的話Feed餵食類只能給Dog類吃DogFood,若是引進了其餘動物,Feed類此時是沒法完成餵食工做的。後來對Feed類、Dog類和DogFood類作出了修改,讓Dog類和DogFood類分別依賴IEat和IFood接口,使Feed類依賴於IEat和IFood接口,這樣的話就使得高層模塊(Feed)和底層模塊(Dog、DogFood)都是依賴於接口。

抽象不該該依賴細節,細節應該依賴抽象:抽象就是抽象類或者接口,細節就是實現抽象類或接口的具體類,這句話的意思其實就是,抽象類或者接口不該該依賴具體的實現類,而應該讓具體的實現類去依賴抽象類或者接口。

遵循依賴倒置原則的好處就是下降了模塊和模塊之間的耦合性,下降了修改模塊後對程序所形成的風險。

4、里氏替換原則(LSP)

一個程序中若是使用的是一個父類,那麼該程序必定適用於其子類,並且程序察覺不出父類和子類對象的區別。也就是說在程序中,把父類都替換成它的子類,程序的行爲沒有任何變化。

其實里氏替換原則是很容易理解的,若是想知足里氏替換原則,子類繼承父類時,能夠有本身的特色,能夠重寫父類的方法,可是父類中有的方法,子類必須是徹底能夠實現的。開閉原則中的例子就是符合里氏替換原則,而關於里氏替換原則的反例也有不少,例如:正方形不是長方形、玩具槍不能殺人、鴕鳥不會飛,這裏就拿企鵝不會飛來舉個反例:

Birds鳥類、Sparrow麻雀類,全部的鳥類都具備一個飛行時間

public abstract class Birds
{
    //全部鳥類都應該具備飛行速度
    public abstract double FlySpeed();
}

//麻雀類
public class Sparrow : Birds
{
    public override double FlySpeed()
    {
        return 10.5;
    }
}

此時咱們添加一個Penguin企鵝類,由於企鵝也是鳥,因此也應該繼承自Birds鳥類

//企鵝
public class Penguin: Birds
{
    //實現飛的方法
    public override double FlySpeed()
    {
        return 0;
    }
}

可是因爲Penguin並不會飛,因此飛行速度爲0,可是也實現了FlySpeed方法,編譯也沒有報錯啊,可是若是此時有一個Fly方法須要根據鳥類的飛行速度來計算飛行300米所須要的時間

public static double Fly(Birds b)
{
    return 300 / b.FlySpeed();
}

那麼,將Penguin企鵝類放入時,則會報出異常,由於300/0是不符合邏輯的,也就不知足里氏替換原則,由於此時做爲父類Birds,若是傳入子類Penguin,程序就會出錯。

不知足里氏替換原則的根本仍是Penguin企鵝類並無徹底繼承Birds鳥類,由於實現不了FlySpeed方法,因此此時解決方案有兩種,

一種就是在Fly方法中進行判斷:

public static double Fly(Birds b)
{
    //若是傳入的類型爲鴕鳥,默認返回0
    if (b.GetType().Equals(typeof(Penguin)))
    {
        return 0;
    }
    else {
        return 300 / b.FlySpeed();
    }
    
}

這樣的話就會違反開閉原則,並且更改代碼會很是麻煩,後續添加功能還需修改。

第二種就是Penguin企鵝類不繼承Birds鳥類,由於此時企鵝類繼承鳥類在此案例中就是強行繼承,雖然現實世界中企鵝也是鳥,可是在編程世界中就行不通了。

總結下來實現里氏替換原則的根本就是,不要強行繼承,若是繼承就要徹底實現。

5、接口隔離原則(ISP)

客戶端不該該依賴它不須要的接口;一個類對另外一個類的依賴應該創建在最小的接口上

知足接口隔離原則的前提就是,接口不要設計的太過龐大,什麼叫龐大呢?好比一個動物接口就很是龐大,由於若是細分的話,就能夠分不少種類的動物,此時動物接口就須要考慮率足夠多的狀況來保證動物接口後續不被修改,那麼一開始設計時,就能夠將動物接口根據具體的需求(例如動物是否會飛和游泳)細分爲水裏遊的動物、天上飛的動物和地上跑的動物,若是仍是過於龐大,就再細分。

就好比開閉原則中的IEat接口,就知足了接口隔離原則,該接口只負責吃的接口,全部須要吃的動物均可以實現該接口,而Feed餵食類依賴IEat接口,此時IEat接口也是最小接口。舉個反例:

此時Feed餵食類不在依賴於IEat接口,而是依賴於IAnimal接口,全部動物(Dog、Tiger)都實現IAnimal接口

public interface IAnimal
{
    //全部動物都會吃
    void eat(IFood food);
    //全部動物都會呼吸
    void breathe();
    //全部動物都會移動
    void move();
    //........動物的功能確定不止這麼多
}


public class Tiger : IAnimal
{
    public void breathe()
    {
        Console.WriteLine("老虎會呼吸");
    }

    public void eat(IFood food)
    {
        Console.WriteLine("老虎吃" + food.Value);
    }

    public void move()
    {
        Console.WriteLine("老虎會跑");
    }
}

public class Dog : IAnimal
{
    public void breathe()
    {
        Console.WriteLine("狗會呼吸");
    }

    public void eat(IFood food)
    {
        Console.WriteLine("狗吃" + food.Value);
    }

    public void move()
    {
        Console.WriteLine("狗會跑");
    }
}
View Code

那麼此時讓Feed餵食類依賴IAnimal

public class Feed
{
    //使用接口接收,後續能夠傳入實現該接口的子類
    public void StartFeed(IEnumerable<IAnimal> d, IFood f)
    {
        foreach (IAnimal item in d)
        {
            item.eat(f);
        }
    }
}

這樣的話就違反了接口隔離原則,由於Feed餵食類只須要調用對象的eat方法,動物的其餘的方法都是不調用的,可是卻依賴了IAnimal接口,這樣的話就顯得很臃腫,並且若是之後不傳入動物,該工廠也負責餵養機器人吃電池,是否是依賴IAnimal就不合適了(若是非要讓機器人實現IAnimal接口是可行的,不過這太不合理了)。

可是Feed若是依賴IEat接口,那麼只要能吃東西就能夠實現IEat接口,只要能吃東西就能夠傳入Feed餵食類餵養,此時Feed類依賴的IEat接口爲最小接口。當一個類對另外一個類的依賴創建在最小接口上時,該類基本上負責調用此接口中的全部內容,不須要接口中有多餘的方法。

6、迪米特法則(LoD)(最少知識原則(LKP))

一個對象應當對其餘對象有儘量少的瞭解,不要和陌生人說話。

首先來講一下什麼叫「陌生人」,首先咱們有個類A,A自己確定是A的朋友,A的屬性一樣也是A的朋友,若是A的方法須要參數是B類型的,那麼B也是A的朋友,還有A類中直接創造的對象也是A類的朋友,其餘類對於A類來講就是「陌生人」。若是想要知足迪米特法則,就要儘量少的寫public方法和變量,不須要讓別的對象知道的方法或者字段就不要公開。

其實迪米特法則的目的也是爲了減小模塊間的依賴,下降模塊間的耦合性,這樣才能提升代碼的複用率。舉個栗子:

動物園中有不少動物,而管理員須要天天記錄動物的數量

動物和動物園

//動物
public class Animal
{

}

//動物園 
public class Zoo
{
    public List<Animal> animals = new List<Animal>();
}

管理員

//管理員
public class ZooMan
{
    //根據動物園檢查動物的數量
    public void CheckCount(Zoo z)
    {
        //獲取全部的動物
        List<Animal> animals = z.animals;
        //獲取全部動物的數量
        int count = animals.Count;
        //輸出
        Console.WriteLine(count);
    }
}

ZooMan管理員與Animal動物類並無直接的朋友關係,可是卻發生了依賴關係,這樣的設計顯然違反了迪米特法則。咱們應該對此程序進行修改,讓動物園不對外開放animals屬性,將計算動物全部數量的方法交由Zoo動物園來完成,對外提供GetAnimalCount方法獲取數量,使得ZooMan管理員與Animal不產生依賴關係,修改以下:

//動物園 
public class Zoo
{
    //私有全部的動物,只有當前動物園能夠訪問
    private List<Animal> animals = new List<Animal>();
    //可是對外提供獲取全部動物數量的方法
    public int GetAnimalCount()
    {
        return animals.Count;
    }
}

管理員獲取數量只須要 int count = z.GetAnimalCount();就能夠作到了。

迪米特法則的雖然能夠直接避免ZooMan管理員與Animal動物類產生依賴,可是卻須要Zoo動物園對外提供一個GetAnimalCount方法,若是盲目的追求迪米特法則時,就會產生很對相似於GetAnimalCount這樣的「中間」方法或模塊,來傳遞間接的調用,有可能形成模塊間通信效率下降,不容易協調。

其實這麼多原則,遵循與不遵循對於實現特定的功能沒有絲毫影響,但程序不多是一成不變的,重要的是後續須要修改或者添加新的模塊時,你的程序可否作到「擁抱變化」 ?

 

 若是有錯誤或者疑問歡迎留言

相關文章
相關標籤/搜索