譯:面向對象設計原則(Object Oriented Design Principles)

面向對象設計原則
Object Oriented Design Principles

原文:http://www.codeproject.com/Articles/567768/Object-Oriented-Design-Principles java

做者:Marla Sukesh 程序員

 ——文記:本文翻譯純屬興趣,若有雷同,不勝榮幸 數據庫


誰適合對這篇文章? 編程

這篇文章適合對面向對象編程有個基本的概念的程序員。至少能區分類和對象,以及能談論一些面向對象編程的基本主題,如:封裝,抽象,多態和繼承。  設計模式

簡介 架構

在面向對象的世界裏咱們只看對象,對象相互之間交互。類,對象,繼承,多態,抽象等術語是咱們平常工做中耳熟能詳的詞彙。 編程語言

在當今軟件開發行業每個軟件開發者都使用某種類別的面向對象編程語言,可是問題是,是否每個人都真正理解了面向對象編程的的含義?是否知道本身正在使用面向對象編程?若是答案是YES,那麼你真正發揮了面向對象編程的做用嗎? ide

這篇文章咱們除了討論基本的面向對象編程,還將討論面向對象設計。 函數

面向對象設計 測試

在軟件系統中一系列計劃經過對象之間的相互交互來解決特定的問題。俗話說:適當的使用面向對象設計會使得開發者很輕鬆,然而糟糕的設計會帶來災難性的損失。

從何開始

當開發者開始建立一個軟件架構時,他們的目標每每是很好的。他們用已有的經驗創造出優雅而乾淨的設計。 

隨着時間的推移,軟件開始變得糟糕。當每個新特性要求被注入或改變軟件設計來改變其形態,最終一些簡單的變動卻須要更多的努力,更糟糕的是爲Bug創造了更多的機會。

這是誰的錯?

軟件解決現實生活中的業務問題,因爲業務一直在發展,因此軟件也一直在改變。

變動是軟件行業必不可少的一部分。顯然客戶花了錢是爲了能知足他們所指望的需求。因此咱們不能因爲變動而抱怨退化的軟件設計。而是因爲咱們的設計還不夠強大。

對破壞軟件設計最大的因素是在系統中引入了未考慮到的依賴關係。每個系統模塊依賴其餘的模塊,當修改一個模塊時會影響到其它模塊。若是咱們能很好的管理這些依賴關係,那維護這個軟件系統將變得很容易,而且能保證軟件的質量。

例如: 

解決方案:原則,設計模式,軟件架構

  • 軟件架構 好比MVC模式,三層架構,MVP,這些告訴咱們如何結構化整個項目
  • 設計模式 讓咱們能夠重用之前的經驗,甚至更多,它提供可重用的方案解決常見的問題。好比:對象的建立,實例管理,等等。
  • 原則 他告訴咱們,要作什麼你就要現實什麼。至於你怎麼作取決於你本身。每一個人都有本身的一些原則,好比某些人說:我從不說謊,我從不喝酒,等等。遵循這些原則會使他們的生活更輕鬆,可是如何去堅持這些原則則取決於你本身。

同時,面向對象設計也須要聽從一些原則,它使咱們經過軟件設計來管理問題。

Mr Robert Martin (衆所周知的Bob大叔)將這些原則歸類以下:

  1. 類設計原則—也叫SOLID
  2. 包類聚原則
  3. 包耦合原則

本文將經過一些實用的例子來介紹類設計原則:SOLID。

SOLID

J5原則是Bob大叔總結出的五個原則的簡稱,單一職責原則,開閉原則,里氏替換原則,接口隔離原則和依賴倒置原則。維基百科上面說當將這五個應用到一塊兒使得程序員能創造出一個可維護,可擴展的系統。下面詳細介紹每個原則。

I) S - SRP - Single responsibility Principle(單一職責原則) 

現實生活

