结构型模式——将对象结合在一起,将实现和抽象类联系起来
整个代码和文档是跟着Bilibili up主:黑手书生 的系列视频——设计模式总结的
学习地址:https://space.bilibili.com/505571900?from=search&seid=10016231603740656139
视频中的代码地址:https://github.com/OAyUliko/JAVA_Design_pattern
总述
- 包括适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式和代理模式
- 描述了如何将类或对象结合在一起形成更大的结构
- 实例:
例子 | 模式 |
---|---|
每一个数据库引擎的JDBC驱动都是介于JDBC接口和数据库接口的 | 适配器 |
①适配器模式 Adapter Pattern
- 动机:将一个类的 接口 转换成客户希望的 另一个接口,使得原本由于接口 不兼容 而不能一起工作的类可以一起工作(绝对是 针对接口 设计)
- 类结构型的适配器:①适配器 继承 老系统,在其中 调用 老系统的 方法 供新系统接口使用
- 对象结构型的适配器:①适配器 关联 老系统,老系统对象 作为 类对象 放入,由类对象去 调用 老系统的方法
②优点:适配的范围更广(因为是关联,非继承,继承的话只能继承一个类)
- 适配器包装的是 适配者Adaptee(被适配的类)
- 适配器 是没有实际工作的,只是起转换作用,整合运算
- 优点:①将目标类和适配器类 解耦 ,通过适配器来 重用
②增加了类的 透明性 和 复用性,对客户端是透明的,灵活性 和 扩展性 好
③通过 配置文件 可以更改适配器,符合“开闭原则”,符合“合成复用原则” - 缺点:①过多使用适配器会让系统零乱,不易进行整体把握。明明调用的 A 接口,内部被适配成了 B 接口,因此如果不是很有必要,不使用适配器,而是对系统进行重构
- 适用场景:①系统需要使用现有的类,但发现接口不符合系统需要
②想要建立一个重复使用的类,用于与彼此之间无太大关联的类工作 - 类图:
②桥接模式 Bridge Pattern
- 动机:把 抽象 与 实现 解耦,二者可以独立变化,用 组合关系 代替了继承关系
- 抽象接口和抽象类,抽象接口类中有抽象的 功能方法 ,抽象类中有 protected 的 接口对象 ,用这个接口对象去调用与接口关联的类内部的 详细功能 方法
- 注意是扩展维度而非扩展功能!!
//客户端代码
//这里和下一行最好是用配置文件XMLUtil,实现客户端的透明
Color color= new Red(); //找颜料
Pen pen=new BigPen(); //找笔
pen.setColor(color); //笔沾上颜料
pen.draw("Meiko"); //笔画什么
//抽象化角色Pen类
public void setColor(Color color) {this.color = color;}
protected Color color; //一定要有抽象接口的类对象
public abstract void draw(String Name);
- 优点:①抽象和实现的分离
②扩展力强,支持“开闭原则”,“合成复用原则”
③实现细节对客户 透明,通过配置文件改,完全不用改内部代码 - 缺点:①增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计
- 适用场景:①有 两个维度 在同时变化,但又想糅合在一起
- 类图:
③组合模式 Composite Pattern
- 动机:把一组相似的对象当作一个单一的对象。依据 树形 结构来组合对象,用来表示 “整体-部分” 层次。使得用户对 单个对象(叶子对象)和 组合对象(容器对象)的使用具有一致性
- 角色:抽象构件(描述叶子和容器的共同特征)、叶子构件、容器构件(可以叶子套叶子,也可以容器套容器)
//客户端代码
MyElement e1, e2, e3;
Plate p1, p2;
e1 = new Apple();
e2 = new Orange();
p1 = new Plate();
p1.Add(e1); //大盘子里装 一个苹果
p1.Add(e2); //一个橘子
p2 = new Plate();
p1.Add(p2); //一个盘子
e3 = new Apple();
p2.Add(e3); //盘子里再装一个苹果
p1.eat(); //吃整个盘子可以吃到??? 苹果 橘子 苹果
//盘子里的递归调用 针对arrrylist里的每个元素 叶子结点就“吃”,盘子就递归调用eat
void eat() {
for(Object object:arrayList)
((MyElement)object).eat();
}
- 透明组合模式:①程序员不用去关心对叶子还是容器操作,达到透明
- 对象结构型 的适配器:①不会发生误调用
- 优点:①清楚定义分层次的复杂对象,增加新构件更容易了,结点自由增加
②客户端调用简单,一致地使用叶子或容器 - 缺点:①设计变的抽象,不是所有的方法都与叶子有关联
②增加新构件可能会产生问题,很难对容器中的构件类型进行限制
③递归要是不当,会造成时间和空间的消耗
④叶子和容器是实现类,而不是接口,违反了“依赖倒置原则” - 适用场景:①可以忽略整体与部分的差异,一致对待,无需关心层次细节
②对象的结构是动态的但复杂程度不一样
④装饰模式 Decorator Pattern
- 动机:以 透明 的方式 动态地 给一个对象增加 额外的职责 ,又不改变其结构,这比生成它的子类 更灵活
- 这种模式创建了一个装饰类来 包裹住 原有的类,并在保持类方法签名完整性的前提下,提供额外的功能
//客户端代码
BirthdayCake birthdayCake = new Cake(); //创建一个蛋糕胚
birthdayCake.show();
Cream cream=new Cream(birthdayCake); //蛋糕上加奶油
cream.PutCream();
Fruit fruit=new Fruit(cream); //在 已经加上奶油的蛋糕胚 上加水果
fruit.PutFruit();
//抽象装饰类中的重要代码
private BirthdayCake birthdayCake; //然后在构造函数里加入进去
//加工类中要继承父类的接口类对象
public Fruit(BirthdayCake birthdayCake) { super(birthdayCake); }
- 角色:抽象构件、具体构件(被包装者)、抽象装饰、具体装饰(谁来包装,里面有具体的加工方法,加工次序可以更改)
- 优点:①装饰模式可以提供比继承更多的 灵活性,而且可以通过 配置文件 选择不同的 装饰器,符合“合成复用原则”
②装饰类和构件类可以独立发展,不相互影响,可以创造不同行为的组合,符合“开闭原则” - 缺点:①产生很多小对象,很多具体装饰类(比如加水果时要用到奶油对象),系统 复杂度 增加
②排错困难,需要逐级排查,难度增加 - 适用场景:①动态,透明 增加 单个对象的功能,动态 撤销
②不能采用继承方式或继承不利于扩展和维护时 - 类图:
⑤外观模式 Facade Pattern
- 动机:把子系统中的一组接口用 统一的外观对象 包裹,它定义了一个 高层接口 来简化使用
- 外观对象本身是不提供子系统中要完成的 功能 ,只提供 交互,即用户只需跟外观角色交互,降低 系统 耦合
//客户端代码
GeneralSwitchFacade gsf =new GeneralSwitchFacade();//相当于总面板
gsf.on();
System.out.println("----------------------");
gsf.off();
//外观器代码
private Light light[]=new Light[4];
private AirConditioner ac;
private Television tv;
public GeneralSwitchFacade() { ...全部对象加入 }
public void on() { light[0].on();...... }
//其实是通过小对象去调用对象内部的具体功能方法
}
- 角色:外观角色(沟通通道)、子系统角色(提供实际功能)
- 优点:①将系统划分为若干个子系统来 降低复杂度,引入外观对象作为访问子系统的入口,符合“单一职责原则”
②引入外观类降低客户类和子系统类的耦合,屏蔽 了 子系统 组件,打破了二者的直接连接,符合“迪米特原则”
③子系统 组件变化 不会影响到客户类,只需调整外观类
④降低了大型软件系统中的 编译依赖性 ,简化了不同平台的移植过程 - 缺点:①不能很好限制客户使用子类(可以new一个子类来调用子类功能)
②增加 新的 子系统 要修改外观类和客户类代码,不符合“开闭原则”(可通过引入抽象外观类解决) - 适用场景:①为一个复杂系统提供接口,该接口可以满足多数用户需求,用户也可以跨过外观类直接访问子系统
②将客户程序和子系统解耦,需要提高子系统的独立性和可移植性 - 一般将外观对象设为 单例模式 ,只能有一个;不要企图继承外观类扩展它的功能,重点在让各子系统完成工作
- 类图:
⑥享元模式 Flyweight Pattern
- 动机:运用 共享 技术有效地支持 大量细粒度 的对象(大多为相似的,状态变化很小的对象)的 复用 ,实现相同或相似对象的重用
- 内部状态:可以共享的相同内容(连接池的关键字)
外部状态:需要外部环境来设置的,不能共享的内容(连接的ip,端口)
//客户端代码
Random rm = new Random();
PieceFactroy pieceFactroy = new PieceFactroy();//生产棋子的工厂
Piece piece;//棋子对象
for(int i=0;i<19;i++)//按棋盘大小随机落子
for(int j=0;i<19;i++)
{
if(rm.nextInt()%2==0)
piece=pieceFactroy.GetPiece("黑");//随机数产生黑白棋子
...
piece.Play(rm.nextInt(19),rm.nextInt(19));//随机放置
}
System.out.println(pieceFactroy.GetPieceCount());//其实系统中只有两个对象,黑子和白子,都是共享这两个对象
//关键代码 PieceFactroy类
public Piece GetPiece(String key) {
if (hashMap.containsKey(key)) //是否有黑白子对象,有的话在map里按key返回,没有的话创建一个再返回,其实都是返回的已有的对象
return (Piece) hashMap.get(key);
else {
Piece piece = new OnePiece(key);
hashMap.put(key, piece);
return piece;
}}
- 角色:抽象享元类、具体享元类(被享元工厂管着,如果没有就new一个具体享元对象)、非共享具体享元类(不适合状态交换的东西,类似账号和密码)、享元工厂类
- 享元模式通常会出现 工厂模式 ,需要创建一个 享元工厂 来维护享元池,用于存储具有相同内部状态的对象
- 优点:① 减少 对象的创建,节约内存占用,提高效率
- 缺点:①需要分离内外部状态,提高了 复杂度
- 适用场景:①系统有大量相似对象,对象消耗大量内存时
②需要缓冲池的场景 - 类图:
- 与原型模式的区别:原型模式返回的是克隆出的对象,享元模式直接返回了对象本身
⑦代理模式 Proxy Pattern
- 动机:给某一个对象提供一个代理,用代理来 实现 对真实对象的 操作 或将代理作为原对象的 替身。因为有时直接访问对象会带来麻烦
- 静态代理:代理类和被代理类都 继承 自同一接口,用户在使用时,以为来自于被代理类(远程的类);靠代理类去show
//静态代理的客户端
LocalPic localPic=new LocalPic(); //新建本地对象
localPic.show("Meiko照片"); //看似是调用本地图片,其实本地对象在背后调用远程图片类,加载图片
//关键代码 LocalPic类
iShowPic=new RemotePic(); //通过接口对象去真正完成远程代理(因为远程图片继承了接口)
System.out.println("准备载入图片"+picName);
Thread thread=new Thread(this); //把自己放入线程
thread.start();//启动线程
...
iShowPic.show(picName);
- 动态代理:代理类不知道被代理类是什么形态,需要 动态创建 代理类,得益于反射机制;靠接口类对象去show
//动态代理的客户端
IShowPic iShowPic=new RemotePic(); //得到远程的对象
LocalPic localPic= new LocalPic(iShowPic); //得到本地的对象,把远程对象放进来
IShowPic LP= (IShowPic) localPic.GetProxyInstance(); //把远程对象实例化,通过实例化了的远程对象来加载图片
LP.show("Meiko"); //接口对象去show!!
//关键代码 LocalPic类
public Object GetProxyInstance()
{ //参数:被代理类的名字列表,被代理的方法,方法的参数数组
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
//调用了被代理类里面的方法, Method method是接口方法 , Object[] args是参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("准备载入图片"+args[0]);//args是名字,只有一个参数
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
method.invoke(target,args); //远程图片调用,Method method就是远程的方法
}});
thread.start();
}});}
- 优点:①协调代理类和被代理类,降低耦合度
②远程代理:客户端可以访问在远程机器上的对象,可以快速响应处理客户端对象
虚拟代理:减少系统资源消耗,优化提升 - 缺点:①有些类型的代理模式可能会造成请求的处理速度变慢
②有些代理模式的实现非常复杂