不知從什麼時候起,我不過輕易去設計抽象類了,一方面是由於我寫的業務確實沒有設計抽象類的需求,另外一方面則基於如下三個考慮:html
一、面向對象編程中建議多使用「組合」而不是使用「抽象」,緣由在於「組合」更加靈活。編程
二、由於要公用一個「方法」,就火燒眉毛的設計出抽象關係,很容易形成抽象類不夠SRP,長此以往抽象類成了大雜燴,不忍直視。ide
三、抽象設計要符合LSP(里氏替換原則),LSP是指:子類型必須可以替換掉它們的基本類型。函數
咱們常常說繼承關係就是IS-A關係,也就是說,若是一個新類型的對象被認爲和一個已有類的對象之間知足IS-A關係,那麼這個新類型就能夠從已知類派生。測試
在《敏捷軟件開發 原則、模式與實踐》一書中,Robert C.Martin提到了一個關於正方形和矩形的例子讓我記憶猶新:設計
從通常意義上講,一個正方形就是一個矩形。所以,把Square視爲從Rectangle類派生彷佛是符合邏輯的。可是這一設計卻會帶來微妙的問題:視頻
public class Rectangle { protected double Width; protected double Height; public void SetWidth(double width) { Width = width; } public void SetHeight(double height) { Height = height; } public double Area { get { return Height*Width; } } }
若是咱們從Rectangle類派生一個Square類會怎樣呢?htm
首先Square並不須要Width和Height兩個成員,更加嚴重的問題是SetWidth和SetHeight這兩個方法對Square類而言是不合適的,由於正方形的長度和寬度是相等的,不過咱們能夠複寫這兩個方法:對象
public class Square : Rectangle { public new void SetWidth(double width) { Width = Height = width; } public new void SetHeight(double height) { Width = Height = height; } }
這樣彷佛沒問題了,可是考慮下面的測試:blog
public void TestArea(Rectangle rectangle) { rectangle.SetHeight(10); rectangle.SetWidth(2); var area = rectangle.Area; Debug.Assert(area == 20,"area should be 20"); }
根據LSP原則,全部基類均可以用子類代替,這意味着咱們在這個函數中能夠傳入一個Square實例,這將致使測試不經過,也就意味着該設計不符合LSP。
因而可知,一個良好的繼承設計並非將基類標記爲abstract這麼簡單。
我在再談擴展方法,從String.IsNullOrEmpty一文中提到過一種繼承的替換方案,能夠將公共方法擴展在接口上,本文我將再介紹一種帶有函數式味道的方案。
舉個栗子:
一、傳統的繼承方案
這是一個使用繼承關係的設計方案,基類WebCrawlerProvider提供了三個虛方法,子類能夠選擇複寫,好比寫一個專門抓圖片的ImageCrawlerProvider子類和一個專門抓取視頻的VedioCrawlerProvider子類:
public class WebCrawlerProvider { public virtual bool CheckContent(RequestContext context) { return true; } public virtual Crawler GetCrawler(RequestContext content) { return new Crawler(); } public virtual void SaveContent() { //save it } }
public class ImageCrawlerProvider:WebCrawlerProvider { public override bool CheckContent(RequestContext context) { //if it is image return true; } public override void SaveContent() { //save to D:\image } } public class VedioCrawlerProvider : WebCrawlerProvider { public override bool CheckContent(RequestContext context) { //if it is vedio return true; } public override Crawler GetCrawler(RequestContext content) { return new VedioCrawler(); } public override void SaveContent() { //save to d:\vedio } }
二、帶有函數式味道的方案
相比於使用類繼承,接口繼承更加穩定和靈活,通常而言使用接口繼承基本不會違反「面向對象」的各類原則,因此咱們先定義一個接口:
public interface IWebCrawlerProvider { Func<RequestContext, bool> CheckContent { get; set; } Func<RequestContext, Crawler> GetCrawler { get; set; } Action SaveContent { get; set; } }
該接口的實現只須要一個通用實現便可:
public class WebCrawlerProvider : IWebCrawlerProvider { public Func<RequestContext, bool> CheckContent { get; set; } public Func<RequestContext, Crawler> GetCrawler { get; set; } public Action SaveContent { get; set; } public WebCrawlerProvider() { CheckContent = context => true; GetCrawler=context=>new Crawler(); SaveContent=()=>{/*save to c:\default*/}; } }
該實現將以前的方法設計爲公共屬性,這意味着咱們能夠直接對該屬性賦值,此時若是實現一個ImageCrawlerProvider和VedioCrawlerProvider該當如何?
//use default WebCrawlerProvider var crawlerProvider=new WebCrawlerProvider(); //use image CrawlerProvider var imageCrawlerProvider=new WebCrawlerProvider() { CheckContent = context =>/*if it is image*/ true, GetCrawler = context=>new Crawler(), SaveContent = () => { /*save it to d:\image*/} }; //use vedio CrawlerProvider var vedioCrawlerProvider = new WebCrawlerProvider() { CheckContent = context => /*if it is vedio*/ true, GetCrawler = context => new Crawler(), SaveContent = () =>{/*save it to d:\vedio*/} };
直觀感覺,後面的方案比繼承方案更加簡潔,代碼量更少,熟練使用該方案和再談擴展方法,從String.IsNullOrEmpty一文中提到的方案將會使你的代碼增色很多。你以爲該方案相比類繼承的方案如何呢?