假設我是一個印度軟件公司裏面的小組leader,在業餘時間我從事寫做,新聞編輯,還作着其餘不一樣的項目。簡單的說,我身兼多職。

當某些很差的事情發生在個人工做場所裏時,好比老闆由於某些錯誤責罵我,或者我被其餘工做干擾。基本上,當一件事情變得糟糕時,全部事情都陷入困境。

發現問題

在咱們討論這個原則以前先看看下面的代碼 


  • 每次變動插入邏輯時都要修改此類
  • 每次變動報告格式時也要修改此類





問題出在哪裏?

每次當一個發生改變時其餘的也會跟隨着改變,緣由是他們都住在同一個屋子裏,擁有相同的父母。咱們沒法控制一切。因此一個改變致使雙倍的測試,甚至更多。

什麼是單一職責原則?(SRP)

SRP:每個軟件模塊只有一個能引發其改變的緣由

  • 軟件模塊:類,方法,等等。
  • 變化的緣由:職責

不違反SRP原則的方法

如今看咱們怎樣去實現他。咱們將建立3個不一樣的類

  1. Employee — 包含了數據屬性
  2. EmployeeDB — 數據庫操做
  3. EmployeeReport — 報表相關的操做 
public class Employee
{
    public string EmployeeName { get; set; }
    public int EmployeeNo { get; set; }
}
public class EmployeeDB
{
    public void Insert(Employee e) 
    {
        //Database Logic written here
    }
 public Employee Select() 
    {
        //Database Logic written here
    }
}
public class EmployeeReport
{
    public void GenerateReport(Employee e)
    {
        //Set report formatting
    }
}

NOTE:單一職責原則一樣適用於方法,每個方法應該只幹一件事。

類能夠有多個方法嗎?

答案是確定的。可是你可能會問:

  1. 一個類只有一個職責
  2. 一個方法只有一個職責
  3. 一個類有多個方法

固然這個問題的答案很是簡單。那就是從總體上下文看待問題。這裏,職責是跟咱們所說的上下文是相關的。說到類的職責時每每是從更高層次上看待的。對於實例,好比EmployeeDB類主要的職責是對employee數據庫相關的操做,然而EmployeeReport則是對員工報表相關的操做。

當咱們說到方法的職責時那就是較低層次上。看下面的例子: 

//Method with multiple responsibilities – violating SRP
public void Insert(Employee e)
{     
    string StrConnectionString = "";       
    SqlConnection objCon = new SqlConnection(StrConnectionString); 
    SqlParameter[] SomeParameters=null;//Create Parameter array from values
    SqlCommand objCommand = new SqlCommand("InertQuery", objCon);
    objCommand.Parameters.AddRange(SomeParameters);
    ObjCommand.ExecuteNonQuery();
}

//Method with single responsibility – follow SRP
public void Insert(Employee e)
{            
    SqlConnection objCon = GetConnection();
    SqlParameter[] SomeParameters=GetParameters();
    SqlCommand ObjCommand = GetCommand(objCon,"InertQuery",SomeParameters);
    ObjCommand.ExecuteNonQuery();
}

private SqlCommand GetCommand(SqlConnection objCon, string InsertQuery, SqlParameter[] SomeParameters)
{
    SqlCommand objCommand = new SqlCommand(InsertQuery, objCon);
    objCommand.Parameters.AddRange(SomeParameters);
    return objCommand;
}

private SqlParameter[] GetParaeters()
{
    //Create Paramter array from values
}

private SqlConnection GetConnection()
{
    string StrConnectionString = "";
    return new SqlConnection(StrConnectionString);
}

測試自己是有利的,使得代碼可讀性強是另外一個好處。代碼可讀性越強就越簡單易懂。


II) O - OCP – Open Close Principle(開閉原則)

現實生活 

假設你想要在圖中第一層和第二層之間在加一層樓。你以爲這個可能嗎?固然可能,可是可行嗎?

