C# 設計模式(0)——設計原則

魏楊楊發表於2021-12-06

1、前言

最近在搭建專案的的時候才會想設計原則問題,之前也看過設計模式,沒有寫部落格很快就忘了也沒有起到什麼作用。現在在專案上遇到了你才會發現它的美。部落格園也有很多前輩寫的很好,對於我來說好記性不如爛筆頭嘛。別人寫的在好你看了之後終究是別人的。只有自己寫下來會用了才是自己的。

2、定義

個人理解設計原則其實就是一個規範一樣,為啥要用設計原則?就是為了寫出適應變化、提高複用率、可維護性、可擴充套件性的程式碼。在進行設計的時候,我們需要遵循單一職責原則、開閉原則、里氏替代原則、依賴倒置原則、介面隔離原則、合成複用原則和迪米特法則。

3、單一職責原則

自己的事情自己幹,一個類只弄它單一職責的模組。比如說Login類就只負責登入相關的業務,User類只負責使用者相關的業務。如果說Login裡面又夾雜著User的功能這樣User牽連著其他的類是不是Login類也牽連進去了,這樣下來耦合度就很好了。單一職責原則優點就是降低耦合,提高程式碼的複用率使得模組看起來有目的性,結構簡單,修改當前模組對於其他模組的影響很低。缺點就是如果過度的單一,過度的細分,就會產生出很多模組,無形之中增加了系統的複雜程度。

比如Login能登入 但是裡面又寫了個User吃飯的方法,登入跟吃飯八杆子打不到一塊去。生物分類學是研究生物分類的方法和原理的生物學分支。分類就是遵循分類學原理和方法,對生物的各種類群進行命名和等級劃分:界門綱目科屬種一樣。程式裡面類也是一樣的。

Login login=new();
login.Sign("登入");

public class Login
{
    /// <summary>
    /// 登入
    /// </summary>
    public void Sign(string name)
    {
        Console.WriteLine($"賬號密碼{name}");
    }
}

現在在弄進去一個Eat(吃飯)login.Sign("吃飯");輸出 賬號密碼吃飯。感覺就不合適了。如果要兼顧兩個職責這時候要把程式做一點改變

第一種:這樣就讓Sign兼顧了兩個職責:登入跟吃飯

 public void Sign2(string name)
    {
        if(name=="登入")
        Console.WriteLine($"賬號密碼{name}");
        else if(name=="吃飯")
            Console.WriteLine($"使用者{name}");
    }

 

第二種:此時Sign跟Eat職責都是單一的,但是我們設計之初就是Login是用來登入的

public class Login
{
    /// <summary>
    /// 登入
    /// </summary>
    public void Sign(string name)
    {
        Console.WriteLine($"賬號密碼{name}");
    }
    /// <summary>
    /// 吃飯
    /// </summary>
    /// <param name="name"></param>
    public void Eat(string name)
    {
        Console.WriteLine($"使用者{name}");
    }
    public void Sign2(string name)
    {
        if(name=="登入")
        Console.WriteLine($"賬號密碼{name}");
        else if(name=="吃飯")
            Console.WriteLine($"使用者{name}");
    }
}

第三種:此時Login 的Sign 跟User的Eat職責都是單一的,只做一件事。

public class Login
{
    /// <summary>
    /// 登入
    /// </summary>
    public void Sign(string name)
    {
        Console.WriteLine($"賬號密碼{name}");
    }
   
}
public class User
{
    /// <summary>
    /// 吃飯
    /// </summary>
    /// <param name="name"></param>
    public void Eat(string name)
    {
        Console.WriteLine($"使用者{name}");
    }
}

4、開閉原則(OCP)

強調的是:一個軟體實體(指的類、函式、模組等)應該對擴充套件開放,對修改關閉。即每次發生變化時,要通過新增新的程式碼來增強現有型別的行為,而不是修改原有的程式碼。修改本省的程式碼破壞現有的程式可能稍微不注意就引起很大的連鎖反應。通過擴充套件來實現就是本可能出現的結果抽象出來。

栗子:狗的叫聲 Dog(狗)、Show(實現)、Voice(聲音)。根據實現傳入動物的聲音

public class Dog
{
    public  void Call(DogVoice dogVoice)
    {
        Console.WriteLine($"狗的叫聲是{dogVoice.Value}");
    }
}
public class DogVoice
{
    public  string Value
    {
        get { return "汪汪汪"; }
    }
}
public class Show
{
    public  void ShowVoice(Dog dog,DogVoice dogVoice)
    {
        dog.Call(dogVoice);
    }
}

呼叫

Dog d = new Dog();
DogVoice dv = new DogVoice();
Show s = new();
s.ShowVoice(d,dv);

 如果有一天我們引入了新的類小貓類Cat跟聲音CatVoice,這個時候就要修改Show 顯示類了,這就違反了開閉原則。如果要符合開閉原則 我們就要對Dog跟Voice類做一個抽象,抽象出Call跟Voice的類或者介面。這裡我們抽象兩個介面ICall 跟IVoice

public interface ICall
{
    void Call(IVoice voice);
}
public interface IVoice
{
    string Value { get; }
}
public class Gog2 : ICall
{
    public void Call(IVoice voice)
    {
        Console.WriteLine($"狗的叫聲是{voice.Value}");
    }
}
public class DogVoice2 : IVoice
{
    public string Value => "汪汪汪";
}
public class Show2
{
    public void ShowVoice(ICall dog, IVoice dogVoice)
    {
        dog.Call(dogVoice);
    }
}

呼叫

