面向對象設計原則


一. 單一職責原則

核心思想:一個類應該只有一個引起它變化的原因。

假設存在這樣的設計。Rectangle類具有兩個方法,一個方法是計算矩形的面積,另一個方法是把矩形繪制在屏幕上。

CaculateArea方法只會進行簡單的數學運算,而Draw方法則調用GUI組件實現繪制矩形的功能。顯然,這個類就包含了兩個不同的職責了。那這樣又會帶來什么問題呢?考慮這樣一個場景:現在有一個幾何學應用程序調用了這一個類,已便實現計算面積的功能,在這個程序中不需要用到繪制矩形的功能。問題一:部署幾何應用程序需要把GUI組件一同部署,而且這個組件根本沒有使用到。問題二:對Rectangle類的改變,比如Draw方法改用另外一套GUI組件,必須對幾何應用程序進行一次重新部署。

可見,一個類如果承擔的職責過多,就等於把職責耦合在一起了,容易導致脆弱的設計,帶來額外的麻煩。在實際開發中,業務規則的處理和數據持久化一般是不同時存在同一個類中的,業務規則往往會頻繁地變化,而持久化的方式卻不會經常性地變化。如果這兩個職責混合在同一個類中,業務規則頻繁變化導致類的修改,只調用持久化方法的類也必須跟着重新編譯,部署的次數常常會超過我們希望的次數。對業務規則和持久化任務的職責分離就是遵循單一職責原則的體現。

對上述Recangle類可進行這樣的修改:

二 Open Closed Principle——開放封閉原則

核心思想:對擴展開放,對修改封閉。

“需求總是變化的“擁抱變化似乎就是軟件開發的真理之一。經常會有這樣令人沮喪的情景出現:新的需求來了,對不起,我的代碼設計必須大幅度推倒重來。設計的壞味道讓我們深受其害,那么怎樣的設計才能面對需求的改變卻可以保持相對穩定呢?

針對這樣的問題,OCP給了我們如下的建議:在發生變化的時候,不要修改類的源代碼,要通過添加新代碼來增強現有類的行為。

對擴展開放,對修改封閉,這兩個特征似乎就是相互矛盾的。通常觀念來講,擴展不就是修改源代碼嗎?怎么可能在不改動源代碼的情況下去更改它的行為呢?

答案就是抽象(Interface 和 抽象基類)。實現OCP的核心思想就是對抽象編程。讓類依賴於固定的抽象,對修改就是封閉的;而通過面向對象的繼承和多態機制,通過覆寫方法改變固有行為,實現新的擴展方法,對於擴展就是開放的。

來看一個例子。實現一個能夠根據客戶端的調用要求繪制圓形和長方形的應用程序。初始設計如下:

View Code
public class Draw
{
    public void DrawRectangle()
    {
        //繪制長方形
    }

    public void DrawCircle()
    {
        //繪制圓形
    }
}

public enum Sharp
{
    /// <summary>
    /// 長方形
    /// </summary>
    Rectangle ,

    /// <summary>
    /// 圓形
    /// </summary>
    Circle ,
}

public class DrawProcess
{
    private Draw _draw = new Draw();

    public void Draw(Sharp sharp)
    {
        switch (sharp)
        {
            case Sharp.Rectangle:
                _draw.DrawRectangle();
                break;
            case Sharp.Circle:
                _draw.DrawCircle();
                break;
            default:
                throw new Exception("調用出錯!");
        }
    }
}

//調用代碼
DrawProcess draw = new DrawProcess();
draw.Draw(Sharp.Circle);

 現在的代碼可以正確地運行。一切似乎都趨近於理想。然而,需求的變更總是讓人防不勝防。現在程序要求要實現可以繪制正方形,在原本的代碼設計下,必須做如下的改動:

View Code
//在Draw類中添加
public void DrawSquare()
{
     //繪制正方形
}

//在枚舉Sharp中添加
 /// <summary>
 /// 正方形
 /// </summary>
 Square ,

//在DrawProcess類的switch判斷中添加
case Sharp.Square:
     _draw.DrawSquare();
     break;

 需求的改動產生了一系列相關模塊的改動,設計的壞味道悠然而生。現在運用OCP,來看一下如何對代碼進行一次重構。

/*第一版代碼*/

/// <summary>
/// 繪制接口
/// </summary>
public interface IDraw
{
    void Draw();
}