這裏有一些方法:

  • 第一種方法是事先在蓋這棟房子時將他蓋成3層。第二層空着,而後在你想要的時候利用第二層。我不知道這是否可行,但的確是一個方法。
  • 第二種方法是你把如今存在的第二層拆掉,而後再蓋兩層新的。但這個方法貌似很不明智。

發現問題

咱們假設EmployeeDB的Select方法會被兩個不一樣的場景調用。一個是普通的員工調用,一個是管理員調用,可是管理員調用的Select方法須要修改。 

若是咱們修改現有Select方法來知足新的需求,原來老的代碼邏輯將會受到影響。同時還要改變現有的測試方案,這可能會致使意想不到的Bug。

什麼是OCP?

OCP:軟件模塊應該對修改關閉,對擴展開放。一個正交的聲明。

不違反OCP的解決方案

1)使用繼承

建立一個EmployeeManagerDB繼承EmployeeDB類,而且根據新的需求重寫select方法 

public class EmployeeDB
{      
    public virtual Employee Select()
    {
        //Old Select Method
    }
}
public class EmployeeManagerDB : EmployeeDB
{
    public override Employee Select()
    {
        //Select method as per Manager
        //UI requirement
    }
}

NOTE:若是這是預期的設計而且提供了可擴展的虛方法,那麼能夠稱得上是良好的面向對象設計。下面是調用代碼:

//Normal Screen EmployeeDB objEmpDb = new EmployeeDB(); Employee objEmp = objEmpDb.Select(); //Manager Screen EmployeeDB objEmpDb = new EmployeeManagerDB(); Employee objEmp = objEmpDb.Select();

2)擴展方法

若是你使用.NET 3.5或者更高的版本,他提供了另一種解決方案叫作擴展方法,它可讓咱們在現有類型中添加新的方法而不用幹涉它。

NOTE:固然有更多的方法來實現咱們想要結果。就像咱們說這些原則不是戒律同樣。

III) L – LSP – Liskov substitution principle(里氏替換原則)

什麼是LSP?

你可能會疑問爲何咱們先定義好例子和要討論的問題。簡而言之,我以爲這樣會更好。

LSP: 在任什麼時候候父類能夠被派生類替換。不要以爲奇怪?若是咱們老是能夠這樣編碼:

BaseClass b = new DerivedClass(),怎麼還會有這條原則?

現實生活 



父親是房地產商人,而他的兒子卻想成爲一名板球運動員。

兒子沒法取代父親的位置,儘管他們是兩爺兒。



發現問題

下面咱們講個很是通用的例子

一般會討論幾何形狀的問題,就是正方形繼承矩形的問題。看下面代碼片斷: 

public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }
}

public class Square:Rectangle
{
    //codes specific to
    //square will be added
}

用法以下:

Rectangle o = new Rectangle();
o.Width = 5;
o.Height = 6;

完美,可是遵照LSP原則咱們將用正方形替換矩形 ,咱們來試試:

Rectangle o = new Square();
o.Width = 5;
o.Height = 6;

出啥問題了?正方形的邊長不能不相等

這意味着什麼?這意味着咱們不能用子類替換父類,這就違反了LSP原則

那麼咱們爲何不在矩形中虛擬長和寬,而後在正方形中重寫它們?

代碼以下: 

public class Square : Rectangle 
{
    public override int Width
    {
        get{return base.Width;}
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }
    public override int Height
    {
        get{return base.Height;}
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }        
}

這樣作不行,仍是違反了LSP原則,因爲在子類改變了寬和高的屬性(對於矩形來講寬和高不能相等,相等就不是矩形了)。(看來這個方法行不通)

解決方法

這裏應該抽象一個名爲Shape的基類,Shape代碼以下: 

public abstract class Shape
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
}

如今這裏應該有兩個具體的且相互獨立的子類。一個是正方形,一個是矩形。他們都將繼承自Shape類。

如今程序員可使用以下代碼: 

Shape o = new Rectangle();
o.Width = 5;
o.Height = 6;

