设计模式之美笔记10

记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步

序言

之前是创建型模式,主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。其中单例模式创建全局唯一的对象;工厂模式创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定的参数决定创建哪种类型的对象;建造者模式创建复杂对象,通过设置不同的可选参数,定制化的创建不同的对象;原型模式针对创建成本较大的对象,利用对已有对象进行复制的方式创建,达到节省创建时间的目的。

之后,学习另一种类型的设计模式:结构型模式。主要总结了一些类或对象组合在一起的经典结构,包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。

代理模式

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数据的读写。如果分类,可从下面两个维度分为四类

字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

针对不同的读取和写入场景,又在此基础上扩展很多子类。

以下面代码为例,打开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类二次封装原始类。区别:

  • 代理模式:在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式的最大不同
  • 桥接模式:目的是将接口部分和实现部分分离,从而让它们可较为容易、相对独立的加以改变
  • 装饰器模式:在不改变原始类接口的情况下,对原始类功能增强,并支持多个装饰器的嵌套使用
  • 适配器模式:事后的补救策略,提供跟原始类不同的接口,而代理类、装饰器模式提供的是跟原始类相同的接口
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值