设计模式之结构型模式

结构型模式关注组织类/对象,

结构型模式关注如何将类/对象按某种布局组成更大的结构。

  • 类结构型模式,采用继承机制来组织接口和类
  • 对象结构型模式,釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式包括 7 种:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式。

代理模式

使用者通过代理对象来控制目标对象。

  • 静态代理:静态代理类在编译期生成。
  • 动态代理:动态代理类在运行时生成,分为 JDK 代理和 CGLib 代理。

代理模式(Proxy)的角色

  • 抽象主题(Subject):目标接口/抽象类。
  • 真实主题(Real Subject):实现抽象主题。
  • 代理(Proxy):访问、控制、拓展真实主题。

代理模式将使用者与目标对象隔离,可保护、拓展目标对象。

使用场景:远程代理、防火墙

静态代理
// 抽象主题
public interface Supplier {
    void sell();
}
// 真实主题
public class ComputerSupplier implements Supplier {
    @Override
    public void sell() {}
}
// 代理
public class ComputerSupplierProxy {

    // 持有真实主题
    private ComputerSupplier supplier = new ComputerSupplier();

    // 代理真实主题的所有操作
    public void sell() {
        supplier.sell();
    }

}
// 使用者
ComputerSupplierProxy proxy = new ComputerSupplierProxy();
proxy.sell();
JDK 动态代理

动态代理类 Proxy 提供了创建代理对象的静态方法 newProxyInstance() 来获取代理对象。

// JDK 动态代理
public class ComputerSupplierProxyFactory {

    // 持有目标对象
    private ComputerSupplier supplier = new ComputerSupplier();

    // 获取代理对象
    public Supplier getProxySupplier() {
        Supplier proxySupplier = (Supplier) Proxy.newProxyInstance(
                // 类加载器
                supplier.getClass().getClassLoader(),
                // 代理类实现的接口
            	// 要求代理类必须实现接口
                supplier.getClass().getInterfaces(),
                // 代理对象的调用处理程序InvocationHandler
                (
                        proxy,  // 代理对象
                        method, // 接口的方法
                        args    // 方法的参数
                ) -> {
                    // 执行目标对象的方法
                    Object obj = method.invoke(supplier, args);
                    // 返回方法的返回值
                    return obj;
                }
        );
        return proxySupplier;
    }
    
}
// 使用者
ComputerSupplierProxyFactory factory = new ComputerSupplierProxyFactory();
Supplier supplier = factory.getProxySupplier();

注意,上述方式中的 InvocationHandler 对接口中的每一个方法生效,若要精确增强,可对 method 的方法名进行判断。

CGLIB 动态代理

CGLIB 可以为没有实现接口的类提供代理。

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
// CGLIB 动态代理
public class ComputerSupplierProxyFactory implements MethodInterceptor {

    // 持有目标对象
    private ComputerSupplier supplier = new ComputerSupplier();

    // 获取代理对象
    public ComputerSupplier getProxySupplier() {
        // Enhancer类似JDK中的Proxy
        Enhancer enhancer = new Enhancer();
        // 设置代理类的父类的字节码对象:指定父类
        enhancer.setSuperclass(ComputerSupplier.class);
        // 设置回调函数
        enhancer.setCallback(this);
        // 创建代理对象
        return (ComputerSupplier) enhancer.create();
    }

    @Override
    public Object intercept(
            Object obj,         // 代理对象
            Method method,      // 目标对象的方法
            Object[] args,      // 方法的参数
            MethodProxy proxy   // 方法代理
    ) throws Throwable {
        // 调用目标对象方法
        return method.invoke(supplier, args);
    }
    
}
// 使用者
ComputerSupplierProxyFactory factory = new ComputerSupplierProxyFactory();
ComputerSupplier supplier = factory.getProxySupplier();
代理方式对比

JDK 代理和 CGLIB 代理

  • CGLib 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。CGLib 生成的代理类是被代理类的子类,故不能对声明为 final 的类或者方法进行代理。
  • JDK1.8 及往后,JDK 代理效率高于 CGLib 代理,故有接口使用 JDK 动态代理,没有否则使用 CGLIB 代理。