Shape o = new Square();
o.Width = 5; //both height and width become 5
o.Height = 6; //both height and width become 6

即便咱們在派生類中不改變寬和高的行爲,當咱們討論Shape對象時,寬和高沒有具體的限制,它們能夠相等也能夠不相等。

IV) I – ISP– Interface Segregation principle

(接口隔離原則)

現實生活

假設你購買了一臺新電腦。你會看到一系列的USB接口,串行接口,VGA接口等等。若是你打開機箱,你會看到不少插槽在主板上用於鏈接各個部件,主要由硬件工程師裝配的時候使用。

這些插槽都是看不見的除非你打開機箱。簡單說,它只暴露出你須要的接口。想象這麼一種狀況,全部接口都在外部或內部,這樣會使得硬件故障概率變大。 

假設咱們要去商店買東西(好比要買個一板球棒)。想象一下商店老闆開始向你介紹球和球樁。咱們可能會迷惑,最終可能買了咱們並不須要的東西。甚至可能忘了來這裏的最初目的。

發現問題

假設咱們想要開發一個報告管理系統。如今,第一首要任務是建立一個業務層,它將被用在三個不一樣角色的UI層級上。

  1. EmployeeUI — 顯示當前登陸employee的相關報告
  2. ManagerUI — 顯示manager本身以及他管理的小組報告
  3. AdminUI — 顯示相關employee,小組,公司相關的報告。
public interface IReportBAL
{
    void GeneratePFReport();
    void GenerateESICReport();

    void GenerateResourcePerformanceReport();
    void GenerateProjectSchedule();

    void GenerateProfitReport();
}
public class ReportBAL : IReportBAL
{    
    public void GeneratePFReport()
    {/*...............*/}

    public void GenerateESICReport()
    {/*...............*/}

    public void GenerateResourcePerformanceReport()
    {/*...............*/}

    public void GenerateProjectSchedule()
    {/*...............*/}

    public void GenerateProfitReport()
    {/*...............*/}
}
public class EmployeeUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
    }
}
public class ManagerUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport ();
        objBal.GenerateProjectSchedule ();
    }
}
public class AdminUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport();
        objBal.GenerateProjectSchedule();
        objBal.GenerateProfitReport();
    }
}

如今在每個UI層當開發者輸入」objBal」下面的提示將會出現:

有什麼問題?

開發者在使用EmployeeUI時卻能夠調用其餘方法,這可能會致使開發者困惑。

什麼是ISP?

ISP:客戶不該該被迫實現他不使用的藉口。 還能夠說成:多個特定的客戶接口比一個通用接口更好。 簡單的說,若是你的接口太臃腫,應該把它拆分紅多個接口。

根據ISP原則更新後的代碼

public interface IEmployeeReportBAL
{
    void GeneratePFReport();
    void GenerateESICReport();
}
public interface IManagerReportBAL : IEmployeeReportBAL
{
    void GenerateResourcePerformanceReport();
    void GenerateProjectSchedule();
}
public interface IAdminReportBAL : IManagerReportBAL
{
    void GenerateProfitReport();
}
public class ReportBAL : IAdminReportBAL 
{    
    public void GeneratePFReport()
    {/*...............*/}

    public void GenerateESICReport()
    {/*...............*/}

    public void GenerateResourcePerformanceReport()
    {/*...............*/}

    public void GenerateProjectSchedule()
    {/*...............*/}

    public void GenerateProfitReport()
    {/*...............*/}
}
public class EmployeeUI {
    public void DisplayUI() {
        IEmployeeReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
    }
}

代碼提示:


public class ManagerUI
{
    public void DisplayUI()
    {
        IManagerReportBAL  objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport ();
        objBal.GenerateProjectSchedule ();
    }
}

代碼提示:


public class AdminUI
{
    public void DisplayUI()
    {
        IAdminReportBAL  objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport();
        objBal.GenerateProjectSchedule();
        objBal.GenerateProfitReport();
    }
}

