极客时间:设计模式之美笔记

在面向对象编程中,抽象类和接口是两个经常被用到的语法概念,是面向对象四大特性,以及很多设计模式、设计思想、设计原则编程实现的基础。比如,我们可以使用接口来实现面向对象的抽象特性、多态特性和基于接口而非实现的设计原则使用抽象类来实现面向对象的继承特性和模板设计模式等等

不过,并不是所有的面向对象编程语言都支持这两个语法概念,比如,C++ 这种编程语言只支持抽象类,不支持接口;而像 Python 这样的动态编程语言,既不支持抽象类,也不支持接口。尽管有些编程语言没有提供现成的语法来支持接口和抽象类,我们仍然可以通过一些手段来模拟实现这两个语法概念。

这两个语法概念不仅在工作中经常会被用到,在面试中也经常被提及。比如,“接口和抽象类的区别是什么?什么时候用接口?什么时候用抽象类?抽象类和接口存在的意义是什么?能解决哪些编程问题?”等等。

什么是抽象类和接口?区别在哪里?

不同的编程语言对接口和抽象类的定义方式可能有些差别,但差别并不会很大。Java 这种编程语言,既支持抽象类,也支持接口,所以,为了让你对这两个语法概念有比较直观的认识,我们拿 Java 这种编程语言来举例讲解。

在 Java 这种编程语言中,我们是如何定义抽象类的。

下面这段代码是一个比较典型的抽象类的使用场景(模板设计模式)。Logger 是一个记录日志的抽象类,FileLogger 和 MessageQueueLogger 继承 Logger,分别实现两种不同的日志记录方式:记录日志到文件中和记录日志到消息队列中。FileLogger 和 MessageQueueLogger 两个子类复用了父类 Logger 中的 name、enabled、minPermittedLevel 属性和 log() 方法,但因为这两个子类写日志的方式不同,它们又各自重写了父类中的 doLog() 方法。