ICall d = new Dog2();
IVoice dv = new DogVoice2();
Show2 s = new();
s.ShowVoice(d, dv);

 這樣你要新加錨的叫聲不是就不用修改show了;新新增兩個類就可以了Cat跟CatVoice。

public class Cat : ICall
{
    public void Call(IVoice voice)
    {
        Console.WriteLine($"貓的叫聲是{voice.Value}");
    }
}
public class CatVoice : IVoice
{
    public string Value => "喵喵喵";
}

 呼叫

ICall d = new Dog2();
IVoice dv = new DogVoice2();
Show2 s = new();
s.ShowVoice(d, dv);

ICall c = new Cat();
IVoice v = new CatVoice();
s.ShowVoice(c, v);

5、里氏替換原則(LSP)

一個程式中的子類如果繼承了父類那麼他將滿足父類所有的方法與屬性。也就是說在程式中,把父類都替換成它的子類,程式的行為沒有任何變化。子類可以有自己的特點也可以重寫父類的方法,但是父類有的方法子類一定要實現,這裡舉一個簡單的列子,父類動物(Adimal),子類 母雞(Hen)

/// <summary>
/// 抽象類父類 動物
/// </summary>
public abstract class Animal
{
    /// <summary>
    /// 抽象方法 下蛋量
    /// </summary>
    /// <returns></returns>
    public abstract double LayEgg();
    /// <summary>
    /// 一年平均每天下蛋
    /// </summary>
    /// <returns></returns>
    public abstract double LayEggAvg(Animal s);
}
public class Hen : Animal
{
    public override double LayEgg()
    {
        return 100;
    }

    public override double LayEggAvg(Animal s)
    {
        return 365 / s.LayEgg();
    }
}

這時候如果在加一個類 羊  Sheep  羊也屬於動物類 但是它不會下蛋呀,這個時候計算平均下蛋量的時候程式就要出錯;0作為被除數程式編譯都通過不了。

public class Sheep : Animal
{
    public override double LayEgg()
    {
        return 0;
    }

    public override double LayEggAvg(Animal s)
    {
        return 365 / s.LayEgg();//0
    }
}

 要執行也可以直接判斷是不是綿羊類。是的話返回0,這樣又不符合開閉原則了呀。因為Sheep就完全沒有繼承Animal類,它實現不了LayEggAve的方法。所以現實中綿羊屬於動物類,但是程式中不屬於,不要強行繼承,如果繼承就要完全實現。

public class Sheep2 : Animal
{
    public override double LayEgg()
    {
        return 0;
    }

    public override double LayEggAvg(Animal s)
    {
        //傳入的型別如果是綿羊類 返回0
        if (s.GetType().Equals(typeof(Sheep2)))
        {
            return 0;
        }
        else
        {
            return 365 / s.LayEgg();
        }
    }
}

6、合成複用原則

如果程式一個物件A包含了另一個物件B,那麼A就可以委託B來使用B的功能。 這裡學生表李有班級 我們就可以通過學生直接看到班級的資訊。

public class Student
{
    public Class Class { get; set; }
}
public class Class
{
    public string GetName
    {
        get { return "計科一班"; }
、    }
}

 

呼叫

Student student = new();
var name=student.Class.GetName;

7、介面隔離原則

原則上使用多個介面,應用程式端不依賴不用多餘的介面,這樣也可以保證程式的安全性。比如說系統內部用的介面我們都要實現增刪查改,而對於第三方我們只提供查詢的介面就可以了。

public interface ICardServiceOut
{
    void Query();//
}
/// <summary>
/// 系統內部就繼承兩個介面
/// </summary>
public class OurCard : ICardService, ICardServiceOut
{
    public void Add()
    {
        throw new NotImplementedException();
    }

    public void Query()
    {
        throw new NotImplementedException();
    }

    public void Remove()
    {
        throw new NotImplementedException();
    }

    public void Update()
    {
        throw new NotImplementedException();
    }
}
/// <summary>
/// 第三方就只能繼承外部介面 只能查詢操作
/// </summary>
public class ThirdCard : ICardServiceOut
{
    public void Query()
    {
        throw new NotImplementedException();
    }
}

8、依賴倒置原則(DIP)

抽象不應該依賴細節,而細節應該依賴抽象,高層模組不依賴於低層模組的實現,而低層模組依賴於高層模組定義的介面。一般來講,就是高層模組定義介面,低層模組負責具體的實現。針對介面程式設計而不是針對細節程式設計。詳情檢視Asp.Net Core 3.1學習-依賴注入、服務生命週期(6) 

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

指的是一個物件應當對其他物件有儘可能少的瞭解。也就是說,一個模組或物件應儘量少的與其他實體之間發生相互作用,使得系統功能模組相對獨立,這樣當一個模組修改時,影響的模組就會越少,擴充套件起來更加容易。如果想要滿足迪米特法則,就要儘可能少的寫public方法和變數,不需要讓別的物件知道的方法或者欄位就不要公開,好朋友之間都有自己的祕密一個的意思。

其實用不用設計原則程式都能執行出來,沒有多大影響,但是後面要增加模組或者修改功能的時候就看你的程式能不能經得起折騰咯?

PS:學習向日葵,做一個積極吸收正能量的人。人生多數時候都是自尋煩惱。就是吸收的負能量太多。要學習向日葵,哪裡有陽光就朝向哪裡。多接觸優秀的人,多談論健康向上的話題,多想想有利於人生髮展的問題。心裡若是充滿陽光,人生即便下雨,也會變成春雨。

相關文章