前言:上篇总结了创建型模型,这周来总结下结构型模式,如果说创建型模式是用来处理和优化对象的创建和复用的问题的话,那么结构型模式则是用于通过继承、合成/聚合等方式处理类与对象,达到能更加适应'业务需求的变化和扩展'的结果
。另外,从手段而言,类与类的结构上合成/聚合是要优于类的继承的方式的,其可以在运行时改变组合对象的关系,更加灵活(桥接模式)。
结构型模型包括
1.代理模式
2.桥接模式
3.外观模式
4.适配器模式
5.组合模式
6.享元模式
7.装饰模式
代理模式
1.定义:为其他对象提供一种代理以控制对这个对象的访问。
2.优缺点:隔离实际调用对象,同名处理方法中可以加入其他细节而不用修改实际输出的类,缺点是增加代理类。
3.总结:比较常用的设计模式,分为静态和动态代理。
静态代理:代理类和被代理类实现相同的接口,即:含有相同的方法A,B,C,。。。。
代理类持有被代理类的引用,在代理类的方法X中调用被代理类.X();
动态代理(示例):
//1.定义接口
/**
* 作者:wl on 2017/11/27 17:15
* 邮箱:[email protected]
*/
//基本需求
public interface BaseNeed {
String rentHouse();
String rentCar();
void learnKnowledge();
}
//2.定义接口实现类
public class Worker implements BaseNeed {
@Override
public String rentHouse() {
Log.d("test","打工者需要租房");
return "打工者需要租房";
}
@Override
public String rentCar() {
Log.d("test","打工者也要成为司机");
return "打工者也要成为司机";
}
@Override
public void learnKnowledge() {
Log.d("test","打工者也要充电");
}
}
//3.定义动态代理的'请求处理类' MyInvocationHandle
public class MyInvocationHandle implements InvocationHandler {
private T target;
public MyInvocationHandle(Class clazz) {
super();
try {
target = (T) clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Log.d("test", "开始调用代理");
if(method.getName().equals("learnKnowledge")){
Log.d("test", "对于学习方法注入扩展");
}
Object test = method.invoke(target, args);
// Log.d("test", "结束调用代理");
return test;
}
public T getProxy() {
//return (T) Proxy.newProxyInstance(BaseNeed.class.getClassLoader(), new Class[]{BaseNeed.class}, this);
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
//4.外部调用
MyInvocationHandle myInvocationHandle=new MyInvocationHandle(Worker.class);
BaseNeed proxy= myInvocationHandle.getProxy();
proxy.rentHouse();
proxy.rentCar();
proxy.learnKnowledge();
//日志输出:
11-28 11:14:32.289 18103-18103/XXX D/test: 打工者需要租房
11-28 11:14:32.289 18103-18103/XXX D/test: 打工者也要成为司机
11-28 11:14:32.290 18103-18103/XXX D/test: 对于学习方法注入扩展
11-28 11:14:32.290 18103-18103/XXX D/test: 打工者也要充电
原理简单描述:Proxy在运行时生成代理类$Proxy11
public final class $Proxy11 extends Proxy
这个类在静态代码块中通过反射等技术初始化实现了 接口 中定义的所有方法:如
m3 = Class.forName("dynamic.proxy.BaseNeed").getMethod("rentHouse", new Class[0]);
如:
public final void rentHouse()
{
try
{
// 实际上就是调用
//MyInvocationHandler的
//public Object invoke(Object proxy, Method method, Object[] args)方法
//所以无论外部调用那个方法,都会走MyInvocationHandle中的invoke
super.h.invoke(this, m3, null);
return;
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
总结:动态代理能在运行时动态的生成代理类,当接口方法很多时,使用静态代理的话代码量很大,扩展起来也比较繁琐,而动态代理在内存中自动生成代理类,很好的解决了这个问题。缺点是代理的对象必须实现接口(cglib这好玩意儿解决了这个问题),retrofit就是采用动态代理来实现的(用动态代理,将API接口类中的所有请求在invoke中统一转化成okhttpcall或者observerable)
对比JDK的动态代理和cglib的动态代理:
JDK的动态代理
的原理是通过反射拿到接口中所有的方法,然后通过二进制流的方式生成代理类class,并且代理类的实现要继承Proxy类,又不能多继承,所以必须要有接口。
CGLib
采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。(cglib部分参考的连接)
JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
最后送上一篇风趣幽默的文章:Java帝国-动态代理
装饰模式
1.定义:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
2.心得分析:装饰模式在结构上几乎和代理一样,装饰类跟被装饰类实现同一个接口,然后装饰类持有被装饰类的引用,在内部去 增强
被装饰类的同名方法。
注意这里我用了一个词 增强
,这也正是装饰和代理的最大区别:
1.装饰模式的目的是去 增强、扩大、修饰 被装饰对象的操作,而代理模式的侧重点是去控制
对 被代理类 的访问。
2.一般不会再弄个代理类来控制原先的代理,而装饰的话经常会存在嵌套的情况,即一层装饰一层。
外观模式
1.定义:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
2.优缺点:对客户程序隐藏子系统细节,解耦客户和子系统,同时使得整个系统封装的更易于调用。另一方面外观类本身需要提供不同api接口,并且外观类无法遵循开闭原则,需求变化-可能需要直接修改外观类。
3.总结:以前在做'风跑app'的时候地图定位等模块完全依赖于高德地图,高德地图api就相当于是子系统,我自己弄了
个MapHelper类,里面就是封装了用高德api画线,画点等方法~~当时都不知道有这个设计模式,但确实就是这么用的,可见这个 外观 模式的使用很广泛,很常见!
适配器模式
1.定义:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
2.总结:
对象适配器
:跟静态代理很像,需要适配的类Adaptee,适配器类Adapter,期望的接口Target内部要实现的方法为A。
使用过程就是 适配器类Adapter实现Target接口中的方法A,具体的实现方式就是自己持有需要适配的类Adaptee的引用,然后调用Adaptee的那个不兼容的方法。 这TM跟代理类持有被代理类的引用,然后调用被代理类的方法去实现一模一样,区别就是适配器模式要求松一些,Adaptee无需继承Target。
类适配器
:与对象适配器不同,适配器类Adapter直接继承需要适配的类Adaptee。相比较于对象适配器
模式而言缺点是会暴露出Adaptee中的方法,对使用者并不友好(再次证明:合成/聚合>继承)
3.优缺点:
扩展性不错,能复用系统现有的轮子。
过多的使用适配器会使系统凌乱,不易整体把握。如命名调用的是A,内部确是适配成调用B
所以说这玩意儿不是很必要就不要用,优先重构!
组合模式
1.定义:将对象组合成树形结构以表示‘部分’-‘整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
2.总结:
透明方式:抽象的接口中包含管理子类的方法,这样叶子类和枝节类都会实现对应的方法,只不过叶子类没有下线,所以方法实现都为空。
安全方式:相反,管理子类的方法放到了枝节类,叶子类更干净
这个模式的理解和应用自我感觉还不到位,不写了,暂时放放
享元模式
1.定义:运用共享技术有效地支持大量细粒度的对象。
2.示例:俄罗斯方块的生产
public abstract class Shape{ //形状
public void doRotate(int degree); //名为‘旋转 ’的方法
}
public class Tetris extend Shape{ //俄罗斯方块
private String shapeType;
public Tertis (String type){ //构造器传入 形状的类型 如:7型,I型,田型,山型。
shapeType=type;
}
public void doRotate(int degree){
Toast("将 shapeType 形状的方块旋转 degree 度)
}
}
public class TetrisFactory { //俄罗斯方块制造工厂
private map realTetris=new HashMap<>(); //缓存不同类型的俄罗斯方块实体
public Tetris getTetrisInstance(String type) {
if(realTetris.get(type)==null){
realTetris.put(type,new Tetris(type));
}
return realTetris.get(type);
}
}
//模拟俄罗斯方块的产生过程:
int[ ] degrees=new int[]{90,180,270};
String[ ] types=new String[]{"7","山","口","田"};
TetrisFactory factory=new TetrisFactory ();
Tetris item=null;
for(int i=0;i<10000;i++){
int degree=degrees[new Random().nextInt(2)];
String type=types[new Random().nextInt(3)];
item=factory.getTetrisInstance();
item.doRotate(degree);
}
代码简单明了,循环体中产生10000个俄罗斯方块,并且方块的形状和角度随机,如果不用享元,则实例化10000次,利用享元模式,实例化仅仅4次,并且将 角度 这一变化的因素抽离出去,作为外部参数传入。
这个模式的核心就是将状态外部化,通过读取外部化参数来复用缓存的实例。
桥接模式
1.定义:将抽象部分与它的实现部分分离,使它们可以独立的变化。
(定义不太好理解,实际上核心就是:一个类存在两个独立变化的维度,且这两个维度都要扩展,那么将其中一个维度抽象出来,通过合成或者聚合的方式关联到另一维度)
2.举例:电脑的品牌和电脑上的USB插口
品牌有 联想、惠普、方正等
USB接口有 2.0 3.0 等
如果这两个变量都通过
继承
的方式来实现的话:
类的继承关系如图,看似很清晰,可是如果以后科技发展,USB出了4.0 5.0 ,同时又有更多的电脑品牌诞生时,通过这种继承的方式实现的代码改起来可以说灰常痛苦,每增加一种USB,需要为所有品牌去添加一个类,显然是不科学的,问题的关键就在处理这两个变化维度
的方式上!即:对 品牌 和接口类型 都是通过 继承 去实现的
如果我们使用桥接模式,通过聚合或者合成
的方式 将变化维度中的 USB接口隔离, 实现如图:
此时将变化的维度USB接口类型隔离开,跟电脑形成"合成"的关系,而不再是继承,好处在哪里呢?
新增品牌,OK,和接口没半毛钱关系,new 新品牌类(Usb接口)的时候传入USB接口类型就行。
同样 新增USB4.0, 制造新电脑的时候传入 new USB4.0 这个构造就行,不用去动 具体电脑品牌类的代码,完全符合开放闭合原则!
3.总结:考虑类与类之间关系的构建时:聚合和合成
是优先于继承
的,对于桥接模式,强调的就是 一个类,当他的变化是多维度的时候,将其他维度抽离出去,让它们单独演变,然后通过聚合或者合成的方式关联回这个类。