// 抽象类
public abstract class Logger {private String name;private boolean enabled;private Level minPermittedLevel;public Logger(String name, boolean enabled, Level minPermittedLevel) {this.name = name;this.enabled = enabled;this.minPermittedLevel = minPermittedLevel;}public void log(Level level, String message) {boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());if (!loggable) return;doLog(level, message);}protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {private Writer fileWriter;public FileLogger(String name, boolean enabled,Level minPermittedLevel, String filepath) {super(name, enabled, minPermittedLevel);this.fileWriter = new FileWriter(filepath); }@Overridepublic void doLog(Level level, String mesage) {// 格式化level和message,输出到日志文件fileWriter.write(...);}
}
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {private MessageQueueClient msgQueueClient;public MessageQueueLogger(String name, boolean enabled,Level minPermittedLevel, MessageQueueClient msgQueueClient) {super(name, enabled, minPermittedLevel);this.msgQueueClient = msgQueueClient;}@Overrideprotected void doLog(Level level, String mesage) {// 格式化level和message,输出到消息中间件msgQueueClient.send(...);}
}

通过上面的这个例子,我们来看一下,抽象类具有哪些特性。我总结了下面三点。

  • 抽象类不允许被实例化,只能被继承。也就是说,你不能 new 一个抽象类的对象出来(Logger logger = new Logger(…); 会报编译错误)。
  • 抽象类可以包含属性和方法方法既可以包含代码实现(比如 Logger 中的 log() 方法),也可以不包含代码实现(比如 Logger 中的 doLog() 方法)。不包含代码实现的方法叫作抽象方法。
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法。对应到例子代码中就是,所有继承 Logger 抽象类的子类,都必须重写 doLog() 方法。

在 Java 这种编程语言中,我们如何定义接口。

// 接口
public interface Filter {void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {@Overridepublic void doFilter(RpcRequest req) throws RpcException {//...鉴权逻辑..}
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {@Overridepublic void doFilter(RpcRequest req) throws RpcException {//...限流逻辑...}
}
// 过滤器使用Demo
public class Application {// filters.add(new AuthencationFilter());// filters.add(new RateLimitFilter());private List<Filter> filters = new ArrayList<>();public void handleRpcRequest(RpcRequest req) {try {for (Filter filter : filters) {filter.doFilter(req);}} catch(RpcException e) {// ...处理过滤结果...}// ...省略其他处理逻辑...}
}

上面这段代码是一个比较典型的接口的使用场景。我们通过 Java 中的 interface 关键字定义了一个 Filter 接口。AuthencationFilter 和 RateLimitFilter 是接口的两个实现类,分别实现了对 RPC 请求鉴权和限流的过滤功能。

代码非常简洁。结合代码,我们再来看一下,接口都有哪些特性。我也总结了三点。

  • 接口不能包含属性(也就是成员变量)。
  • 接口只能声明方法,方法不能包含代码实现。(jdk1.8可以使用了)
  • 类实现接口的时候,必须实现接口中声明的所有方法。

前面我们讲了抽象类和接口的定义,以及各自的语法特性。从语法特性上对比,这两者有比较大的区别,比如抽象类中可以定义属性、方法的实现,而接口中不能定义属性,方法也不能包含代码实现等等。除了语法特性,从设计的角度,两者也有比较大的区别。

抽象类实际上就是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承。我们知道,继承关系是一种 is-a 的关系,那抽象类既然属于类,也表示一种 is-a 的关系。

相对于抽象类的 is-a 关系来说,接口表示一种 has-a 关系,表示具有某些功能。对于接口,有一个更加形象的叫法,那就是协议(contract)

抽象类和接口能解决什么编程问题?

刚刚我们学习了抽象类和接口的定义和区别,现在我们再来学习一下,抽象类和接口存在的意义,让你知其然知其所以然。

我们为什么需要抽象类?它能够解决什么编程问题?

刚刚我们讲到,抽象类不能实例化,只能被继承。而前面,我们还讲到,继承能解决代码复用的问题。所以,抽象类也是为代码复用而生的。多个子类可以继承抽象类中定义的属性和方法,避免在子类中,重复编写相同的代码。

不过,既然继承本身就能达到代码复用的目的,而继承也并不要求父类一定是抽象类,那我们不使用抽象类,照样也可以实现继承和复用。从这个角度上来讲,我们貌似并不需要抽象类这种语法呀。那抽象类除了解决代码复用的问题,还有什么其他存在的意义吗?

我们还是拿之前那个打印日志的例子来讲解。我们先对上面的代码做下改造。在改造之后的代码中,Logger 不再是抽象类,只是一个普通的父类,删除了 Logger 中 log()、doLog() 方法,新增了 isLoggable() 方法。FileLogger 和 MessageQueueLogger 还是继承 Logger 父类,以达到代码复用的目的。具体的代码如下:

// 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable().
public class Logger {private String name;private boolean enabled;private Level minPermittedLevel;public Logger(String name, boolean enabled, Level minPermittedLevel) {//...构造函数不变,代码省略...}protected boolean isLoggable() {boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());return loggable;}
}
// 子类:输出日志到文件
public class FileLogger extends Logger {private Writer fileWriter;public FileLogger(String name, boolean enabled,Level minPermittedLevel, String filepath) {//...构造函数不变,代码省略...}public void log(Level level, String mesage) {if (!isLoggable()) return;// 格式化level和message,输出到日志文件fileWriter.write(...);}
}
// 子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {private MessageQueueClient msgQueueClient;public MessageQueueLogger(String name, boolean enabled,Level minPermittedLevel, MessageQueueClient msgQueueClient) {//...构造函数不变,代码省略...}public void log(Level level, String mesage) {if (!isLoggable()) return;// 格式化level和message,输出到消息中间件msgQueueClient.send(...);}
}

这个设计思路虽然达到了代码复用的目的,但是无法使用多态特性了。像下面这样编写代码,就会出现编译错误,因为 Logger 中并没有定义 log() 方法。

Logger logger = new FileLogger("access-log", true, Level.WARN, "/users/wangzheng/access.log");
logger.log(Level.ERROR, "This is a test log message.");

你可能会说,这个问题解决起来很简单啊。我们在 Logger 父类中,定义一个空的 log() 方法,让子类重写父类的 log() 方法,实现自己的记录日志的逻辑,不就可以了吗?

public class Logger {// ...省略部分代码...public void log(Level level, String mesage) { // do nothing... }
}
public class FileLogger extends Logger {// ...省略部分代码...@Overridepublic void log(Level level, String mesage) {if (!isLoggable()) return;// 格式化level和message,输出到日志文件fileWriter.write(...);}
}
public class MessageQueueLogger extends Logger {// ...省略部分代码...@Overridepublic void log(Level level, String mesage) {if (!isLoggable()) return;// 格式化level和message,输出到消息中间件msgQueueClient.send(...);}
}

这个设计思路能用,但是,它显然没有之前通过抽象类的实现思路优雅。我为什么这么说呢?主要有以下几点原因。

  • 在 Logger 中定义一个空的方法,会影响代码的可读性。如果我们不熟悉 Logger 背后的设计思想,代码注释又不怎么给力,我们在阅读 Logger 代码的时候,就可能对为什么定义一个空的 log() 方法而感到疑惑,需要查看 Logger、FileLogger、MessageQueueLogger 之间的继承关系,才能弄明白其设计意图。
  • 当创建一个新的子类继承 Logger 父类的时候,我们有可能会忘记重新实现 log() 方法之前基于抽象类的设计思路,编译器会强制要求子类重写 log() 方法,否则会报编译错误。你可能会说,我既然要定义一个新的 Logger 子类,怎么会忘记重新实现 log() 方法呢?我们举的例子比较简单,Logger 中的方法不多,代码行数也很少。但是,如果 Logger 有几百行,有 n 多方法,除非你对 Logger 的设计非常熟悉,否则忘记重新实现 log() 方法,也不是不可能的。
  • Logger 可以被实例化,换句话说,我们可以 new 一个 Logger 出来,并且调用空的 log() 方法。这也增加了类被误用的风险。当然,这个问题可以通过设置私有的构造函数的方式来解决。不过,显然没有通过抽象类来的优雅

我们为什么需要接口?它能够解决什么编程问题?

抽象类更多的是为了代码复用,而接口就更侧重于解耦

接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性

实际上,接口是一个比抽象类应用更加广泛、更加重要的知识点。

比如,我们经常提到的“基于接口而非实现编程”,就是一条几乎天天会用到,并且能极大地提高代码的灵活性、扩展性的设计思想。关于接口这个知识点,后面会有更详细的博文

如何模拟抽象类和接口两个语法概念?

在前面举的例子中,我们使用 Java 的接口语法实现了一个 Filter 过滤器。不过,如果你熟悉的是 C++ 这种编程语言,你可能会说,C++ 只有抽象类,并没有接口,那从代码实现的角度上来说,是不是就无法实现 Filter 的设计思路了呢?

实际上,我们可以通过抽象类来模拟接口。怎么来模拟呢?这是一个不错的面试题,你可以先思考一下,然后再来看我的讲解。

我们先来回忆一下接口的定义:接口中没有成员变量,只有方法声明,没有方法实现,实现接口的类必须实现接口中的所有方法。只要满足这样几点,从设计的角度上来说,我们就可以把它叫作接口。实际上,要满足接口的这些语法特性并不难。在下面这段 C++ 代码中,我们就用抽象类模拟了一个接口(下面这段代码实际上是策略模式中的一段代码)。

class Strategy { // 用抽象类模拟接口public:~Strategy();virtual void algorithm()=0;protected:Strategy();
};

抽象类 Strategy 没有定义任何属性,并且所有的方法都声明为 virtual 类型(等同于 Java 中的 abstract 关键字),这样,所有的方法都不能有代码实现,并且所有继承这个抽象类的子类,都要实现这些方法。从语法特性上来看,这个抽象类就相当于一个接口。

不过,如果你熟悉的既不是 Java,也不是 C++,而是现在比较流行的动态编程语言,比如 Python、Ruby 等,你可能还会有疑问:在这些动态语言中,不仅没有接口的概念,也没有类似 abstract、virtual 这样的关键字来定义抽象类,那该如何实现上面的讲到的 Filter、Logger 的设计思路呢?实际上,除了用抽象类来模拟接口之外,我们还可以用普通类来模拟接口。具体的 Java 代码实现如下所示。

public class MockInteface {protected MockInteface() {}public void funcA() {throw new MethodUnSupportedException();}
}

我们知道类中的方法必须包含实现,这个不符合接口的定义。但是,我们可以让类中的方法抛出 MethodUnSupportedException 异常,来模拟不包含实现的接口,并且能强迫子类在继承这个父类的时候,都去主动实现父类的方法,否则就会在运行时抛出异常。我们将构造函数设置成 protected 属性的,这样就能避免非同包下的类去实例化 MockInterface。不过,这样还是无法避免同包中的类去实例化 MockInterface。为了解决这个问题,我们可以学习 Google Guava 中 @VisibleForTesting 注解的做法,自定义一个注解,人为表明不可实例化。

刚刚我们讲了如何用抽象类来模拟接口,以及如何用普通类来模拟接口,那如何用普通类来模拟抽象类呢?这个问题留给你自己思考,你可以留言说说你的实现方法。

实际上,对于动态编程语言来说,还有一种对接口支持的策略,那就是 duck-typing。我们在前面讲到多态的时候也有讲过,你可以再回忆一下。

如何决定该用抽象类还是接口?

刚刚的讲解可能有些偏理论,现在,我们就从真实项目开发的角度来看一下,在代码设计、编程开发的时候,什么时候该用抽象类?什么时候该用接口?

实际上,判断的标准很简单。如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类
如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口

从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。

回顾

  1. 抽象类和接口的语法特性

抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。接口不能包含属性,只能声明方法,方法不能包含代码实现(1.8已经可以实现了)。类实现接口的时候,必须实现接口中声明的所有方法。

  1. 抽象类和接口存在的意义

抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

  1. 抽象类和接口的应用场景区别

什么时候该用抽象类?什么时候该用接口?实际上,判断的标准很简单。如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,我们就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口。

实习成长之路:设计模式二:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?相关推荐

  1. Java后端架构师的成长之路(二)——Java设计模式(2)

    Java设计模式 23种设计模式 结构型模式 适配器模式 基本介绍 适配器模式工作原理 类适配器模式 对象适配器模式 接口适配器模式 适配器模式在SpringMVC框架应用的源码分析 适配器模式的注意 ...

  2. Java后端架构师的成长之路(二)——Java设计模式(1)

    Java设计模式 Java设计模式入门 先看几个经典的面试题 原型设计模式问题 设计模式的七大原则 金融借贷平台项目 解释器设计模式 单例设计模式 设计模式的重要性 设计模式七大原则 设计模式的目的 ...

  3. Java后端架构师的成长之路(二)——Java设计模式(3)

    Java设计模式 23种设计模式 行为型模式 模板方法模式 豆浆制作问题 模板方法模式基本介绍 模板方法模式原理类图 模板方法模式解决豆浆制作问题 模板方法模式的钩子方法 模板方法模式在Spring框 ...

  4. 菜鸟程序员的成长之路(二)——时间都去哪儿了

    距离我的上篇博客菜鸟程序员的成长之路(一)已经过去六个月的时间,又到了总结收获的季节.在漫长的六个月里,发生的了很多的事.残存在大脑中的记忆(不是脑残),已经不能将我带回到去年的九月份,想问自己一句: ...

  5. 我的创新成长之路(二)

    大二年是我成长的一年,在这一年中我成长了许多,在这一年中,我得极力感谢07级学长们,还有08级部分同僚们. 就这样我成了创新  电子设计部的一员.进部之后,我才了解到我的进部跟两个人有很大的关系,这个 ...

  6. CISSP的成长之路(二十一):用户持有凭证

    在51CTO安全频道特别策划的CISSP的成长之路系列的上一篇文章<讲解身份验证过程>里,J0ker给大家介绍了用户验证时所依赖的三种验证要素.密码和密码短语及其安全使用原则.作为技术实施 ...

  7. type接口和class的区别(类型和类)

    Class public final class Class < T > implements java . io . Serializable, GenericDeclaration, ...

  8. 十年风雨,一个普通程序员的成长之路(二)外包:颠沛与流离

    海风.宁波 2012年5月,又是一次外包的工作. 此时的工资已经是5500,出差补助每天50,包住,算下来,每月也能结余5000以上了. 这次是外包到宁波,给神州数码做一个外包项目. 哦,神码,我的神 ...

  9. (九)Java架构师成长之路-设计模式:组合模式

    一.定义 组合模式也叫整体-部分模式,目的是为了实现整体和部分都有一组相同的操作方法. 属于结构型模式 注意区别一下组合和聚合的区别: 组合:各部分组织结构有相同的生命周期,例如人的各个器官和人,人死 ...

  10. python 代理ip验证_python爬虫成长之路(二):抓取代理IP并多线程验证

    上回说到,突破反爬虫限制的方法之一就是多用几个代理IP,但前提是我们得拥有有效的代理IP,下面我们来介绍抓取代理IP并多线程快速验证其有效性的过程. 一.抓取代理IP 提供免费代理IP的网站还挺多的, ...

最新文章

  1. 《课程的反思与重建--我们需要什么样的课程观》 心得体会
  2. pyinstaller使用方法及案例
  3. python中e-r图_E-R图基本步骤
  4. apache缺省banner_http服务器缺省banner
  5. 网友的VOIP总结 1
  6. KNN(一)--简单KNN原理及实现
  7. projecteuler_problem12
  8. spring @POSTConstruction注解的作用
  9. H5传奇源码,附带微信支付,商城系统,新增了元宝交易商城系统源码
  10. office word 页码从任意页开始
  11. 数码管与74HC573,74HC138电路
  12. JavaScript形而上的策略模式
  13. CheXNet-master:CUDA out of memery
  14. Android向系统日历添加日程提醒事件
  15. Android wps自定义功能,完美兼容安卓4.2新增多项功能 WPS移动版升级至5.2
  16. CentOS7.5安装oracle11g
  17. OGRE粒子系统脚本详解
  18. 支持两个USB Type-C接口都能投屏的便携显示器方案
  19. 如何获取用户当前位置并生成国际拨号前缀+验证不同国家的手机号格式
  20. 科技翻译面面观─ 从计算机领域谈起

热门文章

  1. dategridview代码选中行_使用IntelliJ IDEA进行Java代码调试的技巧
  2. 能直接挂在iis的动静态网站_如何防止网站高并发引起的系统崩溃?
  3. python requests 上传文件_Python 实现小程序云存储文件上传
  4. bio和bieos哪个标注模式好_通用块层bio详解
  5. 实战Vue:ToDoList
  6. linux 执行安装脚本,批量执行Linux安装程序和脚本
  7. MpscGrowableArrayQueue分析
  8. 给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
  9. 如何使用 DefaultServlet DefaultServletHttpRequestHandler 来处理静态资源
  10. 数据结构-查找-折半查找-二叉排序树查找