public class Circle:IDraw
{
    public void Draw()
    {
        //繪制圓形
    }
}

public class Rectangle:IDraw
{
    public void Draw()
    {
        //繪制長方形
    }
}

public class DrawProcess
{
    private IDraw _draw;

    public IDraw Draw { set { _draw = value; } }
    
    private DrawProcess() { }

    public DrawProcess(IDraw draw)
    {
        _draw = draw;
    }

    public void DrawSharp()
    {
        _draw.Draw();
    }
} 


//調用代碼
IDraw circle = new Circle();
DrawProcess draw = new DrawProcess(circle);
draw.DrawSharp();

/*第二版代碼(謝謝來自5樓的提醒)*/

/// <summary>
/// 繪制接口
/// </summary>
public interface IDraw
{
    void Draw();
}

public class Circle:IDraw
{
    public void Draw()
    {
        //繪制圓形
    }
}

public class Rectangle:IDraw
{
    public void Draw()
    {
        //繪制長方形
    }
}



//調用代碼
IDraw circle = new Circle();
circle.Draw();

//這里刪除了DrawProcess類,對於程序要實現的功能來說,DrawProcess是一種過度設計,所以第二版代碼刪除了這個類
View Code

假如現在需要有繪制正方形的功能,則只需添加一個類Square 即可。

View Code
public class Square:IDraw
{
    public void Draw()
    {
        //繪制正方形
    }
}

只需新增加一個類且對其他的任何模塊完全沒有影響,OCP出色地完成了任務。

如果一開始就采用第二種代碼設計,在需求的暴雨來臨時,你會欣喜地發現你已經到家了,躲過了被淋一身濕的悲劇。所以在一開始設計的時候,就要時刻地思考,根據對應用領域的理解來判斷最有可能變化的種類,然后構造抽象來隔離那些變化。經驗在這個時候會顯得非常寶貴,可能會幫上你的大忙。

OCP很美好,然而絕對的對修改關閉是不可能的,都會有無法對之封閉的變化。同時必須清楚認識到遵循OCP的代價也是昂貴的,創建適當的抽象是要花費開發時間和精力的。如果濫用抽象的話,無疑引入了更大的復雜性,增加維護難度。

三 Liskov Subsitution Principle——里氏替換原則

核心思想:子類必須能夠替換掉它們的父類型。

考慮如下情況:

View Code
public class ProgrammerToy
{
    private int _state;

    public  int State
    {
        get { return _state; }
    }

    public virtual void SetState(int state)
    {
        _state = state;
    }
}

public class CustomProgrammerToy:ProgrammerToy
{
    public override void SetState(int state)
    {
        //派生類缺乏完整訪問能力,即無法訪問父類的私有成員_state
        //因此該類型也許不能完成其父類型能夠滿足的契約
    }
} 




//控制台應用程序代碼
class Program
{
    static void Main(string[] args)
    {
        ProgrammerToy toy = new CustomProgrammerToy();
        toy.SetState(5);
        Console.Write(toy.State.ToString());
    }
}

從語法的角度來看, 代碼沒有任何問題. 不過從行為的角度來看 , 二者卻存在不同. 在使用CustomProgrammerToy替換父類的時候, 輸出的是0而不是5, 與既定的目標相差千里. 所以不是所有的子類都能安全地替換其父類使用. 

前面談到的開發封閉原則和里氏替換原則存在着密切的關系. 實現OCP的核心是對抽象編程, 由於子類型的可替換性才使得使用父類類型的模塊在無需修改的情況下就可以擴展, 所以違反了里氏替換原則也必定違反了開放封閉原則.

慶幸的是, 里氏替換原則還是有規律可循的.父類盡可能使用接口或抽象類來實現,同時必須從客戶的角度理解,按照客戶程序的預期來保證子類和父類在行為上的相容.

四 InterFace Segregation Principle——接口隔離原則

核心思想:使用多個小的專門的接口,而不要使用一個大的總接口.

直接來看一個例子: 假設有一個使用電腦的接口

            

程序員類實現接口IComputerUse, 玩游戲,編程,看電影, 多好的事情.

現在有一個游戲發燒友,他也要使用電腦, 為了重用代碼 , 實現OCP, 他也實現接口IComputerUse

 