动态代理和静态代理

  • 对于动态代理,接口中声明的所有方法都被转移到调用处理器集中统一处理(InvocationHandler.invoke),无需像静态代理那样逐个方法中转。

  • 如果接口增加方法,静态代理的代理类均需手动中转该方法,而动态代理无需如此处理。

适配器模式

将已有接口转为期待接口。

  • 类适配器模式
  • 对象适配器模式

类适配器模式类之间的耦合度比后者高,且要求了解现有组件库中的相关组件的内部结构。

适配器模式(Adapter)的角色

  • 目标接口(Target):期待接口,可以是抽象类或接口。
  • 适配者类(Adaptee):现有接口。
  • 适配器(Adapter)类:转换器,通过继承或引用适配者的对象,把适配者类转换成目标接口,让客户按目标接口的格式访问适配者。

适用场景

  • 旧系统存在满足新系统要求的接口,但其格式与新系统不一致。
  • 第三方组件接口定义与己方要求的接口定义不一致。
类适配器模式
// 使用者:期待读取SD,现能读取TF
public class Computer {
    public String readSD(SDCard sdCard) {
        return sdCard.readSD();
    }
}
// 目标接口:期待的
public interface SDCard {
    String readSD();
}
public class SDCardImpl implements SDCard {
    @Override
    public String readSD() {
        return "";
    }
}
// 现有接口:现有的
public interface TFCard {
    String readTF();
}
public class TFCardImpl implements TFCard {
    @Override
    public String readTF() {
        return "";
    }
}
// 适配器:实现SDCard,则SDAdapterTF也是期待的
public class SDAdapterTF extends TFCardImpl implements SDCard {
    @Override
    public String readSD() {
        // 继承TFCardImpl,则SDAdapterTF拥有现有读取TF的能力
        return readTF();
    }
}
// 使用者
Computer computer = new Computer();
String data = computer.readSD(new SDAdapterTF());

SDAdapterTF 实现了 SDCard,可以被 Computer 的 readSD() 方法接收,Computer 可以通过 readSD() 从 TFCard 中读取数据。

换句话说,Computer 的 readSD() 可以从 SDCard 中读取数据,同时适配了 TFCard。

类适配器模式仅当目标接口存在时可用(需要实现目标接口),并且违背了合成复用原则。

对象适配器模式
// 适配器
public class SDAdapterTF implements SDCard {

    // 持有适配者类
    private TFCard tfCard;

    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    @Override
    public String readSD() {
        return tfCard.readTF();
    }
    
}

对象适配器模式符合合成复用原则,当不存在目标接口(SDCard)时,可采用继承的方式。

源码寻迹

JDK 中的 StreamDecoder 聚合了 InputStream 并继承了 Reader,实现了从字节流到字符流的转换,InputStreamReader 对 StreamDecoder 进行了再次封装。

// 对部分方法参数和方法体进行了省略
public class StreamDecoder extends Reader{
    private final InputStream in;
    public int read(char[] cbuf, int offset, int length){
        implRead();
    }
    int implRead(){
        readBytes();
    }
    private int readBytes(){
        // private final ByteBuffer bb
        in.read(bb.array(), bb.arrayOffset() + pos, rem)
    }
}

由以上代码可以看到,StreamDecoder 的 read() 将字节流转换为了字符流进行输出。

装饰者模式

装饰对象。在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)。

装饰模式(Decorator)的角色:

  • 抽象构件(Component):定义待附加责任的对象。
  • 具体构件(Concrete Component):实现抽象构件。
  • 抽象装饰(Decorator): 继承或实现抽象构件,并包含具体构件的实例。
  • 具体装饰(ConcreteDecorator) :实现抽象装饰。

装饰者则是动态的附加责任,可由使用者动态对附加责任进行组合;继承是静态的附加责任,仅能给出附加责任的一种组合。

装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

适用场景

  • 不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护

    不能采用继承的情况:

    • 存在大量独立拓展,为每种拓展组合创建子类将导致类爆炸。
    • 类不能继承(如 final 类)
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

实现方式
// 抽象构件
public abstract class FastFood {
    // 价格(仅指当前构件的价格)
    private float price;
    // 描述
    private String des;
    // 花费
    public abstract float cost();
}
// 具体构件
public class FriedRice extends FastFood {