代碼提示:


這樣經過遵照ISP原則,可讓客戶看到他們指望的內容。

V) D–DIP– Dependency Inversion principle (依賴倒置原則)

現實生活

咱們拿PC機來講。不一樣的部件,好比內存,硬盤,光驅等等,全都鬆散的鏈接在主板上。這意味着,假如某一天某些部件壞了能夠很容易更換。想像一下全部部件都緊密的耦合在一塊兒,這意味着它不能從主板上拆下來。在這種狀況下若是內存壞了,咱們不得不更換新的主板,這將很是浪費money,若是你是土豪那就無所謂了。

發現問題

看下面代碼:

public class CustomerBAL
{
    public void Insert(Customer c)
    {
        try
        {
            //Insert logic
        }
        catch (Exception e)
        {
            FileLogger f = new FileLogger();
            f.LogError(e);
        }
    }
}

public class FileLogger
{
    public void LogError(Exception e)
    {
        //Log Error in a physical file
    }
}

在上面代碼中CustomerBAL直接依賴於FileLogger,FileLogger會將異常日誌寫入文件。如今咱們假設明天管理決定將異常日誌輸出到事件視圖上。那將會怎麼樣?改變現有的代碼。Oh no!My God,那將會創造出新的錯誤。 

什麼是DIP?

DIP:高層模塊不該該依賴於底層模塊,相反,二者都應該依賴於抽象。

DIP解決方案

public interface ILogger
{
    void LogError(Exception e);
}

public class FileLogger:ILogger
{
    public void LogError(Exception e)
    {
        //Log Error in a physical file
    }
}
public class EventViewerLogger : ILogger
{
    public void LogError(Exception e)
    {
        //Log Error in a physical file
    }
}
public class CustomerBAL
{
    private ILogger _objLogger;
    public CustomerBAL(ILogger objLogger)
    {
        _objLogger = objLogger;
    }

    public void Insert(Customer c)
    {
        try
        {
            //Insert logic
        }
        catch (Exception e)
        {            
            _objLogger.LogError(e);
        }
    }
}

跟你所看到的同樣客戶依賴於抽象,ILogger能夠做爲實例替代任何它的派生類。

到目前爲止咱們討論了SOLID得五個原則。感謝Bob大叔。

結束了嗎?

除了Bob大叔總結的這些原則,還有其餘原則嗎?答案是確定的,可是這裏就不在一一講述其細節了。下面大概列一下:

  • 面向接口編程,而非實現
  • 不要重複本身
  • 封裝變化
  • 依賴於抽象,而非具體的類
  • 最少知識原則/迪米特法則
  • 組合優先於繼承
  • 好萊塢原則
  • 儘量使用設計模式
  • 爭取鬆散耦合的系統
  • 保持簡單和直接

總結

咱們沒法阻止需求變動,惟一可作的是開發設計出能夠適應變動的軟件。

  • 當你建立類,方法,或者其餘模塊時(這甚至適用於SQL的儲存過程和函數)SRP原則應該牢記在心中。保持代碼的可讀性,健壯,可測試性
  • 從我我的經驗上來看並非每次咱們都要遵照DIP原則,有時咱們須要依賴於具體的實現類。但須要確定的是理解系統,需求和環境屬性,找出哪些地方應該遵照DIP原則。
  • 遵照DIP和SRP將爲OCP的實現提供便利。
  • 確保建立特定接口,以便讓複雜和困惑遠離開發人員。同時ISP原則也將獲得遵照。
  • 在使用繼承時確保LSP原則

但願這篇文章能給你帶來收穫。多謝你的耐心閱讀。

許可

本文及相關代碼和文件都遵循CPOL協議(Code Project Open License

關於做者 


Marla Sukesh

Technical Lead ShawMan Softwares 

India 

關於譯者 

此處省略若干字

相關文章
相關標籤/搜索