看出什么問題了嗎? GamePlayer PlayGame無可厚非,WatchMovies小消遣, 但要編程干什么?

這就是胖接口帶來的弊端,會導致實現的類必須完全實現接口的所有方法, 而有些方法對客戶來說是無任何用處的,在設計上這是一種"浪費". 同時,如果對胖接口進行修改, 比如程序員要使用電腦配置為服務器, 在IComputerUse上添加Server方法, 同樣GamePlayer也要修改(這種修改對GamePlayer是毫無作用的),是不是就引入了額外的麻煩?

所以應該避免出現胖接口,要使接口實現高內聚(高內聚是指一個模塊中各個部分都是為完成一項具體功能而協同工作,緊密聯系,不可分割). 當出現了胖接口,就要考慮重構.優先推薦的方法是使用多重繼承分離,即實現小接口.

將IComputerUse拆分為IComputerBeFun和IComputerProgram, Progammer類則同時實現IComputerBeFun和IComputerProgram接口,現在就各取所需了.

與OCP類似, 接口也並非拆分地越小越好, 因為太多的接口會影響程序的可讀性和維護性,帶來難以琢磨的麻煩. 所以設計接口的時刻要着重考慮高內聚性, 如果接口中的方法都歸屬於同一個邏輯划分而協同工作,那么這個接口就不應該再拆分.

五 Dependency Inversion Principle——依賴倒置原則

核心思想: 高層模塊不應該依賴底層模塊,兩者都應該依賴抽象。抽象不應該依賴細節,細節應該依賴抽象。

當一個類A存在指向另一個具體類B的引用的時候,類A就依賴於類B了。如:

View Code
/// <summary>
/// 商品類
/// </summary>
public class Product
{
    public int Id { get; set; }
}

/// <summary>
/// 商品持久化類
/// </summary>
public class ProductRepository
{
    public IList<Product> FindAll()
    {
        //假設從SQL Server數據庫中獲取數據
        return null;
    }
}

/// <summary>
/// 商品服務類
/// </summary>
public class ProductService
{
    private ProductRepository _productRepository;

    public IList<Product> GetProducts()
    {
        _productRepository = new ProductRepository();

        return _productRepository.FindAll();
    }
}

 (在前面單一職責原則中有提到,業務邏輯處理和對象持久化分屬兩個職責,所以應該拆分為兩個類。)高層模塊ProductService類中引用了底層模塊具體類ProductRepository,所以ProductService類就直接依賴於ProductRepository了。那么這樣的依賴會帶來什么問題呢?

"需求總是那么不期而至"。原本ProductRepository是從SQL Server數據庫中讀存數據,現在要求從MySQL數據庫中讀存數據。由於高層模塊依賴於底層模塊,現在底層模塊ProductRepository必須被新的ProductRepostiory代替更改(直接在ProductRepository中把SQL Server代碼改為MySql代碼可不是好主意,所以必須新建一個新的ProductRepository類),高層模塊ProductService也需要跟着一起修改,回顧之前談到的設計原則,這是不是就違反了OCP呢?OCP的核心思想是對抽象編程,DIP的思想是依賴於抽象,這也讓我們更清楚地認識到,面向對象設計的時候,要綜合所有的設計原則考慮。DIP給出了解決方案:在依賴之間定義一個接口,使得高層模塊調用接口,而底層模塊實現接口,以此來控制耦合關系。所以可以對代碼做如下的重構:

View Code
/// <summary>
/// 商品持久化接口
/// </summary>
public interface IProductRepository
{
    List<Product> FindAll();
}

/// <summary>
/// 商品持久化類
/// </summary>
public class ProductRepository:IProductRepository
{
    public IList<Product> FindAll()
    {
        //假設從SQL Server數據庫中獲取數據
        return null;
    }
}

/// <summary>
/// 商品服務類
/// </summary>
public class ProductService
{
    private IProductRepository _productRepository;

    private ProductService() { }

    //使用構造函數依賴注入
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public IList<Product> GetProducts()
    {
        return _productRepository.FindAll();
    }
}

現在已對變化進行了抽象隔離,再根據OCP,我相信實現從MySQL數據庫中讀存數據的需求已經可以被輕松地解決掉了。

參考書籍:<敏捷軟件開發(C#版)> <你必須知道的.NET>   <大話設計模式>   <Microsoft.NET 企業級應用架構設計>


免責聲明!

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



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