    public FriedRice() {
        super(10, "炒饭");
    }
    
    @Override
    public float cost() {
        return getPrice();
    }
}
// 抽象装饰
public abstract class Garnish extends FastFood {

    // 抽象构件
    private FastFood fastFood;

    public Garnish(FastFood fastFood, float price, String des) {
        super(price, des);
        this.fastFood = fastFood;
    }

}
// 具体装饰
public class Egg extends Garnish {

    public Egg(FastFood fastFood) {
        super(fastFood, 1, "鸡蛋");
    }

    @Override
    public float cost() {
        // 用Egg装饰FastFood后,总价格=Egg的价格+FastFood的总价格
        return super.getPrice() + getFastFood().cost();
    }

    @Override
    public String getDes() {
        return super.getDes() + getFastFood().getDes();
    }
}
// 使用者
FastFood food = new FriedRice();	// 具体构件
food = new Egg(food);				// 添加装饰
food = new Egg(food);				// 再添加装饰

添加装饰的过程在语义上更像是“往具体装饰中添加具体构件”。

对不能采用继承的情况中的类不能继承进行补充说明

  • 若类不能继承,则无法通过继承方式拓展功能。
  • 抽象装饰继承的是抽象构件,抽象类是可被继承的。
源码寻迹

IO 流中的包装类使用了装饰者模式,如 BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。

以 BufferedWriter 为例

// Writer 抽象构件;FileWriter 具体构件
public class FileWriter extends OutputStreamWriter {}
public class OutputStreamWriter extends Writer{}

// BufferedWriter 具体装饰
public class BufferedWriter extends Writer{
    private Writer out;
    public BufferedWriter(Writer out){}
}
Writer w = new FileWriter("");
w = new BufferedWriter(fw);
w = new BufferedWriter(fw);
VS 静态代理

相同点:都要实现与目标类形同的接口(方法);都要声明目标对象;都可增强目标方法。

不同点:

  • 装饰者注重增强目标对象,静态代理可隐藏目标对象。
  • 装饰者的目标对象由外界传递进来,静态代理的目标对象在内部创建。

桥接模式

聚合功能。将抽象与实现分离,用组合关系代替继承关系来实现。

桥接模式(Bridge)的角色:

  • 实现化(Implementor) :功能,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor) :实现实现化。
  • 抽象化(Abstraction) :使用功能,包含一个对实现化角色的引用。
  • 扩展抽象化(Refined Abstraction) :具体使用功能,抽象化角色的子类,通过组合关系调用实现化角色中的业务方法。

桥接模式使得实现化和抽象化在抽象层建立关联关系,任意扩展一个维度(实现化或抽象化)都不需要修改原有系统,并且实现细节对客户透明

适用场景:一个类存在两个独立变化的维度,且两个维度都需要进行扩展时。

实现方式
// 实现化
public interface VideoFile {
    void decode(String fileName);
}
// 具体实现化
public class Mp4File implements VideoFile {
    @Override
    public void decode(String fileName) {}
}
// 抽象化
public abstract class OperatingSystem {

    // 持有实现化
    protected VideoFile videoFile;

    public OperatingSystem(VideoFile videoFile) {
        this.videoFile = videoFile;
    }

    public abstract void play(String fileName);

}
// 拓展抽象化
public class Windows extends OperatingSystem {

    public Windows(VideoFile videoFile) {
        super(videoFile);
    }

    @Override
    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}
// 使用者
OperatingSystem system = new Windows(new Mp4File());
system.play();

外观模式

门面模式,为多个复杂的子系统提供一致的接口,而使子系统更容易被访问。“迪米特法则”的典型应用。

外观模式(Facade)的角色:

  • 外观(Facade):为多个子系统对外提供统一的接口。
  • 子系统(Sub System):实现具体功能。

外观模式对客户屏蔽了子系统的实现细节,降低了客户端与子系统的耦合度,但不符合开闭原则。

外观模式强调可通过外观/门面使用所有子系统的功能,并不关系如何组合这些功能。

适用场景:存在多个子系统。

实现方式
// 子系统
public class Tv {
    public void on(){}
}
// 子系统
public class Light {
    public void on(){}
}
// 外观
public class Facade {
    // 子系统
    private Light light;
    private Tv tv;

