面向?qū)ο笤O(shè)計(jì)原則
一 Single Responsibility Principle——單一職責(zé)原則
核心思想: 一個(gè)類應(yīng)該只有一個(gè)引起它變化的原因.
假設(shè)存在這樣的設(shè)計(jì). Rectangle類具有兩個(gè)方法,一個(gè)方法是計(jì)算矩形的面積 , 另一個(gè)方法是把矩形繪制在屏幕上.
CaculateArea方法只會(huì)進(jìn)行簡(jiǎn)單的數(shù)學(xué)運(yùn)算,而Draw方法則調(diào)用GUI組件實(shí)現(xiàn)繪制矩形的功能. 顯然,這個(gè)類就包含了兩個(gè)不同的職責(zé)了. 那這樣又會(huì)帶來(lái)什么問(wèn)題呢? 考慮這樣一個(gè)場(chǎng)景:現(xiàn)在有一個(gè)幾何學(xué)應(yīng)用程序調(diào)用了這一個(gè)類,已便實(shí)現(xiàn)計(jì)算面積的功能,在這個(gè)程序中不需要用到繪制矩形的功能. 問(wèn)題一:部署幾何應(yīng)用程序需要把GUI組件一同部署,而且這個(gè)組件根本沒(méi)有使用到.問(wèn)題二:對(duì)Rectangle類的改變,比如Draw方法改用另外一套GUI組件,必須對(duì)幾何應(yīng)用程序進(jìn)行一次重新部署.
可見(jiàn),一個(gè)類如果承擔(dān)的職責(zé)過(guò)多,就等于把職責(zé)耦合在一起了,容易導(dǎo)致脆弱的設(shè)計(jì),帶來(lái)額外的麻煩. 在實(shí)際開(kāi)發(fā)中, 業(yè)務(wù)規(guī)則的處理和數(shù)據(jù)持久化一般是不同時(shí)存在同一個(gè)類中的,業(yè)務(wù)規(guī)則往往會(huì)頻繁地變化,而持久化的方式卻不會(huì)經(jīng)常性地變化.如果這兩個(gè)職責(zé)混合在同一個(gè)類中,業(yè)務(wù)規(guī)則頻繁變化導(dǎo)致類的修改,只調(diào)用持久化方法的類也必須跟著重新編譯,部署的次數(shù)常常會(huì)超過(guò)我們希望的次數(shù). 對(duì)業(yè)務(wù)規(guī)則和持久化任務(wù)的職責(zé)分離就是遵循單一職責(zé)原則的體現(xiàn).
對(duì)上述Recangle類可進(jìn)行這樣的修改:
二 Open Closed Principle——開(kāi)放封閉原則
核心思想:對(duì)擴(kuò)展開(kāi)放,對(duì)修改封閉.
"需求總是變化的." 擁抱變化似乎就是軟件開(kāi)發(fā)的真理之一. 經(jīng)常會(huì)有這樣令人沮喪的情景出現(xiàn):新的需求來(lái)了,對(duì)不起,我的代碼設(shè)計(jì)必須大幅度推倒重來(lái). 設(shè)計(jì)的壞味道讓我們深受其害,那么怎樣的設(shè)計(jì)才能面對(duì)需求的改變卻可以保持相對(duì)穩(wěn)定呢?
針對(duì)這樣的問(wèn)題,OCP給了我們?nèi)缦碌慕ㄗh:在發(fā)生變化的時(shí)候,不要修改類的源代碼,要通過(guò)添加新代碼來(lái)增強(qiáng)現(xiàn)有類的行為.
對(duì)擴(kuò)展開(kāi)放,對(duì)修改封閉,這兩個(gè)特征似乎就是相互矛盾的. 通常觀念來(lái)講,擴(kuò)展不就是修改源代碼嗎?怎么可能在不改動(dòng)源代碼的情況下去更改它的行為呢?
答案就是抽象(Interface 和 抽象基類).實(shí)現(xiàn)OCP的核心思想就是對(duì)抽象編程. 讓類依賴于固定的抽象,對(duì)修改就是封閉的; 而通過(guò)面向?qū)ο蟮睦^承和多態(tài)機(jī)制,通過(guò)覆寫(xiě)方法改變固有行為,實(shí)現(xiàn)新的擴(kuò)展方法,對(duì)于擴(kuò)展就是開(kāi)放的.
來(lái)看一個(gè)例子. 實(shí)現(xiàn)一個(gè)能夠根據(jù)客戶端的調(diào)用要求繪制圓形和長(zhǎng)方形的應(yīng)用程序. 初始設(shè)計(jì)如下:
- public class Draw
- {
- public void DrawRectangle()
- {
- //繪制長(zhǎng)方形
- }
- public void DrawCircle()
- {
- //繪制圓形
- }
- }
- public enum Sharp
- {
- /// <summary>
- /// 長(zhǎng)方形
- /// </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("調(diào)用出錯(cuò)!");
- }
- }
- }
- //調(diào)用代碼
- DrawProcess draw = new DrawProcess();
- draw.Draw(Sharp.Circle);
現(xiàn)在的代碼可以正確地運(yùn)行. 一切似乎都趨近于理想. 然而,需求的變更總是讓人防不勝防. 現(xiàn)在程序要求要實(shí)現(xiàn)可以繪制正方形. 在原本的代碼設(shè)計(jì)下,必須做如下的改動(dòng).
- //在Draw類中添加
- public void DrawSquare()
- {
- //繪制正方形
- }
- //在枚舉Sharp中添加
- /// <summary>
- /// 正方形
- /// </summary>
- Square ,
- //在DrawProcess類的switch判斷中添加
- case Sharp.Square:
- _draw.DrawSquare();
- break;
需求的改動(dòng)產(chǎn)生了一系列相關(guān)模塊的改動(dòng),設(shè)計(jì)的壞味道悠然而生. 現(xiàn)在運(yùn)用OCP, 來(lái)看一下如何對(duì)代碼進(jìn)行一次重構(gòu).
- /// <summary>
- /// 繪制接口
- /// </summary>
- public interface IDraw
- {
- void Draw();
- }
- public class Circle:IDraw
- {
- public void Draw()
- {
- //繪制圓形
- }
- }
- public class Rectangle:IDraw
- {
- public void Draw()
- {
- //繪制長(zhǎng)方形
- }
- }
- 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();
- }
- }
- //調(diào)用代碼
- IDraw circle = new Circle();
- DrawProcess draw = new DrawProcess(circle);
- draw.DrawSharp();
假如現(xiàn)在需要有繪制正方形的功能,則只需添加一個(gè)類Square 即可.
- public class Square:IDraw
- {
- public void Draw()
- {
- //繪制正方形
- }
- }
只需新增加一個(gè)類且對(duì)其他的任何模塊完全沒(méi)有影響,OCP出色地完成了任務(wù).
如果一開(kāi)始就采用第二種代碼設(shè)計(jì),在需求的暴雨來(lái)臨時(shí),你會(huì)欣喜地發(fā)現(xiàn)你已經(jīng)到家了, 躲過(guò)了被淋一身濕的悲劇. 所以在一開(kāi)始設(shè)計(jì)的時(shí)候,就要時(shí)刻地思考,根據(jù)對(duì)應(yīng)用領(lǐng)域的理解來(lái)判斷最有可能變化的種類,然后構(gòu)造抽象來(lái)隔離那些變化. 經(jīng)驗(yàn)在這個(gè)時(shí)候會(huì)顯得非常寶貴,可能會(huì)幫上你的大忙.
OCP很美好,然而絕對(duì)的對(duì)修改關(guān)閉是不可能的,都會(huì)有無(wú)法對(duì)之封閉的變化. 同時(shí)必須清楚認(rèn)識(shí)到遵循OCP的代價(jià)也是昂貴的,創(chuàng)建適當(dāng)?shù)某橄笫且ㄙM(fèi)開(kāi)發(fā)時(shí)間和精力的. 如果濫用抽象的話,無(wú)疑引入了更大的復(fù)雜性,增加維護(hù)難度.
三 Liskov Subsitution Principle——里氏替換原則
核心思想: 子類必須能夠替換掉它們的父類型.
考慮如下情況:
- 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)
- {
- //派生類缺乏完整訪問(wèn)能力,即無(wú)法訪問(wèn)父類的私有成員_state
- //因此該類型也許不能完成其父類型能夠滿足的契約
- }
- }
- //控制臺(tái)應(yīng)用程序代碼
- class Program
- {
- static void Main(string[] args)
- {
- ProgrammerToy toy = new CustomProgrammerToy();
- toy.SetState(5);
- Console.Write(toy.State.ToString());
- }
- }
從語(yǔ)法的角度來(lái)看, 代碼沒(méi)有任何問(wèn)題. 不過(guò)從行為的角度來(lái)看 , 二者卻存在不同. 在使用CustomProgrammerToy替換父類的時(shí)候, 輸出的是0而不是5, 與既定的目標(biāo)相差千里. 所以不是所有的子類都能安全地替換其父類使用.
前面談到的開(kāi)發(fā)封閉原則和里氏替換原則存在著密切的關(guān)系. 實(shí)現(xiàn)OCP的核心是對(duì)抽象編程, 由于子類型的可替換性才使得使用父類類型的模塊在無(wú)需修改的情況下就可以擴(kuò)展, 所以違反了里氏替換原則也必定違反了開(kāi)放封閉原則.
慶幸的是, 里氏替換原則還是有規(guī)律可循的.父類盡可能使用接口或抽象類來(lái)實(shí)現(xiàn),同時(shí)必須從客戶的角度理解,按照客戶程序的預(yù)期來(lái)保證子類和父類在行為上的相容.
四 InterFace Segregation Principle——接口隔離原則
核心思想:使用多個(gè)小的專門的接口,而不要使用一個(gè)大的總接口.
直接來(lái)看一個(gè)例子: 假設(shè)有一個(gè)使用電腦的接口
程序員類實(shí)現(xiàn)接口IComputerUse, 玩游戲,編程,看電影, 多好的事情.
現(xiàn)在有一個(gè)游戲發(fā)燒友,他也要使用電腦, 為了重用代碼 , 實(shí)現(xiàn)OCP, 他也實(shí)現(xiàn)接口IComputerUse
看出什么問(wèn)題了嗎? GamePlayer PlayGame無(wú)可厚非,WatchMovies小消遣, 但要編程干什么?
這就是胖接口帶來(lái)的弊端,會(huì)導(dǎo)致實(shí)現(xiàn)的類必須完全實(shí)現(xiàn)接口的所有方法, 而有些方法對(duì)客戶來(lái)說(shuō)是無(wú)任何用處的,在設(shè)計(jì)上這是一種"浪費(fèi)". 同時(shí),如果對(duì)胖接口進(jìn)行修改, 比如程序員要使用電腦配置為服務(wù)器, 在IComputerUse上添加Server方法, 同樣GamePlayer也要修改(這種修改對(duì)GamePlayer是毫無(wú)作用的),是不是就引入了額外的麻煩?
所以應(yīng)該避免出現(xiàn)胖接口,要使接口實(shí)現(xiàn)高內(nèi)聚(高內(nèi)聚是指一個(gè)模塊中各個(gè)部分都是為完成一項(xiàng)具體功能而協(xié)同工作,緊密聯(lián)系,不可分割). 當(dāng)出現(xiàn)了胖接口,就要考慮重構(gòu).優(yōu)先推薦的方法是使用多重繼承分離,即實(shí)現(xiàn)小接口.
將IComputerUse拆分為IComputerBeFun和IComputerProgram, Progammer類則同時(shí)實(shí)現(xiàn)IComputerBeFun和IComputerProgram接口,現(xiàn)在就各取所需了.
與OCP類似, 接口也并非拆分地越小越好, 因?yàn)樘嗟慕涌跁?huì)影響程序的可讀性和維護(hù)性,帶來(lái)難以琢磨的麻煩. 所以設(shè)計(jì)接口的時(shí)刻要著重考慮高內(nèi)聚性, 如果接口中的方法都?xì)w屬于同一個(gè)邏輯劃分而協(xié)同工作,那么這個(gè)接口就不應(yīng)該再拆分.
五 Dependency Inversion Principle——依賴倒置原則
核心思想: 高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴抽象。抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。
當(dāng)一個(gè)類A存在指向另一個(gè)具體類B的引用的時(shí)候,類A就依賴于類B了。如:
- /// <summary>
- /// 商品類
- /// </summary>
- public class Product
- {
- public int Id { get; set; }
- }
- /// <summary>
- /// 商品持久化類
- /// </summary>
- public class ProductRepository
- {
- public IList<Product> FindAll()
- {
- //假設(shè)從SQL Server數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
- return null;
- }
- }
- /// <summary>
- /// 商品服務(wù)類
- /// </summary>
- public class ProductService
- {
- private ProductRepository _productRepository;
- public IList<Product> GetProducts()
- {
- _productRepository = new ProductRepository();
- return _productRepository.FindAll();
- }
- }
(在前面單一職責(zé)原則中有提到,業(yè)務(wù)邏輯處理和對(duì)象持久化分屬兩個(gè)職責(zé),所以應(yīng)該拆分為兩個(gè)類。)高層模塊ProductService類中引用了底層模塊具體類ProductRepository,所以ProductService類就直接依賴于ProductRepository了。那么這樣的依賴會(huì)帶來(lái)什么問(wèn)題呢?
"需求總是那么不期而至"。原本ProductRepository是從SQL Server數(shù)據(jù)庫(kù)中讀存數(shù)據(jù),現(xiàn)在要求從MySQL數(shù)據(jù)庫(kù)中讀存數(shù)據(jù)。由于高層模塊依賴于底層模塊,現(xiàn)在底層模塊ProductRepository發(fā)生了更改,高層模塊ProductService也需要跟著一起修改,回顧之前談到的設(shè)計(jì)原則,這是不是就違反了OCP呢?OCP的核心思想是對(duì)抽象編程,DIP的思想是依賴于抽象,這也讓我們更清楚地認(rèn)識(shí)到,面向?qū)ο笤O(shè)計(jì)的時(shí)候,要綜合所有的設(shè)計(jì)原則考慮。DIP給出了解決方案:在依賴之間定義一個(gè)接口,使得高層模塊調(diào)用接口,而底層模塊實(shí)現(xiàn)接口,以此來(lái)控制耦合關(guān)系。(在上面OCP的例子中,也是使用了這一個(gè)方法。)所以可以對(duì)代碼做如下的重構(gòu):
- View Code
- /// <summary>
- /// 商品持久化接口
- /// </summary>
- public interface IProductRepository
- {
- List<Product> FindAll();
- }
- /// <summary>
- /// 商品持久化類
- /// </summary>
- public class ProductRepository:IProductRepository
- {
- public IList<Product> FindAll()
- {
- //假設(shè)從SQL Server數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
- return null;
- }
- }
- /// <summary>
- /// 商品服務(wù)類
- /// </summary>
- public class ProductService
- {
- private IProductRepository _productRepository;
- private ProductService() { }
- //使用構(gòu)造函數(shù)依賴注入
- public ProductService(IProductRepository productRepository)
- {
- _productRepository = productRepository;
- }
- public IList<Product> GetProducts()
- {
- return _productRepository.FindAll();
- }
- }
現(xiàn)在已對(duì)變化進(jìn)行了抽象隔離,再根據(jù)OCP,我相信實(shí)現(xiàn)從MySQL數(shù)據(jù)庫(kù)中讀存數(shù)據(jù)的需求已經(jīng)可以被輕松地解決掉了。