- 1. 桥接模式
- 1.1 角色职责
- 1.2 使用场景
- 2 装饰器模式
- 2.1 例子
- 2.2 角色职责
- 2.3 框架中的使用
- 3.适配器模式
- 3.1 角色职责
- 3.2 例子
- 3.3 框架中的应用
- 4 门面模式
- 4.1 角色职责
- 4.2 例子
- 4.3 框架中的应用
- 5 组合模式
- 5.1 角色职责
- 5.2 例子
- 5.3 总结
- 6 享元模式
- 6.1 角色职责
- 6.2 例子
- 6.3 区别
- 6.4 框架中的应用
- 7 结构型设计模式总结
一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
“组合优于继承”设计原则,通过组合关系来替代继承关系,避免继承层次的指数级爆炸。
当一个类内部具备两种或多种变化维度时,使用桥接模式可以[解耦]这些变化的维度,使高层代码架构稳定。
桥接模式和策略模式区别
桥接模式强调的是抽象与实现的分离,策略模式强调的是算法的封装和相互替换,前者是结构模式,后者是行为模式。
1.1 角色职责观察上面的 UML 类图,我们可以发现,桥接模式分为以下四个角色:
| 角色 | 职责 |
|---|---|
| Abstraction | 定义抽象接口,拥有一个Implementor类型的对象引用 |
| RefinedAbstraction | 扩展Abstraction中的接口定义 |
| Implementor | 实现部分,可以为接口或者是抽象类,其方法不一定要与抽象部分中的一致,一般情况下是由实现部分提供基本的操作,而抽象部分定义的则是基于实现部分基本操作的业务方法 |
| 具体实现化角色 | 实现Implementor接口,给出具体实现 |
让手机既可以按照品牌分类,又可以按照软件类型分类。
1.2 使用场景 不同的品牌针对不同的软件有这不同的兼容支持,起初针对A品牌添加游戏功能。
public class ABrandGame {
public void run(){
System.out.println("A品牌游戏运行");
}
}
新增需求针对B品牌添加游戏功能,基于面向对象的理解,需要抽象一个接口,AB品牌均该接口,UML类图如下:
public interface BrandPhone {
void run();
}
添加一个通话功能,A、B品牌均需要,则意味着接口为品牌、其下有抽象子类A、B品牌,其下有对应的游戏、通讯功能。UML类图如下:
如此,添加更多的功能和更多的品牌,则类的扩展成倍的增长,因为此处使用继承这种强耦合的关系,在业务过于复杂的时候弊端明显,其实仔细分析该需求有两个维度的变化一种是品牌,一种是软件功能。使用桥接模式可以很好解决继承过深和强耦合关系。
桥接模式的使用
2 装饰器模式 装饰器模式对某一个事物进行装饰的一种模式。从代码层面而言,是对类的一个扩展或者是修饰,从传统方法而言,我们可以使用继承来对某一个类进行扩展,但是往往会导致会出现非常多的子类,如果我们要想避免这种情况,那么我们就可以使用设计模式中的——装饰器模式。
2.1 例子我们以女孩最爱喝的奶茶为例子,定义一个奶茶接口MilkyTea
public interface MilkyTea {
public void make();
}
实现椰香奶茶、珍珠奶茶、芋圆奶茶
public class CoconutMilkyTea implements MilkyTea{
//制作椰香奶茶
@Override
public void make(){
System.out.println("制作普通奶茶");
System.out.println("添加椰蓉");
}
}
public class PearlMilkyTea implements MilkyTea {
//制作珍珠奶茶
@Override
public void make() {
System.out.println("制作普通奶茶");
System.out.println("添加珍珠");
}
}
public class TaroBallsMilkyTea implements MilkyTea {
//制作芋圆奶茶
@Override
public void make() {
System.out.println("制作普通奶茶");
System.out.println("添加芋圆");
}
}
需求变动,除了普通类型的奶茶,为了健康增加半糖选项,为了应付高温增加冰奶茶,按照如上逻辑,上述的三种奶茶都需要派生出对应的半糖产品和冷饮产品,如此满足如上需求产品的类就会变成3*3的九个类
如果再增加更多的奶茶或者产品特性,天啊,类的数量会成倍增加。这时候装饰器模式就要起作用了。
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。其作用如下:
-
给原始类添加增强功能
-
可以对原始类嵌套使用多个装饰器
此处对业务进行重构,将上述的材料抽取处理珍珠、芋圆、椰蓉等’装饰’食材,提供最初的奶茶接口的实现,需要添加材料可以通过装配模式丰富功能。
实现接口
public class NormalMilkyTea implements MilkyTea {
@Override
public void make() {
System.out.println("制作普通奶茶");
}
}
装配类
//珍珠装饰类
public class PearlDecorator implements MilkyTea {
//持有一个需要增强的原始类
private MilkyTea milkyTea;
public PearlDecorator(MilkyTea milkyTea){
this.milkyTea = milkyTea;
}
@Override
public void make() {
//原始类功能
milkyTea.make();
//装饰增强
System.out.println("添加珍珠");
}
}
//其他装饰类如上所示
单元测试
@Test
public void test(){
//制作珍珠芋圆奶茶
//普通奶茶
MilkyTea milkyTea = new NormalMilkyTea();
//半糖装饰
SugarHalfDecorator sugarHalfMilyTea = new SugarHalfDecorator(milkyTea);
//珍珠装饰
PearlDecorator pearMilyTea = new PearlDecorator(sugarHalfMilyTea);
//芋圆装饰
TaroBallsDecorator taroBallsMilyTea = new TaroBallsDecorator(pearMilyTea);
//椰蓉装饰
CoconutDecorator coconutMilyTea = new CoconutDecorator(pearMilyTea);
//冷饮装饰
ColdDecorator coldMilyTea = new ColdDecorator(coconutMilyTea);
coldMilyTea.make();
}
使用装配模式后,可以灵活的配置制作各种奶茶品种了,而且对于新增奶茶材料只需要实现一个装饰类即可。
2.2 角色职责装饰器模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能(上述例子中因为只有一个接口所以没有抽象装饰角色)。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
java的IO流使用了装饰模式
装饰器和代理模式异同
- 同:都是进行功能的增强。
- 异:装饰器是对原始类相关功能进行增强而代理模式是对非相关功能的增强。
一般来说,适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。
3.1 角色职责相关角色
适配器模式(Adapter)包含以下主要角色。
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
应用场景:
- 外部系统接口的二次封装,抽象出符合我们业务需要的接口方法
- 同一功能点的多种实现框架的整合适配,比如多种日志系统的适配示例sl4j。
- 兼容老版本接口
- 适配不同格式的数据
适配器的两种实现
- 类适配器:主要使用方式是继承方式,Adapter(适配器)继承Adaptee(适配者类)
- 对适配器:对象适配器使用组合关系来实现,Adapter(适配器)持有Adaptee(适配者类)成员变量。
电动剃须刀、手机、电脑、平板这些家用电器需要不同电压的来进行充电,但是国家电网统一提供220V的电压,所以此处需要使用转接头进行处理。
目标接口
// 充电器接口
public interface Charger {
void charge();
}
适配者
//pc适配者
public class PcCharge {
public void chargePc(){
System.out.println("180V电压给电脑充电");
}
}
//手机适配者
public class PhoneCharge {
public void chargePhone(){
System.out.println("5V电压给手机充电");
}
}
适配器
//pc电源适配器(对象适配)
public class PcChargeAdaptor implements Charger {
//对象适配,以组合的形式持有适配者,Charger接口都需要被实现
private PcCharge pcCharge;
@Override
public void charge() {
System.out.println("220V电压转换为180V");
pcCharge.chargePc();
}
}
//pc电源适配器(类适配)继承形式拥有适配者方式,同名函数可以不用去实现
public class PhoneChargeAdaptor extends PhoneCharge implements Charger {
@Override
public void charge() {
System.out.println("220V电压转换为5V");
chargePhone();
}
}
UML类图
3.3 框架中的应用-
slf4j 为logback、log4j、apache log等提供适配
-
为了兼容1.0时期的Enumeration,Collections类中使用适配器模式,兼容新老版本。这里用的是对象适配模式,用iterator代替Enumeration
门面模式,也叫外观模式,英文全称是 Facade Design Pattern。门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。说白了门面为了简化接口细粒度情况下的接口调用的复杂度。
作用
- 解决易用性问题,隐藏底层细节,降低复杂性,提供简单易用的高层接口组装。
| 角色 | 职责 |
|---|---|
| 外观角色(Facade) | 也称门面角色,系统对外的统一接口 |
| 子系统角色(SubSystem) | 可以同时有一个或多个SubSystem |
本例子以饭店提供服务为例,客户点餐不需要主动去和厨房的厨师说明,只需和饭店的服务员(门面角色)说明自己的诉求然后等待即可,不需要关注厨师、食材坐等自己的餐品端到自己面前即可
chef接口及实现
public interface Chef {
void cook();
}
//冷菜师
public class ColdDishChef implements Chef {
@Override
public void cook() {
System.out.print(" 制作凉拌黄瓜 ");
}
}
//面点师
public class PastryChef implements Chef {
@Override
public void cook() {
System.out.print(" 制作华夫饼 ");
}
}
//热炒师
public class StirFryChef implements Chef {
@Override
public void cook() {
System.out.print(" 制作烤全羊 ");
}
}
waiter 门面
public class Waiter {
private ColdDishChef coldDishChef = new ColdDishChef();
private StirFryChef stirFryChef = new StirFryChef();
private PastryChef pastryChef = new PastryChef();
//套餐A 凉拌黄瓜 烤全羊
public void foodMenuA(){
System.out.print("套餐A:");
coldDishChef.cook();
stirFryChef.cook();
System.out.println();
}
//套餐B 华夫饼 烤全羊
public void foodMenuB(){
System.out.print("套餐B:");
pastryChef.cook();
stirFryChef.cook();
System.out.println();
}
//套餐B 华夫饼 烤全羊 凉拌黄瓜
public void foodMenuC(){
System.out.print("套餐C:");
coldDishChef.cook();
pastryChef.cook();
stirFryChef.cook();
System.out.println();
}
}
4.3 框架中的应用
5 组合模式
5.1 角色职责
将一组对象组织(Compose)成树形结构,以表示一种“部分-整
体”的层次结构。组合让客户端可以统一单个对象和组合对象的处理逻辑。
比如文件系统中的文件(单一对象)和目录(组合对象),组织架构的子部门(组合对象)和员工(单一对象)。
| 角色 | 职责 |
|---|---|
| 抽象组件(Component)角色 | 为组合对象和叶子对象声明公共的接口,让客户端可以通过这个接口来访问和管理整个对象树,并可以为这些定义的接口提供缺省的实现。 |
| 组合对象(Composite)角色 | 通常会存储子组件(组合对象、叶子对象),定义包含子组件的那些组件的行为,并实现在抽象组件中定义的与子组件有关的操作,例如子组件的添加(addChild)和删除(removeChild)等。 |
| 叶子对象(Leaf)角色 | 定义和实现叶子对象的行为,并且它不再包含其他的子节点对象 |
| 客户端(Client)角色 | 通过Component接口来统一操作组合对象和叶子对象,以创建出整个对象树结构。 |
实现整个公司的人员架构图(部门、员工的隶属关系),并且提供接口计算出部门的薪资成本。
部门和员工抽象组件
public abstract class Component {
protected long id;
protected double salary;
public Component(long id){
this.id = id;
}
public Component(long id,Double salary){
this.id = id;
this.salary = salary;
}
abstract Double calcSalary();
}
部门
public class Depart extends Component {
private List staffList = new ArrayList<>();
public Depart(long id){
super(id);
}
public Depart(long id,List staffList){
super(id);
this.staffList.addAll(staffList);
}
public void addStaff(Component staff){
this.staffList.add(staff);
}
public void addStaffList(List staffList){
this.staffList.addAll(staffList);
}
@Override
public Double calcSalary() {
Double totalSalary = 0D;
this.salary = totalSalary;
if(CollectionUtils.isEmpty(staffList)){
return totalSalary;
}
for(Component staff:staffList){
totalSalary +=staff.calcSalary();
}
this.salary = totalSalary;
return totalSalary;
}
}
员工
//员工
public class Staff extends Component {
public Staff(long id,Double salary) {
super(id,salary);
}
@Override
public Double calcSalary() {
return this.salary;
}
}
5.3 总结
组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
对于相同的对象可以在内存中保留一份实例多出使用,对于相似对象也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。
6 享元模式 6.1 角色职责 享元模式对于可能出现的大量重复对象,将其不可变的属性抽取出现形成一个不可变的对象即为享元对象。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
| 角色 | 职责 |
|---|---|
| 抽象享元⾓⾊(Flyweight) | 是所有的具体享元类的基类,为具体享元规范需要实现的公共接⼝,⾮享元的外部状态以参数的形式通过⽅法传⼊ |
| 具体享元(Concrete Flyweight)⾓⾊ | 实现抽象享元⾓⾊中所规定的接⼝。 |
| ⾮享元(Unsharable Flyweight)⾓⾊ | 是不可以共享的外部状态,一般情况与具体享元以组合的形式关联(成员变量或者方法形参) |
| 享元⼯⼚(Flyweight Factory)⾓⾊ | 负责创建和管理享元⾓⾊。当客户对象请求⼀个享元对象时,享元⼯⼚检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建⼀个新的享元对象。 |
我们以一个大型象棋为例,游戏有成千上万个房间,每一个房间都有一个棋局,每个棋局有若干棋子,如果创建这些对象会导致程序出现庞大的相似对象,但是分析发现每个棋子的颜色(红方、黑方)、文字(车,炮,相)是一致的,那么我们可以将其抽取出来创建一个享元对象。
享元对象
public class ChessPieceUnit {
//唯一标识
private Integer id;
//文字 车马炮
private String text;
//红方、黑方
private String color;
public ChessPieceUnit(Integer id, String text, String color) {
this.id = id;
this.text = text;
this.color = color;
}
享元对象工厂
public class ChessPieceUnitFactory {
private static Map chessPieces = new HashMap<>();
static{
chessPieces.put(1,new ChessPieceUnit(1,"车","红"));
chessPieces.put(1,new ChessPieceUnit(1,"马","黑"));
//...省略其他棋子
}
public static ChessPieceUnit getChessUnit(Integer chessPieceId){
ChessPieceUnit chessPieceUnit = chessPieces.get(chessPieceId);
return chessPieceUnit;
}
}
非享元对象
public class ChessPiece {
//享元对象
private ChessPieceUnit chessPieceUnit;
//棋子所在x轴坐标
private Double positionX;
//棋子所在y轴坐标
private Double positionY;
public ChessPiece(ChessPieceUnit chessPieceUnit, Double positionX, Double positionY) {
this.chessPieceUnit = chessPieceUnit;
this.positionX = positionX;
this.positionY = positionY;
}
6.3 区别
享元与单例对象的区别
单例对象是一个类只能创建一个对象,享元对象是可以创建多个,享元对象可以看做单例对象的变种:多例对象(有限)对象的复用。
享元与对象池的区别
都是对对象的复用,但是对象池更侧重于使用者独占该对象,使用完成后放回池中再供其他对象使用。而享元模式侧重于多个使用者共同使用
6.4 框架中的应用 jdk中的Integer,Long中使用享元模式缓存一部分数据。
jdk中字符串常量池
7 结构型设计模式总结| 设计模式 | 作用 |
|---|---|
| 代理模式 | 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。 |
| 桥接模式 | 桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。 |
| 适配器模式 | 适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口 |
| 装饰器模式 | 装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。 |
| 门面模式 | 简化接口细粒度情况下的接口调用的复杂度 |
| 组合模式 | 更像是对业务场景的一种数据结构和算法的抽象 |
| 享元模式 | “享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存 |



