1. 适配器模式(Adapter)
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。
主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
类的适配器模式
类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
将 USB 接口转为 VGA 接口:
public interface Usb {
void playMKV();
}
public class UsbImpl implements Usb {
@Override
public void playMKV() {
System.out.println("播放视频内容");
}
}
Usb2VgaAdapter 首先继承 USBImpl 获取 USB 的功能,其次,实现 VGA 接口,表示该类的类型为 VGA:
public interface Vga {
void projection();
}
public class Usb2VgaAdapter extends UsbImpl implements Vga {
@Override
public void projection() {
super.playMKV();
}
}
Projector 将 USB 映射为 VGA,只有 VGA 接口才可以连接上投影仪进行投影:
public class Projector {
public void projection(T t) {
if (t instanceof Vga) {
System.out.println("开始投影");
Vga v = (Vga) t;
v.projection();
} else {
System.out.println("接口不匹配,无法投影");
}
}
}
测试,只有 VGA 接口才能投影播放内容:
public class MainTest {
public static void main(String[] args) {
Projector projector = new Projector();
Usb usb = new UsbImpl();
projector.projection(usb);
System.out.println("=================");
Vga vga = new Usb2VgaAdapter();
projector.projection(vga);
}
}
接口不匹配,无法投影
=================
开始投影
播放视频内容
对象的适配器模式
对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个 Wrapper 类,持有原类的一个实例,在 Wrapper 类的方法中,调用实例的方法就行。
对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承。
public class Usb2VgaAdapter extends UsbImpl implements Vga {} // 原先的代码
public class Usb2VgaWrapper implements Vga {
Usb usb = new UsbImpl();
@Override
public void projection() {
usb.playMKV();
}
}
能够得到一样的测试结果:
接口不匹配,无法投影
=================
开始投影
播放视频内容
接口的适配器模式
接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类 Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。
VGA 接口中增加一些别的方法:
public interface Vga {
void projection();
void m();
void n();
}
所有继承者都要实现 m() 和 n() 方法,我们可以引入中间层 Wrapper,让真正的实现类选择性实现 VGA 接口:
public abstract class Usb2VgaWrapper implements Vga {
Usb usb = new UsbImpl();
@Override
public void projection() {
usb.playMKV();
}
@Override
public void m() {
}
@Override
public void n() {
}
}
public class Usb2VgaAdapter extends Usb2VgaWrapper {
@Override
public void projection() {
super.projection();
}
@Override
public void m() {
super.m();
}
}
能够得到一样的测试结果:
接口不匹配,无法投影
=================
开始投影
播放视频内容
根据合成复用原则,组合大于继承。因此,类的适配器模式应该少用。
2. 装饰者模式(Decorator)
装饰者模式动态地将责任附加到对象上。若要扩展功能,装置者提供了比继承更有弹性的替代方案。
主要实现方式:
- Decorator 继承了 Component ,附加 Decorator 的 Component 本制还是一种 Component ,后续可进一步对其修饰;
- Decorator 关联了 Component ,便于对其附加行为。
咖啡店有多种口味的咖啡:
public abstract class Drink {
private BigDecimal price;
public String description = "";
public abstract BigDecimal cost();
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class Coffee extends Drink {
@Override
public BigDecimal cost() {
return super.getPrice();
}
}
public class Espresso extends Coffee {
public Espresso(BigDecimal price) {
this.setPrice(price);
this.setDescription(this.getClass().getSimpleName());
}
}
public class LongBlack extends Coffee {
public LongBlack(BigDecimal price) {
this.setPrice(price);
this.setDescription(this.getClass().getSimpleName());
}
}
装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。
咖啡除了原味咖啡,还可以选择加牛奶,加糖:
public class CoffeeDecorator extends Drink {
private Drink drink;
public CoffeeDecorator(Drink drink) {
this.drink = drink;
}
@Override
public BigDecimal cost() {
return super.getPrice().add(drink.cost());
}
@Override
public String getDescription() {
return drink.getDescription() + " && " + super.getDescription();
}
}
public class Milk extends CoffeeDecorator {
public Milk(Drink drink) {
super(drink);
this.setPrice(new BigDecimal("0.20"));
this.setDescription(this.getClass().getSimpleName());
}
}
public class Sugar extends CoffeeDecorator {
public Sugar(Drink drink) {
super(drink);
this.setPrice(new BigDecimal("0.10"));
this.setDescription(this.getClass().getSimpleName());
}
}
我们来实现一个测试类:
public class MainTest {
public static void main(String[] args) {
Drink order1 = new Espresso(new BigDecimal("10.00"));
System.out.println("Order1 price: " + order1.cost());
System.out.println("Order1 description: " + order1.getDescription());
System.out.println("****************");
Drink order2 = new LongBlack(new BigDecimal("20.00"));
order2 = new Milk(order2);
order2 = new Sugar(order2);
System.out.println("Order2 price: " + order2.cost());
System.out.println("Order2 description: " + order2.getDescription());
}
}
Order1 price: 10.00
Order1 description: Espresso
****************
Order2 price: 20.30
Order2 description: LongBlack && Milk && Sugar
在这里应用继承并不是实现方法的复制,而是实现类型的匹配。因为装饰者和被装饰者是同一个类型,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。根据装饰者模式的理念,我们可以在任何时候,实现新的装饰者增加新的行为。
3. 代理模式(Proxy)
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
开闭原则:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。
代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是通过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目 加入缓存、日志这些功能 ,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
静态代理
静态代理虽然可以做到在符合开闭原则的情况下对目标对象进行功能扩展,但是 代理对象与目标对象要实现相同的接口,我们得为每一个服务都得创建代理类,工作量太大不易管理,同时接口一旦发生改变,代理类也得相应修改。
public interface BuyHouse {
void buyHouse();
}
public class BuyHouseImpl implements BuyHouse {
@Override
public void buyHouse() {
System.out.println("买房的交易过程");
}
}
public class BuyHouseProxy implements BuyHouse {
private final BuyHouse buyHouse;
public BuyHouseProxy(final BuyHouse buyHouse) {
this.buyHouse = buyHouse;
}
@Override
public void buyHouse() {
System.out.println("买房前中介处理");
buyHouse.buyHouse();
System.out.println("买房后中介处理");
}
}
动态代理(JDK)
JDK 动态代理的代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)。代理类不用再实现接口了,但是要求被代理对象必须有接口。
public interface BuyHouseInterface {
void buyHouse();
}
public class BuyHouseService implements BuyHouseInterface {
@Override
public void buyHouse() {
System.out.println("买房的交易过程");
this.doOther();
}
public void doOther() {
System.out.println("做了点其他事情");
}
}
java.lang.reflect.InvocationHandler 中的 invoke(Object proxy, Method method, Object[] args)方法:
- 参数 Object proxy:代理对象(慎用)
- 参数 Method method:当前执行的方法
- 参数 Object[] args:当前执行的方法运行时传递过来的参数
public class BuyHouseJdkProxy implements InvocationHandler {
private final Object target;
public BuyHouseJdkProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("买房前中介处理");
Object result = method.invoke(target, args);
System.out.println("买房后中介处理");
return result;
}
java.lang.reflect.Proxy 中的 newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 生成一个代理对象:
- 参数 ClassLoader loader:代理对象的类加载器 一般使用被代理对象的类加载器
- 参数 Class[] interfaces: 代理对象的要实现的接口 一般使用的被代理对象实现的接口
- 参数 InvocationHandler h: (接口)执行处理类
public class BuyHouseJdkProxyTest {
public static void main(String[] args) {
BuyHouseService buyHouseService = new BuyHouseService();
BuyHouseInterface buyHouseJdkProxy = (BuyHouseInterface)
Proxy.newProxyInstance(
BuyHouseService.class.getClassLoader(),
new Class[]{BuyHouseInterface.class},
new BuyHouseJdkProxy(buyHouseService));
buyHouseJdkProxy.buyHouse();
}
}
虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是 它始终无法摆脱仅支持 interface 代理的桎梏(我们要使用被代理的对象的接口),因为它的设计注定了这个遗憾。
动态代理(CGLIB)
CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是 final 的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用 JAVA 反射的 JDK 动态代理要快。
CGLIB 底层:使用字节码处理框架 ASM,来转换字节码并生成新的类。不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。
public class BuyHouseService {
public void buyHouse() {
System.out.println("买房的交易过程");
this.doOther();
}
public void doOther() {
System.out.println("做了点其他事情");
}
}
MethodInterceptor 中的 intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) 方法:
- 参数 Object object:由 CGLIB 动态生成的代理类实例
- 参数 Method method:为上文中实体类所调用的被代理的方法引用
- 参数 Object[] args:为参数值列表
- 参数 MethodProxy methodProxy:为生成的代理类对方法的代理引用
public class BuyHouseCglibProxy implements MethodInterceptor {
private final Object target;
public BuyHouseCglibProxy(Object target) {
this.target = target;
}
public static T getInstance(T target) {
BuyHouseCglibProxy cglibProxy = new BuyHouseCglibProxy(target);
Enhancer enhancer = new Enhancer();
enhancer.setCallback(cglibProxy);
enhancer.setSuperclass(target.getClass());
return (T) enhancer.create();
}
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("买房前中介处理");
Object result = methodProxy.invokeSuper(object, args);
System.out.println("买房后中介处理");
return result;
}
}
Enhancer 类似 CGLib 中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展。首先将被代理类 TargetObject 设置成父类,然后设置拦截器 TargetInterceptor,最后执行 enhancer.create() 动态生成一个代理类。
methodProxy.invokeSuper(object, args) 调用代理类实例上的 proxy 方法的父类方法(即实体类 TargetObject 中对应的方法)。
public class BuyHouseCglibProxyTest {
public static void main(String[] args) {
BuyHouseService buyHouseService = new BuyHouseService();
BuyHouseService buyHouseCglibProxy = BuyHouseCglibProxy.getInstance(buyHouseService);
buyHouseCglibProxy.buyHouse();
}
}
CGLIB 创建的动态代理对象比 JDK 创建的动态代理对象的性能更高,但是 CGLIB 创建代理对象时所花费的时间却比 JDK 多得多。所以对于单例的对象,因为无需频繁创建对象,用 CGLIB 合适,反之使用 JDK 方式要更为合适一些。同时由于 CGLib 由于是采用动态创建子类的方法,对于 final 修饰的方法无法进行代理。
动态代理(JDK 和 CGLIB)的细节
细心的同学会发现,上述代码中有同样的如下片段:
public void buyHouse() {
System.out.println("买房的交易过程");
this.doOther();
}
public void doOther() {
System.out.println("做了点其他事情");
}
JDK 动态代理 buyHouseJdkProxy.buyHouse() 的输出结果:
买房前中介处理
买房的交易过程
做了点其他事情
买房后中介处理
CGLIB 动态代理 buyHouseCglibProxy.buyHouse() 的输出结果:
买房前中介处理
买房的交易过程
买房前中介处理
做了点其他事情
买房后中介处理
买房后中介处理
内部调用的形式中,JDK 动态代理 不会去代理内部方法,而 CGLIB 动态代理 自动代理内部方法。
4. 外观模式(Facade)
外观模式隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。简单来说,该模式就是把一些复杂的流程封装成一个接口供给外部用户更简单的使用。
外观模式一般涉及到三个角色:
- 门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。(客户调用,同时自身调用子系统功能)
- 子系统角色:实现了子系统的功能。它对客户角色和Facade时未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。(实现具体功能)
- 客户角色:通过调用Facede来完成要实现的功能。(调用门面角色)
public class Computer {
private CPU cpu;
private Disk disk;
private Memory memory;
public Computer() {
cpu = new CPU();
memory = new Memory();
disk = new Disk();
}
public void start() {
System.out.println("Computer start begin");
cpu.start();
disk.start();
memory.start();
System.out.println("Computer start end");
}
public void shutDown() {
System.out.println("Computer shutDown begin");
cpu.shutDown();
disk.shutDown();
memory.shutDown();
System.out.println("Computer shutDown end");
}
public static class CPU {
public void start() {
System.out.println("CPU is start...");
}
public void shutDown() {
System.out.println("CPU is shutDown...");
}
}
public static class Disk {
public void start() {
System.out.println("Disk is start...");
}
public void shutDown() {
System.out.println("Disk is shutDown...");
}
}
public static class Memory {
public void start() {
System.out.println("Memory is start...");
}
public void shutDown() {
System.out.println("Memory is shutDown...");
}
}
}
每个 Computer 都有 CPU、Memory、Disk。在 Computer 开启和关闭的时候,相应的部件也会开启和关闭:
public class MainTest {
public static void main(String[] args) {
Computer computer = new Computer();
computer.start();
System.out.println("=================");
computer.shutDown();
}
}
Computer start begin
CPU is start...
Disk is start...
Memory is start...
Computer start end
=================
Computer shutDown begin
CPU is shutDown...
Disk is shutDown...
Memory is shutDown...
Computer shutDown end
外观模式主要的优点:
- 松散耦合:使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;
- 简单易用:客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟 Facade 类交互即可;
- 划分访问层次:有些方法是对系统外的,有些方法是系统内部相互交互的使用的。子系统把那些暴露给外部的功能集中到门面中,这样就可以实现客户端的使用,很好的隐藏了子系统内部的细节。
5. 桥接模式(Bridge)
桥接模式将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
我们思考一个问题:我有 N 种不同品牌的手机,手机里存在 M 个不同的软件,我们要求每种不同品牌的手机中,不同的软件都实现不一样的功能,如果按照一般的继承思路去扩展类,至少需要 N * M 个子类去实现不一样的需求。
继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿。因此,在设计模式中,有一个原则为优先使用组合/聚合,而不是继承。
我们换一种解决思路:品牌手机和手机的软件本身就是两种独立不同的元素。但是他们之间又存在关联关系,如果把这两种不同的元素,放在两个不同维度上去继承扩展子类,再将两个维度关联起来,那么我们的不同的子类数量可以减少到 N + M 个,通过维度组合类来达到 N * M 个需求的功能。
public interface Software {
void run();
}
public class SoftwareA implements Software {
@Override
public void run() {
System.out.println("Run software A");
}
}
public class SoftwareB implements Software {
@Override
public void run() {
System.out.println("Run software B");
}
}
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
public abstract class Phone {
protected Software software;
public void setSoftware(Software software) {
this.software = software;
}
protected abstract void run();
public void execute() {
this.run();
software.run();
}
}
public class PhoneA extends Phone {
@Override
public void run() {
System.out.println("Use phone A");
}
}
public class PhoneB extends Phone {
@Override
public void run() {
System.out.println("Use phone B");
}
}
public class PhoneC extends Phone {
@Override
public void run() {
System.out.println("Use phone C");
}
}
桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。
public class MainTest {
public static void main(String[] args) {
Phone phoneA = new PhoneA();
phoneA.setSoftware(new SoftwareB());
phoneA.execute();
}
}
Use phone A
Run software B
当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。
桥接模式通常适用于以下场景:
很多时候,我们分不清该使用继承还是组合/聚合或其他方式等,其实可以从现实语义进行思考。因为软件最终还是提供给现实生活中的人使用的,是服务于人类社会的,软件是具备现实场景的。当我们从纯代码角度无法看清问题时,现实角度可能会提供更加开阔的思路。
6. 组合模式(Composite)
组合模式有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
组合模式的应用场景:
- 在需要表示一个对象整体与部分的层次结构的场合。
- 要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。
组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点。
public interface Component {
public void operation();
}
public class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("树叶" + name + ":被访问!");
}
}
其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。
public class Composite implements Component {
private ArrayList children = new ArrayList();
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
public void operation() {
for (Component obj : children) {
obj.operation();
}
}
}
这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。
public class MainTest {
public static void main(String[] args) {
Composite c0 = new Composite();
Component leaf1 = new Leaf("1");
c0.add(leaf1);
Composite c1 = new Composite();
c0.add(c1);
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
树叶1:被访问!
树叶2:被访问!
树叶3:被访问!
组合模式的主要优点有:
7. 享元模式(Flyweight)
享元模式通过共享的方式高效的支持大量细粒度的对象。在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
享元模式的应用场景:
- 系统中有大量对象。
- 这些对象消耗大量内存。
- 这些对象的状态大部分可以外部化。
- 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对- 系统不依赖于这些对象身份,这些对象是不可分辨的。
简单来说,我们抽取出一个对象的外部状态(不能共享)和内部状态(可以共享)。然后根据外部状态的决定是否创建内部状态对象。内部状态对象是通过哈希表保存的,当外部状态相同的时候,不再重复的创建内部状态对象,从而减少要创建对象的数量。
public interface IFlyweight {
void print();
}
public class Flyweight implements IFlyweight {
private String id;
public Flyweight(String id) {
this.id = id;
}
@Override
public void print() {
System.out.println("Flyweight. ...");
}
public String getId() {
return id;
}
}
创建工厂,这里要特别注意,为了避免享元对象被重复创建,我们使用 HashMap 中的 key 值保证其唯一。
public class FlyweightFactory {
private Map flyweightMap = new HashMap();
public IFlyweight getFlyweight(String str) {
IFlyweight flyweight = flyweightMap.get(str);
if (flyweight == null) {
flyweight = new Flyweight(str);
flyweightMap.put(str, flyweight);
}
return flyweight;
}
public int getFlyweightMapSize() {
return flyweightMap.size();
}
}
测试,我们创建三个字符串,但是只会产生两个享元对象。
public class MainTest {
public static void main(String[] args) {
FlyweightFactory flyweightFactory = new FlyweightFactory();
IFlyweight flyweight1 = flyweightFactory.getFlyweight("A");
IFlyweight flyweight2 = flyweightFactory.getFlyweight("B");
IFlyweight flyweight3 = flyweightFactory.getFlyweight("A");
flyweight1.print();
flyweight2.print();
flyweight3.print();
System.out.println(flyweightFactory.getFlyweightMapSize());
}
}
Flyweight.id = A ...
Flyweight.id = B ...
Flyweight.id = A ...
2
享元模式 与 池化技术 的区别
对象池、连接池(比如数据库连接池)、线程池等也是为了复用,但其与享元模式的复用有区别的。
享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。应用实例比如 Java 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。
池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从对象池中取一个对象,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。