桥接模式
定义
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and
Body)模式或接口(Interfce)模式。
使用场景
桥我们大家都熟悉,顾名思义就是用来将河的两岸联系起来的。而此处的桥是用来将两个独立的结构联系起来,而这两个被联系起来的结构可以独立的变化,所有其他的理解只要建立在这个层面上就会比较容易。
下面是一些官方的说明,
-
如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
-
“抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
-
一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展
-
对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
优点:
- 分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
- 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
- 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
缺点:
- 增加了系统的理解与设计难度
- 需要正确地识别出系统中两个独立变化的维度
uml图
桥梁模式所涉及的角色有:
- 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。
- 修正抽象化(RefinedAbstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。
- 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
- 具体实现化(ConcreteImplementor)角色:这个角色给出实现化角色接口的具体实现。
场景
星巴克的咖啡有大杯中杯超大杯,口味有原味加奶加糖
分析当前业务场景,认为可以将咖啡的容量作为抽象化Abstraction,而咖啡口味为实现化Implementor
第一步:创建实现化部分的接口定义(咖啡口味的维度)
public interface ICoffeeAdditives {
void addSomething();
}
第二步:创建抽象化部分的接口定义(咖啡容量的维度)
//抽象化Abstraction
public abstract class Coffee {
protected ICoffeeAdditives additives;
public Coffee(ICoffeeAdditives additives){
this.additives=additives;
}
public abstract void orderCoffee(int count);
}
第三步:实现实现化部分(咖啡口味维度)的接口ICoffeeAdditives
//加奶
public class Milk implements ICoffeeAdditives {
@Override
public void addSomething() {
System.out.println("加奶");
}
}
//加糖
public class Sugar implements ICoffeeAdditives {
@Override
public void addSomething() {
System.out.println("加糖");
}
第四步:实现抽象化部分(咖啡的容量维度)的接口Coffee
public class LargeCoffee extends Coffee {
public LargeCoffee(ICoffeeAdditives additives) {
super(additives);
}
@Override
public void orderCoffee(int count) {
additives.addSomething();
System.out.println(String.format("大杯咖啡%d杯",count));
}
}
//小杯
public class SmallCoffee extends Coffee {
public SmallCoffee(ICoffeeAdditives additives) {
super(additives);
}
@Override
public void orderCoffee(int count) {
additives.addSomething();
System.out.println(String.format("小杯咖啡%d杯",count));
}
}
第五步:客户端调用
public class test {
public static void main(String[] args) {
//点两杯加奶的大杯咖啡
Coffee largeWithMilk=new LargeCoffee(new Milk());
largeWithMilk.orderCoffee(2);
SmallCoffee smallCoffee = new SmallCoffee(new Sugar());
smallCoffee.orderCoffee(1);
}
}
结果
通过使用桥接模式,就使得咖啡的容量和口味这两个维度可以独立变化,互不干扰。原本需要2x2=4个类的功能,现在只需要2+2=4 个类,有的同学就要说了,我去,加上那些基类和原来也差不多,没有多好啊?别急,等来了新需求你就知道哪个好了。新需求:增加一种加蜂蜜的口味,现在不使用桥接模式的类的个数是2x3=6,而使用了的是2+3=5,你说哪个好。
jdk 源码的桥接模式
java.util.logging是JDK自带的日志包,可以将日志输出到文件、内存或者控制台,作用与我们常用的log4j类似。
包中的Handler类和Formatter类在设计上利用了桥接模式,首先看类关系图:
Handle和Formatter类是两个抽象类,它们可以分别独立的变化(有不同的子类);而Handle类中包含对Formatter类的引用。
public abstract class Handler {
private static final int offValue = Level.OFF.intValue();
private final LogManager manager = LogManager.getLogManager();
private volatile Filter filter;
//引用 Formatter
private volatile Formatter formatter;
private volatile Level logLevel = Level.ALL;
private volatile ErrorManager errorManager = new ErrorManager();
private volatile String encoding;
//设置Formatter
public synchronized void setFormatter(Formatter newFormatter) throws SecurityException {
checkPermission();
// Check for a null pointer:
newFormatter.getClass();
formatter = newFormatter;
}
Handle通过setFormatter方法可以方便的替换不同的Formatter类。
Handle与Formatter介绍
1、Handlej和实现类
handle对象可以从一个logger中取出信息并输出到控制台、文件或者调用其它api发送到网络中;
handle可以通过setLevel方法关闭或打开;
handle通常是利用LogManager去设置自身的Handler、Filter、Formatter等属性值。
*备注:虽然父类Handle中提供了setFormatter方法,但子类streamhandle中也重新提供了配置各个成员变量值的方法:
private void configure() {
LogManager manager = LogManager.getLogManager();
String cname = getClass().getName();
setLevel(manager.getLevelProperty(cname +".level", Level.INFO));
setFilter(manager.getFilterProperty(cname +".filter", null));
setFormatter(manager.getFormatterProperty(cname +".formatter", new SimpleFormatter()));
try {
setEncoding(manager.getStringProperty(cname +".encoding", null));
} catch (Exception ex) {
try {
setEncoding(null);
} catch (Exception ex2) {
// doing a setEncoding with null should always work.
// assert false;
}
}
}
2、Formatter和实现类
Formatter支持格式化日志数据;通常每个Handler中都会有一个Formatter引用,Formatter可以将LogRecord对象转换为一个string字符串。
SimpleFormatter 的方法
public synchronized String format(LogRecord record) {
dat.setTime(record.getMillis());
String source;
if (record.getSourceClassName() != null) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
source += " " + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
String message = formatMessage(record);
String throwable = "";
if (record.getThrown() != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
record.getThrown().printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
return String.format(format,
dat,
source,
record.getLoggerName(),
record.getLevel().getLocalizedLevelName(),
message,
throwable);
}