I walk very slowly, but I never walk backwards
设计模式 - 桥接模式
寂然
大家好,我是寂然,本节课,我们来聊设计模式中的桥接模式,闲言少叙,首先,我们来看一个生活中的案例
案例演示 - 电脑分类
现在我们对不同类型的电脑,不同品牌实现操作编程,其实我们知道,电脑有各种各样的类型,比如台式机,笔记本,平板电脑等,而每个类型都有不同的品牌,比如台式机的品牌有华为,三星,苹果,笔记本的品牌也有华为,三星,苹果等....,如下图所示
案例分析
这样的话,我们现在对不同类型的电脑,不同品牌进行编程的时候,第一种方案,我们为每种类型都提供各种品牌,如上图所示,把电脑作为父类,台式机去继承父类电脑,而华为台式机去继承台式机类....,这种思路来完成编程
这样是可以完成当前的业务逻辑的,下面我们一起来考虑扩展性问题:
如果我们再增加一个电脑类型,比如一体机,就需要增加各个品牌的一体机,同样如果再增加一个品牌小米,也要在各个类型下增加,这样的话,扩展性很差,即(类爆炸)
违反了单一职责原则,当我们增加一体机的时候,要增加所有品牌的一体机,很大程度上增加了代码维护的成本,针对这样的问题,我们来思考下解决方案 - 我们来引入桥接模式
基本介绍
桥接模式(Bridge pattern)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变
桥接模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责,它的主要特点是把抽象与行为实现分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展
将抽象部分与他的实现部分分离这句话不是很好理解,其实这并不是将抽象类与他的派生类分离,而是抽象类和它的派生类用来实现自己的对象,这句话套用《大话设计模式》里面的就是实现系统可能有多个角度分类,每一种角度都可能变化,那么把这种多角度分类给分离出来让他们独立变化,减少他们之间耦合,桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意
原理类图
Client 类:桥接模式的调用者
Abstraction:抽象类,维护了 Implementor / 即它的实现类 ConcreteImplementorA..,二者是聚合关系,
Abstraction 充当桥接类
RefinedAbstraction : 是 Abstraction 抽象类的子类
Implementor : 行为实现类的接口
ConcreteImplementorA /B :行为的具体实现类
通过类图大家也可以看到,桥接模式把抽象与行为实现两个层分开了,来保持各部分的独立性以及他们的功能扩展
案例重构 - 桥接模式
根据桥接模式的思想,我们来画下使用桥接模式重构案例的类图
使用桥接模式重构案例代码如下:
//品牌
public interface Brand {
void open();
void close();
void work();
}
//华为
public class Huawei implements Brand{
@Override
public void open() {
System.out.println("华为电脑开机中");
}
@Override
public void close() {
System.out.println("华为电脑关机中");
}
@Override
public void work() {
System.out.println("华为电脑办公中");
}
}
//苹果
public class Apple implements Brand {
@Override
public void open() {
System.out.println("苹果电脑开机中");
}
@Override
public void close() {
System.out.println("苹果电脑关机中");
}
@Override
public void work() {
System.out.println("苹果电脑办公中");
}
}
//电脑
public class Computer {
//聚合品牌
private Brand brand;
public Computer(Brand brand) {
this.brand = brand;
}
public void open(){
this.brand.open();
}
public void close(){
this.brand.close();
}
public void work(){
this.brand.work();
}
}
//平板电脑
public class Ipad extends Computer{
public Ipad(Brand brand) {
super(brand);
}
public void open(){
super.open();
System.out.println("平板电脑");
}
public void close(){
super.close();
System.out.println("平板电脑");
}
public void work(){
super.work();
System.out.println("平板电脑");
}
}
//笔记本电脑
public class NoteBook extends Computer {
public NoteBook(Brand brand) {
super(brand);
}
public void open(){
super.open();
System.out.println("笔记本电脑");
}
public void close(){
super.close();
System.out.println("笔记本电脑");
}
public void work(){
super.work();
System.out.println("笔记本电脑");
}
}
//客户端
public class Client {
public static void main(String[] args) {
//获取笔记本电脑(类型加品牌)任意组合都可以
NoteBook noteBook = new NoteBook(new Huawei());
noteBook.open();
noteBook.close();
noteBook.work();
}
}
案例总结
使用桥接模式重构案例需求后,大家重点来分析,桥是这么搭起来的,首先 Ipad 的open() 方法其实调用的是父类Computer 的 open() 方法,而父类里面的 open() 方法其实是调用的是聚合的接口 Brand 的open() 方法,那实际调用的就是 Brand 接口的实现类 Apple/Huawei,所以类 Computer 其实充当了一个桥梁的作用, Ipad 的open()方法真正的实现在Brand 接口的实现类里
同样,我们再来考虑这样设计的扩展性问题:
如果我们再增加一个电脑类型,比如一体机(或者说再增加一个品牌小米),其实只需要增加一个类即可,如图
public class DeskPC extends Computer {
public DeskPC(Brand brand) {
super(brand);
}
public void open(){
super.open();
System.out.println("台式机");
}
public void close(){
super.close();
System.out.println("台式机");
}
public void work(){
super.work();
System.out.println("台式机");
}
}
你会发现,就已经拥有了各种品牌的台式机了,包括你新增加一个品牌,那该品牌的各种类型的你就拥有了,这就是桥接模式设计的精妙之处
注意事项
优势
分离抽象接口及其实现部分,提高了比继承更好的解决方案,可以减少子类的个数,降低系统的管理和维护成本
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
劣势
桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性
使用场景
对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
一个系统存在两个独立变化的维度,且这两个维度都需要进行扩展,桥接模式最合适不过
JDK源码分析
当然还是我们说的,思想是很相似的,但是并不是完全的按照桥接模式的类图严格实施
java.util.logging 是 JDK自带的日志包,可以将日志输出到文件、内存或者控制台,作用与我们常用的log4j类似
包内handler 可以从一个logger中取出信息并输出到控制台、文件或者调用其它api发送到网络中
Formatter支持格式化日志数据,通常每个Handler中都会有一个Formatter引用,Formatter可以将LogRecord对象转换为一个string字符串
Handler类和Formatter类在设计上利用了桥接模式,我们进入 Handler 源码进行分析
可以看到,Handler 和Formatter类是两个抽象类,它们可以分别独立的变化(有不同的子类)
Handler
Formatter
而Handler类中包含对Formatter类的引用
* @since 1.4
*/
public abstract class Handler {
private static final int offValue = Level.OFF.intValue();
private final LogManager manager = LogManager.getLogManager();
// We're using volatile here to avoid synchronizing getters, which
// would prevent other threads from calling isLoggable()
// while publish() is executing.
// On the other hand, setters will be synchronized to exclude concurrent
// execution with more complex methods, such as StreamHandler.publish().
// We wouldn't want 'level' to be changed by another thread in the middle
// of the execution of a 'publish' call.
private volatile Filter filter;
private volatile Formatter formatter;
private volatile Level logLevel = Level.ALL;
private volatile ErrorManager errorManager = new ErrorManager();
private volatile String encoding;
Handle通过setFormatter方法可以方便的替换不同的Formatter类
public synchronized void setFormatter(Formatter newFormatter) throws SecurityException {
checkPermission();
// Check for a null pointer:
newFormatter.getClass();
formatter = newFormatter;
}
二者类图如下图所示
下节预告
OK,到这里,桥接模式的相关内容就结束了,下一节,我们开启装饰者模式的学习,希望大家能够一起坚持下去,真正有所收获,就像开篇那句话,我走的很慢,但是我从来不后退,哈哈,那我们下期见~