    public Facade() {
        light = new Light();
        tv = new Tv();
    }

    public void on(String msg) {
        if ("light".equals(msg)) light.on();
        else if ("tv".equals(msg)) tv.on();
    }

}
// 使用者
Facade facade =new Facade();
facade.on("light");
源码寻迹

Tomcat 会将请求信息封装成 ServletRequest 对象,该对象实际是 RequestFacade 对象。

public class RequestFacade implements HttpServletRequest {
    protected Request request;
}
public class Request implements HttpServletRequest{}
public interface HttpServletRequest extends ServletRequest{}

组合模式

部分整体模式,把一组相似对象当作单一对象,依据树形结构进行组合。

组合模式(Composite)的角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

组合模式分类

  • 透明组合模式:在抽象根节点中声明所有管理成员对象的方法。
  • 安全组合模式:在抽象根节点不声明任何管理成员对象的方法。

组合模式可以清楚地定义分层次的复杂对象,统一对待单一对象和组合结构,并且符合“开闭原则”。

适用场景:树型结构

实现方式
// 抽象根节点
public abstract class MenuComponent {
    // 名称
    protected String name;
    // 层级
    protected int level;

    // 添加子菜单
    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    // 打印菜单名称(含子菜单)
    public abstract void print();

}
// 树枝
public class Menu extends MenuComponent {

    private List<MenuComponent> menuComponentList = new ArrayList<>();

    @Override
    public void add(MenuComponent component) {
        menuComponentList.add(component);
    }

    @Override
    public void print() {
        for (int i = 0; i < level; i++) {
            System.out.println("--");
        }
        System.out.println(name);
        menuComponentList.forEach(MenuComponent::print);
    }
}
// 叶子
public class MenuItem extends MenuComponent {
    @Override
    public void print() {
		for (int i = 0; i < level; i++) {
            System.out.println("--");
        }
        System.out.println(name);
    }
}
// 使用者
MenuComponent m1 = new Menu("菜单管理",1);
MenuComponent m2 = new MenuItem("删除菜单",2);
MenuComponent m3 = new MenuItem("添加菜单",3);
m1.add(m2);
m1.add(m3);
ma.print();

享元模式

对象复用。

享元模式的状态

  • 内部状态,不会随着环境的改变而改变的可共享部分。
  • 外部状态,随环境改变而改变的不可以共享的部分。

享元模式(Flyweight)的角色

  • 抽象享元(Flyweight):提供方法向外界提供享元对象的内部数据(内部状态),并设计设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight) :实现抽象享元,单例,为内部状态提供存储空间。
  • 非享元(Unsharable Flyweight) :抽象享元不能被共享的子类设计为非共享具体享元类,非享元类对象通过实例化创建。
  • 享元工厂(Flyweight Factory) :负责创建和管理享元角色。

享元模式极大减少内存中相似或相同对象数量,外部状态相对独立,且不影响内部状态。

适用场景:存在大量相同或者相似的对象。

实现方式
// 抽象享元
public abstract class AbstractBox {

    public abstract String getShape();

    public void display(String color) {
        System.out.println("shape=" + getShape() + "; color=" + color);
    }
}
// 具体享元
public class IBox extends AbstractBox {
    @Override
    public String getShape() {
        return "I";
    }
}
public class LBox extends AbstractBox {
    @Override
    public String getShape() {
        return "L";
    }
}
// 享元工厂
public class BoxFactory {

    private final Map<String, AbstractBox> map;

    private BoxFactory() {
        this.map = new HashMap<>();
        map.put("I", new IBox());
        map.put("L", new LBox());
    }

    private static final BoxFactory factory = new BoxFactory();

    // 提供方法获取工厂对象
    public static BoxFactory getInstance() {
        return factory;
    }

    // 根据名称获取对象
    public AbstractBox getBox(String name) {
        return map.get(name);
    }

}
// 使用者
AbstractBox box = BoxFactory.getInstance().getBox("I");
源码寻迹

Integer 类使用了享元模式。

public final class Integer{
	public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
}

END

文章文档:公众号 字节幺零二四 回复关键字可获取本文文档。

  • 24
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值