继承结构的另类实现方式

目录

不知从何时起,我不太轻易去设计抽象类了,一方面是因为我写的业务确实没有设计抽象类的需求,另一方面则基于以下三个考虑:

1、面向对象编程中建议多使用“组合”而不是使用“抽象”,原因在于“组合”更加灵活。

2、因为要公用一个“方法”,就迫不及待的设计出抽象关系,很容易造成抽象类不够SRP,久而久之抽象类成了大杂烩,不忍直视。

3、抽象设计要符合LSP(里氏替换原则),LSP是指:子类型必须能够替换掉它们的基本类型。

我们经常说继承关系就是IS-A关系,也就是说,如果一个新类型的对象被认为和一个已有类的对象之间满足IS-A关系,那么这个新类型就可以从已知类派生。

在《敏捷软件开发 原则、模式与实践》一书中,Robert C.Martin提到了一个关于正方形和矩形的例子让我记忆犹新:

从一般意义上讲,一个正方形就是一个矩形。因此,把Square视为从Rectangle类派生似乎是符合逻辑的。但是这一设计却会带来微妙的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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类会怎样呢?

首先Square并不需要Width和Height两个成员,更加严重的问题是SetWidth和SetHeight这两个方法对Square类而言是不合适的,因为正方形的长度和宽度是相等的,不过我们可以复写这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class Square : Rectangle
{
    public new void SetWidth(double width)
    {
        Width = Height = width;
    }
 
    public new void SetHeight(double height)
    {
        Width = Height = height;
    }
}

这样似乎没问题了,但是考虑下面的测试:

1
2
3
4
5
6
7
8
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一文中提到过一种继承的替换方案,可以将公共方法扩展在接口上,本文我将再介绍一种带有函数式味道的方案。

举个栗子:

1、传统的继承方案

这是一个使用继承关系的设计方案,基类WebCrawlerProvider提供了三个虚方法,子类可以选择复写,比如写一个专门抓图片的ImageCrawlerProvider子类和一个专门抓取视频的VedioCrawlerProvider子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
    }
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
    }
}

2、带有函数式味道的方案

相比于使用类继承,接口继承更加稳定和灵活,一般而言使用接口继承基本不会违反“面向对象”的各种原则,所以我们先定义一个接口:

1
2
3
4
5
6
public interface IWebCrawlerProvider
{
    Func<RequestContext, bool> CheckContent { get; set; }
    Func<RequestContext, Crawler> GetCrawler { get; set; }
    Action SaveContent { get; set; }
}

该接口的实现只需要一个通用实现即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
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该当如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//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一文中提到的方案将会使你的代码增色不少。你觉得该方案相比类继承的方案如何呢?

posted @   richiezhang  阅读(958)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示