继承结构的另类实现方式
2016-01-10 17:12
246 查看
不知从何时起,我不太轻易去设计抽象类了,一方面是因为我写的业务确实没有设计抽象类的需求,另一方面则基于以下三个考虑:
1、面向对象编程中建议多使用“组合”而不是使用“抽象”,原因在于“组合”更加灵活。
2、因为要公用一个“方法”,就迫不及待的设计出抽象关系,很容易造成抽象类不够SRP,久而久之抽象类成了大杂烩,不忍直视。
3、抽象设计要符合LSP(里氏替换原则),LSP是指:子类型必须能够替换掉它们的基本类型。
我们经常说继承关系就是IS-A关系,也就是说,如果一个新类型的对象被认为和一个已有类的对象之间满足IS-A关系,那么这个新类型就可以从已知类派生。
在《敏捷软件开发 原则、模式与实践》一书中,Robert C.Martin提到了一个关于正方形和矩形的例子让我记忆犹新:
从一般意义上讲,一个正方形就是一个矩形。因此,把Square视为从Rectangle类派生似乎是符合逻辑的。但是这一设计却会带来微妙的问题:
如果我们从Rectangle类派生一个Square类会怎样呢?
首先Square并不需要Width和Height两个成员,更加严重的问题是SetWidth和SetHeight这两个方法对Square类而言是不合适的,因为正方形的长度和宽度是相等的,不过我们可以复写这两个方法:
这样似乎没问题了,但是考虑下面的测试:
根据LSP原则,所有基类都可以用子类代替,这意味着我们在这个函数中可以传入一个Square实例,这将导致测试不通过,也就意味着该设计不符合LSP。
由此可见,一个良好的继承设计并不是将基类标记为abstract这么简单。
我在再谈扩展方法,从String.IsNullOrEmpty一文中提到过一种继承的替换方案,可以将公共方法扩展在接口上,本文我将再介绍一种带有函数式味道的方案。
举个栗子:
1、传统的继承方案
这是一个使用继承关系的设计方案,基类WebCrawlerProvider提供了三个虚方法,子类可以选择复写,比如写一个专门抓图片的ImageCrawlerProvider子类和一个专门抓取视频的VedioCrawlerProvider子类:
2、带有函数式味道的方案
相比于使用类继承,接口继承更加稳定和灵活,一般而言使用接口继承基本不会违反“面向对象”的各种原则,所以我们先定义一个接口:
该接口的实现只需要一个通用实现即可:
该实现将之前的方法设计为公共属性,这意味着我们可以直接对该属性赋值,此时如果实现一个ImageCrawlerProvider和VedioCrawlerProvider该当如何?
直观感受,后面的方案比继承方案更加简洁,代码量更少,熟练使用该方案和再谈扩展方法,从String.IsNullOrEmpty一文中提到的方案将会使你的代码增色不少。你觉得该方案相比类继承的方案如何呢?
1、面向对象编程中建议多使用“组合”而不是使用“抽象”,原因在于“组合”更加灵活。
2、因为要公用一个“方法”,就迫不及待的设计出抽象关系,很容易造成抽象类不够SRP,久而久之抽象类成了大杂烩,不忍直视。
3、抽象设计要符合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类会怎样呢?
首先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; } }
这样似乎没问题了,但是考虑下面的测试:
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子类:
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 } }
2、带有函数式味道的方案
相比于使用类继承,接口继承更加稳定和灵活,一般而言使用接口继承基本不会违反“面向对象”的各种原则,所以我们先定义一个接口:
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一文中提到的方案将会使你的代码增色不少。你觉得该方案相比类继承的方案如何呢?
相关文章推荐
- object-c 新旧两种弹出框
- 【spring配置】——spring与mybatis整合
- TCP通信流程解析
- JAVA里的字符串,String 类简单介绍
- linux网络编程之socket(十一):套接字I/O超时设置方法和用select实现超时
- 实战利用WireShark对Telnet协议进行抓包分析
- 使用手势输入数字
- POJ2288 Islands and Bridges(TSP:状压DP)
- SPOJ 4110 Fast Maximum Flow (最大流模板)
- 第 21 章 动态链接库
- SAX解析XML文件
- 重拾编程之路--jeetcode(java)--Length of Last Word
- TCP/IP传输层,你懂多少?
- [LeetCode] Course Schedule I (207) & II (210) 解题思路
- 在手机网页端实现分享朋友圈【转载】
- 多个ajax请求的同步异步问题
- Zabbix错误提示MySQL server has gone away解决
- TCP的状态 (SYN, FIN, ACK, PSH, RST, URG)
- windows下升级11.2.0.4.0到11.2.0.4.21的经验
- 第四章 变量和常量