設計模式六大原則


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中設計模式的目的就是遵循開閉原則。


免責聲明!

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



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