繼承結構的另類實現方式

不知從什麼時候起,我不過輕易去設計抽象類了,一方面是由於我寫的業務確實沒有設計抽象類的需求,另外一方面則基於如下三個考慮: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一文中提到的方案將會使你的代碼增色很多。你以爲該方案相比類繼承的方案如何呢?

相關文章
相關標籤/搜索