记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步
文章目录
序言
之前是创建型模式,主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。其中单例模式创建全局唯一的对象;工厂模式创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定的参数决定创建哪种类型的对象;建造者模式创建复杂对象,通过设置不同的可选参数,定制化的创建不同的对象;原型模式针对创建成本较大的对象,利用对已有对象进行复制的方式创建,达到节省创建时间的目的。
之后,学习另一种类型的设计模式:结构型模式。主要总结了一些类或对象组合在一起的经典结构,包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。
代理模式
proxy design pattern,不改变原始类(或叫被代理类)代码的情况下,通过引入代理类给原始类附加功能。分静态代理和动态代理。
一种是和被代理对象实现相同的接口。一种是字节码。
动态代理之前也了解过。不赘述。
桥接模式
1. 原理解析
桥接模式,也叫做桥梁模式,bridge design pattern。有两种不同的理解。
GoF的《设计模式》的定义:Decouple an astraction from its implementation so that the two can vary independently。将抽象和实现解耦,让他们可以独立的变化。
另一种理解:一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这两个(或多个)维度可以独立进行扩展。也就是组合优于继承。
以JDBC为例解释GoF的定义。JDBC驱动是桥接模式的经典应用。具体代码参看之前整理的数据库部分,如果mysql换成Oracle,只需要将com.mysql.jdbc.Driver
替换为oracle.jdbc.driver.OracleDriver
即可。当然也可以将需要加载的Driver类写到配置文件中,程序启动时,自动加载配置。
具体如何进行数据库切换的?看com.mysql.jdbc.Driver
的代码:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
}catch (SQLException e){
throw new RuntimeException("can't register driver!");
}
}
// construct a new driver and register it with DriverManager
public Driver() throws SQLException{
// Require for Class.forName().newInstance()
}
}
执行Class.forName("com.mysql.jdbc.Driver")
时,实际做两件事。一是要求JVM查找并加载指定的Driver类,第二件事是执行该类的静态代码,也就是将Mysql Driver注册到DriverManager中。
那DriverManager类是干什么的?具体代码如下。当把具体的Driver实现类注册到DriverManager后,后续所有对jdbc接口的调用,都会委派到对具体的Driver实现类来执行,而Driver实现类都实现相同的接口,因此可以灵活切换Driver。
public class DriverManager {
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
//...
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//...
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
}
}
public static Connection getConnection(String url,String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
//...
}
桥接模式的定义是“将抽象和实现解耦,让它们可以独立变化”。在jdbc中,jdbc本身就相当于抽象,也即是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的Driver相当于“实现”,也就是跟具体数据库相关的一套“类库”。jdbc和driver独立开发,通过对象之间的组合关系,组装到一起。jdbc的所有逻辑操作,最终都委托给Driver执行。
2. 桥接模式的应用
之前有过一个API接口监控告警的例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、微信、短信、自动语音电话。通知的紧急程度有多种类型:SERVER(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道,如严重级别的消息通过自动语音电话告知相关人员。
当时的代码实现中,关于发送告警信息的部分,只给出粗略的设计
public class ErrorAlertHandler extends AlertHandler {
public ErrorAlertHandler(AlertRule rule,Notification notification){
super(rule,notification);
}
@Override
public void check(ApiStatInfo apiStatInfo){
if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi())){
notification.notify(NotificationEmergencyLevel.SERVER,"...");
}
}
}
Notification类的代码实现有个最明显的问题,就是很多if-else分支逻辑,每个if-else分支中的代码逻辑都比较复杂,发送通知的所有逻辑扎堆在Notification类中,需要将不同渠道的发送逻辑剥离出来,形成独立的消息发送类MsgSender,其中Notification类相当于抽象,MsgSender相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。重构后
public interface MsgSender {
void send(String message);
}
public class TelephoneMsgSender implements MsgSender {
private List<String> telephones;
public TelephoneMsgSender(List<String> telephones) {
this.telephones = telephones;
}
@Override
public void send(String message) {
//...
}
}
public class EmailMsgSender implements MsgSender{
//...和TelephoneMsgSender类似...
}
public class WechatMsgSender implements MsgSender{
//...和TelephoneMsgSender类似...
}
public abstract class Notification {
protected MsgSender msgSender;
public Notification(MsgSender msgSender) {
this.msgSender = msgSender;
}
public abstract void notify(String message);
}
public class ServereNotification extends Notification {
public ServereNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
public class UrgencyNotification extends Notification {
//...和ServereNotification类似...
}
public class NormalNotification extends Notification {
//...和ServereNotification类似...
}
public class TrivialNotification extends Notification {
//...和ServereNotification类似...
}
当然,Notification类的三个成员变量通过set方法设置,存在一个明显的问题,就是emailAddresses、telephones、wechatIds的数据可能在Notification类外部被修改,最好用构建者模式。
装饰器模式
主要解决继承过于复杂的问题,通过组合替代继承,主要作用是给原始类添加增强功能。有个特点,可以对原始类嵌套使用多个装饰器。以java的io类为例,学习装饰器模式。
java IO类的奇怪用法
java的IO类库非常庞大和复杂,有几十个类,负责IO数据的读写。如果分类,可从下面两个维度分为四类
字节流 | 字符流 | |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
针对不同的读取和写入场景,又在此基础上扩展很多子类。
以下面代码为例,打开test.txt文件,读取数据,其中,InputStream是个抽象类,FileInputStream是专门读取文件流的子类,BufferedInputStream是个支持带缓存功能的数据读取类,可提高数据读取的效率。
InputStream in = new FileInputStream("/xx/xx/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while(bin.read(data)!=-1){
//...
}
初看代码,会觉得java的IO的用法较为麻烦,需要先创建一个FileInputStream对象,然后传递给BufferedInputStream对象来使用。那为啥不设计一个继承FileInputStream并且支持缓存的BufferedFileInputStream类,就可以直接创建BufferedFileInputStream类对象,打开文件读取数据,就简单的多了。
基于继承的设计方案
如果InputStream只有一个子类FileInputStream,在FileInputStream基础上,设计一个孙子类BufferedFileInputStream还可以接受,但实际上,继承InputStream的子类很多,需要给每个子类派生支持缓存读取的子类。除了缓存,还需要对功能进行其他方面的增强,如DataInputStream,支持按照基本数据类型(int、boolean、long等)读取数据。
这种情况下,如果继续按照继承方式,会派生太多的孙子类。而这仅仅是附加了两个增强功能,如果更多增强功能,导致组合爆炸。
基于装饰器模式的设计方案
组合替代继承,只看简化的代码:
public abstract class InputStream{
//...
public int read(byte b[]) throw IOException{
return read(b,0,b.length);
}
//...
}
public class BufferedInputStream extends InputStream{
protected volatile InputStream in;
protected BufferedInputStream(InputStream in){
this.in = in;
}
//...实现基于缓存的读取数据接口...
}
public class DataInputStream extends InputStream{
protected volatile InputStream in;
protected DataInputStream(InputStream in){
this.in = in;
}
//...实现读取基本类型数据接口...
}
那装饰器模式是否就是简单的组合替代继承?当然不是,从java的IO类库设计看,相较于简单的组合,装饰器模式还有两个较为特殊的地方:
第一个是:装饰器类和原始类继承同样的父类,这样就可以对原始类“嵌套”多个装饰器类。如下面,对FileInputStream嵌套两个装饰器类:BufferedInputStream和DataInputStream,让它既支持缓存读取,又支持按照基本数据类型读取数据。
InputStream in = new FileInputStream("/xx/xx/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
第二个是:装饰器类是对功能的增强,也是装饰器模式应用场景的一个重要特点。和代理模式相比,代理模式中,代理类附加的是跟原始类无关的功能,而装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
//代理模式的代码结构(下面的接口可替换为抽象类)
public interface IA{
void f();
}
public class A implements IA{
public void f(){//...}
}
public class AProxy implements IA{
private IA a;
public AProxy(IA a){
this.a = a;
}
public void f(){
//新添加的代理逻辑
a.f();
//新添加的代理逻辑
}
}
//装饰器模式的代码结构(下面的接口可替换为抽象类)
public interface IA{
void f();
}
public class A implements IA{
public void f(){//...}
}
public class ADecorator implements IA{
private IA a;
public ADecorator(IA a){
this.a = a;
}
public void f(){
//功能增强代码
a.f();
//功能增强代码
}
}
实际看JDK的源码,BufferedInputStream、DataInputStream并非继承自InputStream,而是另一个叫FileInputStream的类,为啥要引入这个类呢?
再看BufferedInputStream的代码,InputStream是个抽象类而非接口,而且大部分方法(如read())都有默认实现,按理说,只要在BufferedInputStream重新实现需要增加缓存功能的方法即可,其他方法继承InputStream的默认实现。实际并不行。
对于即便不需要增加缓存功能的方法来说,BufferedInputStream还是必须把它重新实现一遍,简单包裹对InputStream对象的方法调用。具体代码如下。如果不重新实现,BufferedInputStream类无法将最终读取数据的任务,委托给传递进来的InputStream对象来完成。
public class BufferedInputStream extends InputStream{
protected volatile InputStream in;
protected BufferedInputStream(InputStream in){
this.in = in;
}
//f()方法不需要增强,只是重新调用下InputStream in对象的f()
public void f(){
in.f();
}
}
实际上,DataInputStream也存在同样的问题,为避免代码重复,java IO抽象出一个装饰器父类FileInputStream。InputStream的所有的装饰器类都继承自这个装饰器父类。这样,装饰器只要实现需要增强的方法即可,其他方法继承装饰器父类的默认实现。
public class FileInputStream extends InputStream{
protected volatile InputStream in;
protected FileInputStream(InputStream in){
this.in = in;
}
public int read() throw IOException{
return in.read();
}
//...
}
适配器模式
适配器模式Adapter Design Pattern,用来做适配,将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。例子就是USB接口。
有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系实现,对象适配器使用组合关系实现。具体代码如下。其中ITarget表示要转化成的接口定义。Adaptee是一组不兼容ITarget接口定义的接口,Adaptor将Adaptee转化为一组符合ITarget接口定义的接口。
//类适配器:基于继承
public interface ITarget{
void f1();
void f2();
void fc();
}
public class Adaptee{
public void fa(){//...}
public void fb(){//...}
public void fc(){//...}
}
public class Apdator extends Adaptee implements ITarget{
public void f1(){
super.fa();
}
public void f2(){
//...重新实现f2()..
}
// fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
//对象适配器:基于组合
public interface ITarget{
void f1();
void f2();
void fc();
}
public class Adaptee{
public void fa(){//...}
public void fb(){//...}
public void fc(){//...}
}
public class Adaptor implements ITarget{
private Adaptee adaptee;
public Adaptor(Adaptee adaptee){
this.adaptee = adaptee;
}
public void f1(){
adaptee.fa();//委托给Adatee
}
public void f2(){
//...重新实现f2()...
}
public void fc(){
adaptee.fc();
}
}
针对这两种实现方式,实际开发使用哪种的标准:一个是Adaptee接口的个数,一个是Adaptee和ITarget的契合程度。
- 如果Adaptee接口不多,都可以
- 如果Adaptee接口很多,而且Adaptee和ITarget接口定义大部分相同,推荐使用类适配器,因为Adaptor复用父类Adaptee的接口,比起对象适配器的实现方式,Adaptor的代码量少一些
- 如果Adaptee接口很多,而且Adaptee和ITarget接口定义大部分都不相同,推荐使用对象适配器,因为组合结构相对于继承更加灵活
适配器模式应用场景
一般,适配器模式可看做“补偿模式”,用来补救设计上的缺陷,算是无奈之举。如果设计初期,就能协调规避接口不兼容的问题,就没必要用了。哪些场景会出现接口不兼容?
1. 封装有缺陷的接口设计
假如依赖的外部系统在接口设计方面有缺陷(如包含大量静态方法),引入后会影响我们自身代码的可测试性。为隔离设计上的缺陷,希望对外部系统提供的接口二次封装,抽象出更好的接口设计,可采用适配器模式。
public class CD{
//这个类来自外部SDK,无权修改它的代码
//...
public static void staticFunction1(){//...}
public void uglyNamingFunction2(){//...}
public void toomanyParamsFunction3(int a, int b,...){//...}
public void lowPerformanceFunction4(){//...}
}
//使用适配器模式重构
public interface ITarget{
void function1();
void function2();
void function3(ParamsWrapperDefinition paramsWrapper);
void function4();
//...
}
//注意:适配器类的命名不一定非要末尾带Apdator
public class CDAdaptor extends CD implements ITarget{
//...
public void function1(){
super.staticFunction1();
}
public void function2(){
super.uglyNamingFunction2();
}
public void function3(ParamsWrapperDefinition paramsWrapper){
super.toomanyParamsFunction3(paramsWrapper.getA(),...);
}
public void function4(){
//...reimplement it...
}
}
2. 统一多个类的接口设计
某个功能的实现依赖多个外部系统(或者说类),通过适配器模式,将他们的接口适配为统一的接口定义,然后使用多态的特性复用代码逻辑。
假设系统要对用户输入的文本内容做敏感词过滤,为提高过滤的召回率,引入多款第三方敏感词过滤系统,依次对用户输入的内容过滤,过滤掉尽可能多的敏感词,但每个系统提供的过滤接口都不同。意味着无法复用一套逻辑来调用各个系统。此时可用适配器模式,将所有系统的接口适配为统一的接口定义,这样可复用调用敏感词过滤的代码。
public class ASensitiveWordsFilter {
//A敏感词过滤系统的接口
//text是原始文本,方法输出用***替换敏感词之后的文本
public String filterSexyWords(String text){
//...
}
public String filterPoliticalWords(String text){
//...
}
}
public class BSensitiveWordsFilter {
//B敏感词过滤系统提供的接口
public String filter(String text){
//...
}
}
public class CSensitiveWordsFilter {
//C敏感词过滤系统提供的接口
public String filter(String text,String mask){
//...
}
}
public class RiskManagement {
//未适配之前的代码:可测试性、扩展性不好
private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
public String filterSensitiveWords(String text){
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
maskedText = bFilter.filter(maskedText);
maskedText = cFilter.filter(maskedText,"***");
return maskedText;
}
}
// 改造
public interface ISensitiveWordsFilter {
// 统一接口定义
String filter(String text);
}
public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private ASensitiveWordsFilter aFilter;
@Override
public String filter(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
return maskedText;
}
}
//... 省略BSensitiveWordsFilterAdaptor CSensitiveWordsFilterAdaptor...
//扩展性更好,更符合开闭原则,如果添加新的敏感词过滤系统,这个类完全不用改动
public class RiskManagement {
private List<ISensitiveWordsFilter> filters = new ArrayList<>();
public void addSensitiveWordsFilter(ISensitiveWordsFilter filter){
filters.add(filter);
}
public String filterSensitiveWords(String text){
String maskedText = text;
for (ISensitiveWordsFilter filter: filters){
maskedText = filter.filter(maskedText);
}
return maskedText;
}
}
3. 替换依赖的外部系统
当我们把项目中依赖的一个外部系统替换为另一个外部系统,利用适配器模式,可减少代码的改动
//外部系统A
public interface IA{
//...
void fa();
}
public class A implements IA{
//...
public void fa(){//...}
}
//在项目中,外部系统A的使用范例
public class Demo{
private IA a;
public Demo(IA a){
this.a = a;
}
//...
}
Demo d = new Demo(new A());
//将外部系统A替换为外部系统B
public class BAdaptor implements IA{
private B b;
public BAdaptor(B b){
this.b = b;
}
public void fa(){
//...
b.fb();
}
}
//借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,只需将BAdaptor注入到Demo即可
Demo d = new Demo(new BAdaptor(new B()));
4. 兼容老版本接口
做版本升级时,对一些废弃的接口,不能直接删除,而是暂时保留,标注为deprecated,并将内部实现逻辑委托为新的接口实现。如JDK1.0包含一个遍历集合容器的类Enumeration,JDK2.0考虑重构该类,改名为Iterator,并优化代码。为兼容老代码,可暂时保留Enumeration,并将其实现替换为直接调用Iterator
public class Collections{
public static Enumeration enumeration(final Collection c){
return new Enumeration(){
Iterator i = c.iterator();
public boolean hasMoreElements(){
return i.hasNext();
}
public Object nextElement(){
return i.next();
}
}
}
}
5. 适配不同格式的数据
适配器除了用于接口的适配,还能用在不同格式的数据之间的适配。如把不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。此外,java的Arrays.asList()也可看做数据适配器,将数组类型的数据转化为集合容器类型。
适配器模式在java日志中的应用
java有很多日志框架,如log4j、logback,以及JDK提供的JUL(java.util.logging)和Apache的JCL(Jakarta Commons Logging)等。
大部分日志框架提供相似的功能,但并未实现统一的接口,不像jdbc,一开始就制定数据库操作的接口规范。
如果项目的某个组件使用log4j,而项目本身用的是logback,项目相当于有了两套日志打印框架,而每种日志框架都有自己特有的配置方式。管理很复杂,因此,需要统一日志打印框架。
Slf4j相当于jdbc,提供了一套打印日志的统一接口规范,不过只定义了接口,并未提供具体的实现,要配合其他日志框架使用。此外,slf4j也晚于这些日志框架,因此,不仅提供统一的接口定义,还提供了针对不同日志框架的适配器,对不同日志框架的接口二次封装,适配为统一的slf4j接口定义。
//slf4j统一的接口定义
public interface Logger{
public boolean isTraceEnabled();
public void trace(String msg);
public void trace(String format,Object arg);
public void trace(String format,Object arg1,Object arg2);
public void trace(String msg,Throwable t);
public boolean isDebugEnabled();
public void debug(String msg);
public void debug(String format,Object arg);
public void debug(String format,Object arg1,Object arg2);
public void debug(String format,Object[] argArray);
public void debug(String msg,Throwable t);
//...省略info warn error等接口
}
//log4j日志框架的适配器
//Log4jLoggerAdapter实现了LocationAwareLogger接口,而该接口继承自Logger接口
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implents LocationAwareLogger,Serializable{
final transient org.apache.log4j.Logger logger;//log4j
public boolean isDebugEnabled(){
return logger.isDebugEnabled();
}
public void debug(String msg){
logger.log(FQCN,Level.DEBUG,msg,null);
}
public void debug(String format,Object arg){
if(logger.isDebugEnabled()){
FormattingTuple ft = MessageFormatter.format(format,arg);
logger.log(FQCN,Level.DEBUG,ft.getMessage(),ft.getThrowable());
}
}
public void debug(String format,Object arg1,Object arg2){
if(logger.isDebugEnabled()){
FormattingTuple ft = MessageFormatter.format(format,arg1,arg2);
logger.log(FQCN,Level.DEBUG,ft.getMessage(),ft.getThrowable());
}
}
public void debug(String format,Object[] argArray){
if(logger.isDebugEnabled()){
FormattingTuple ft = MessageFormatter.format(format,argArray);
logger.log(FQCN,Level.DEBUG,ft.getMessage(),ft.getThrowable());
}
}
public void debug(String msg,Throwable t){
logger.log(FQCN,Level.DEBUG,msg,t);
}
//...省略一堆接口的实现...
}
所以,开发业务系统或开发框架、组件时,统一使用slf4j提供的接口编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback…),可动态指定(使用java的SPI技术),只需要将相应的SDK导入项目即可。
如果有些老的项目没有用Slf4j,而是直接用JCL打印,想替换为log4j,怎么办?slf4j还提供反向适配器,也就是从slf4j到其他日志框架的适配,先将JCL切换为Slf4j,再将Slf4j切换为log4j。
代理、桥接、装饰器、适配器的区别
都可称为wrapper模式,也就是通过wrapper类二次封装原始类。区别:
- 代理模式:在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式的最大不同
- 桥接模式:目的是将接口部分和实现部分分离,从而让它们可较为容易、相对独立的加以改变
- 装饰器模式:在不改变原始类接口的情况下,对原始类功能增强,并支持多个装饰器的嵌套使用
- 适配器模式:事后的补救策略,提供跟原始类不同的接口,而代理类、装饰器模式提供的是跟原始类相同的接口