设计模式 结构型

结构型模式

外观模式

外观模式又叫门面模式,提供了统一得接口,用来访问子系统中的一群接口。

适用于:

  1. 子系统越来越复杂,增加外观模式提供简单接口调用;
  2. 构建多层系统结构,利用外观对象作为每层的入口,简化层间调用。

优点:

  1. 简化了调用过程,无需了解深入子系统;
  2. 减低耦合度;
  3. 更好的层次划分;
  4. 符合迪米特法则。

缺点:

  1. 增加子系统,拓展子系统行为容易引入风险;
  2. 不符合开闭原则。

举个订外卖的例子。

创建一个外卖实体类Takeaway:

public class Takeaway {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

订外卖过程一般分为三个步骤:下单、支付和配送,所以我们创建三个Service对应这三个过程。新建下单服务OrderService:

public class OrderService {

    public boolean placeAnOrder(Takeaway takeaway) {
        System.out.println(takeaway.getName() + "下单成功");
        return true;
    }
}

新建支付服务PayService:

public class PayService {

    public boolean pay(Takeaway takeaway) {
        System.out.println("商品" + takeaway.getName() + "支付成功");
        return true;
    }
}

新建配送服务DeliveryService:

public class DeliveryService {

    public void delivery(Takeaway takeaway) {
        System.out.println(takeaway.getName() + "已由骑手XX接单,订单派送中");
    }
}

基于外观模式法则,我们需要创建一个Service来聚合这三个服务,客户端只需要和这个Service交互即可。新建外卖服务TakeawayService:

public class TakeawayService {

    private OrderService orderService = new OrderService();
    private PayService payService = new PayService();
    private DeliveryService deliveryService = new DeliveryService();

    public void takeOrder(Takeaway takeaway) {
        if (orderService.placeAnOrder(takeaway)) {
            if (payService.pay(takeaway)) {
                deliveryService.delivery(takeaway);
            }
        }
    }
}

新建个客户端测试一波:

public class Application {

    public static void main(String[] args) {
        Takeaway takeaway = new Takeaway();
        takeaway.setName("泡椒🐸");

        TakeawayService takeawayService = new TakeawayService();
        takeawayService.takeOrder(takeaway);
    }
}

可以看到,客户端只需要调用TakeawayService即可,无需关系内部具体经历了多少个步骤,运行main方法输出如下:

泡椒🐸下单成功
商品泡椒🐸支付成功
泡椒🐸已由骑手XX接单,订单派送中

该例子的UML图如下所示:

QQ截图20191218151629.png

装饰者模式

在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹性的替代方案。

适用于:

  1. 拓展一个类的功能;
  2. 动态给对象添加功能,并且动态撤销。

优点:

  1. 继承的有力补充,不改变原有对象的情况下给对象拓展功能;
  2. 通过使用不同的装饰类、不同的组合方式,实现不同的效果。
  3. 符合开闭原则。

缺点:

  1. 增加程序复杂性;

举个水果沙拉的例子。

比如在点水果沙拉外卖时,可以往水果沙拉里加各种水果,价格也会相应的调整,要让程序支持不同水果自由组合,并计算相应的价格,则可以使用装饰者模式来完成。

定义一个抽象的水果沙拉类AbstractFruitSalad:

public abstract class AbstractFruitSalad {
    public abstract String remark();
    public abstract int price();
}

包含备注和价格抽象方法。

接着创建一个抽象的装饰器AbstractDecorator(关键点,继承抽象水果沙拉类):

public class AbstractDecorator extends AbstractFruitSalad{

    private AbstractFruitSalad fruitSalad;

    public AbstractDecorator(AbstractFruitSalad fruitSalad){
        this.fruitSalad = fruitSalad;
    }

    @Override
    public String remark() {
        return fruitSalad.remark();
    }

    @Override
    public int price() {
        return fruitSalad.price();
    }
}

创建具体的水果沙拉类FruitSalad:

public class FruitSalad extends AbstractFruitSalad{
    @Override
    public String remark() {
        return "水果🥗(标准)\n";
    }

    @Override
    public int price() {
        return 9;
    }
}

该沙拉是标准的水果沙拉,价格是9元。

如果我们的水果沙拉还允许客户添加猕猴桃和西瓜,那么我们可以添加两个新的装饰器。添加猕猴桃装饰器KiwiDecorator:

public class KiwiDecorator extends AbstractDecorator {

    public KiwiDecorator(AbstractFruitSalad fruitSalad) {
        super(fruitSalad);
    }

    @Override
    public String remark() {
        return super.remark() + "加份🥝切\n";
    }

    @Override
    public int price() {
        return super.price() + 2;
    }
}

可以看到,加一份猕猴桃需要在原有基础上加2元。

接着继续创建西瓜装饰器WaterMelonDecorator:

public class WaterMelonDecorator extends AbstractDecorator {

    public WaterMelonDecorator(AbstractFruitSalad fruitSalad) {
        super(fruitSalad);
    }

    @Override
    public String remark() {
        return super.remark() + "加份🍉切\n";
    }

    @Override
    public int price() {
        return super.price() + 3;
    }
}

最后创建客户端Application测试一下:

public class Application {

    public static void main(String[] args) {
        // 点了份水果沙拉,并加了两份🥝和一份🍉,看看最终价格是多少?
        AbstractFruitSalad fruitSalad = new FruitSalad();
        fruitSalad = new KiwiDecorator(fruitSalad);
        fruitSalad = new KiwiDecorator(fruitSalad);
        fruitSalad = new WaterMelonDecorator(fruitSalad);

        System.out.println(fruitSalad.remark() + "价格是:" + fruitSalad.price());
    }
}

上面的写法也可以改为:

public class Application {

    public static void main(String[] args) {
        // 点了份水果沙拉,并加了两份🥝和一份🍉,看看最终价格是多少?
        AbstractFruitSalad fruitSalad = new FruitSalad();
        fruitSalad = new WaterMelonDecorator(new KiwiDecorator(new KiwiDecorator(fruitSalad)));

        System.out.println(fruitSalad.remark() + "价格是:" + fruitSalad.price());
    }
}

程序输出如下:

水果🥗(标准)
加份🥝切
加份🥝切
加份🍉切
价格是:16

通过不同的装饰器自由组合,我们可以灵活的组装出各式各样的水果沙拉,这正是装饰者模式的优点,但明显可以看出代码变复杂了。

这个例子的UML图如下所示:

QQ截图20191218172739.png

适配器模式

将一个类的接口转换为期望的另一个接口,使原本不兼容的类可以一起工作。

适用于:

  1. 已存在的类,它的方法和需求不匹配时(方法结果相同或者相似)

优点:

  1. 提高类的透明性和复用,现有的类复用但不需改变;
  2. 目标类和适配器类解耦,提高程序拓展性;
  3. 符合开闭原则。

缺点:

  1. 适配器编写过程需要全面考虑,可能会增加系统的复杂性;
  2. 降低代码可读性。

分为:类适配器模式和对象适配器模式。

先举个类适配器模式的例子:

假如项目里原有一条水果的产品线,比如包含一个树莓类Raspberry:

public class Raspberry {

    public void addRaspberry() {
        System.out.println("添加点树莓");
    }
}

随着项目的拓展,现在新增了水果派产品线,新建Pie接口:

public interface Pie {

    void make();
}

要将Raspberry加入到Pie产品线,又不想修改Raspberry类的代码,则可以创建一个适配器RaspberryPieAdaptor:

public class RaspberryPieAdaptor extends Raspberry implements Pie{
    @Override
    public void make() {
        System.out.println("制作一个派🥧");
        super.addRaspberry();
    }
}

适配器继承被适配的类,实现新的产品线接口。

在Application里测试一波:

public class Application {
    public static void main(String[] args) {
        Pie pie = new RaspberryPieAdaptor();
        pie.make();
    }
}

输出:

制作一个派🥧
添加点树莓

成功通过适配器制造了树莓派。类适配器模式的UML图很简单:

QQ截图20191219105058.png

对象适配器模式只需要将RaspberryPieAdaptor修改为:

public class RaspberryPieAdaptor implements Pie{

    private Raspberry raspberry = new Raspberry();

    @Override
    public void make() {
        System.out.println("制作一个派🥧");
        raspberry.addRaspberry();
    }
}

这种模式不直接继承被适配者,而是在适配器里创建被适配者。这种模式的UML图:

QQ截图20191219110730.png

享元模式

提供了减少对象数量从而改善应用所需的对象结构的方式,运用共享技术有效地支持大量细粒度的对象。

适用于:

  1. 底层系统开发,解决性能问题;
  2. 系统拥有大量相似对象,需要缓冲池的场景。

优点:

  1. 减少对象的创建,降低内存占用;

缺点:

  1. 关注内/外部状态,关注线程安全问题;
  2. 程序的逻辑复杂化。

内部状态:简单理解为享元对象的属性状态,不会因为外部的改变而改变; 外部状态:简单理解为方法参数。

举个例子,新建派🥧接口Pie:

public interface Pie {

    void make() throws InterruptedException;
}

其实现类水果派FruitPie:

public class FruitPie implements Pie {

    private String name;
    private LocalDateTime productTime;

    public FruitPie(String name) {
        this.name = name;
    }

    public void setProductTime(LocalDateTime productTime) {
        this.productTime = productTime;
    }

    @Override
    public void make() {
        try {
            Thread.sleep(100);
            System.out.println(name + "生产时间:" + this.productTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

包含名称和生产日期属性,并且有个make方法。

接着创建生产FruitPie的工厂FruitPieFactory:

public class FruitPieFactory {

    private static final HashMap<String, FruitPie> PIE_HASH_MAP = new HashMap<>();

    public static FruitPie produce(String name) {
        FruitPie fruitPie = PIE_HASH_MAP.get(name);
        if (fruitPie == null) {
            System.out.println("没有" + name + "制作方法,学习制作...");
            fruitPie = new FruitPie(name);
            PIE_HASH_MAP.put(name, fruitPie);
        }
        return fruitPie;
    }
}

代码关键是通过HashMap存储对象。

编写个测试类:

public class Application {

    private static final String[] PIE = {"🍇派", "🍈派", "🍓派", "🍒派"};

    public static void main(String[] args) {
        IntStream.range(0, 10).forEach((i) -> {
            String name = PIE[(int) (Math.random() * PIE.length)];
            FruitPie fruitPie = FruitPieFactory.produce(name);
            fruitPie.setProductTime(LocalDateTime.now());
            fruitPie.make();
        });
    }
}

输出如下所示:

没有🍓派制作方法,学习制作...
🍓派生产时间:2019-12-19T16:13:26.397
没有🍇派制作方法,学习制作...
🍇派生产时间:2019-12-19T16:13:26.498
🍇派生产时间:2019-12-19T16:13:26.599
没有🍒派制作方法,学习制作...
🍒派生产时间:2019-12-19T16:13:26.700
🍒派生产时间:2019-12-19T16:13:26.800
🍒派生产时间:2019-12-19T16:13:26.901
没有🍈派制作方法,学习制作...
🍈派生产时间:2019-12-19T16:13:27.002
🍓派生产时间:2019-12-19T16:13:27.103
🍇派生产时间:2019-12-19T16:13:27.203
🍇派生产时间:2019-12-19T16:13:27.304

从结果看,在10次循环中,只生产了4个对象,这很好的描述了系统有大量相似对象,需要缓冲池的场景。

JDK中的字符串常量池,数据库连接池等都是用的享元模式。

组合模式

将对象组合成树形结构以表示“部分-整体”的层次结构,使客户端对单个对象和组合对象保持一致的方式处理。

适用于:

  1. 客户端可以忽略组合对象与单个对象的差异;
  2. 处理树形结构数据。

优点:

  1. 层次清晰;
  2. 客户端不必关系层次差异,方便控制;
  3. 符合开闭原则。

缺点:

  1. 树形处理较为复杂。

举个菜单按钮组成的树形例子。

新建菜单按钮的组合抽象类AbstractMenuButton:

public abstract class AbstractMenuButton {

    public void add(AbstractMenuButton abstractMenuButton) {
        throw new UnsupportedOperationException("不支持创建操作");
    }

    public String getName() {
        throw new UnsupportedOperationException("不支持名称获取");
    }

    public String getType() {
        throw new UnsupportedOperationException("不支持类型获取");
    }

    public String getIcon() {
        throw new UnsupportedOperationException("不支持图标");
    }

    public void print() {
        throw new UnsupportedOperationException("不支持打印操作");
    }
}

组合了菜单按钮操作的基本方法。

新增按钮类Button:

public class Button extends AbstractMenuButton {

    private String name;

    public Button(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public String getType() {
        return "按钮";
    }

    @Override
    public void print() {
        System.out.println(getName() + "【" + getType() + "】");
    }
}

按钮拥有名称属性,并且支持名称获取,类型获取和打印方法,所以重写了这三个父类方法。

接着新建菜单类Menu:

public class Menu extends AbstractMenuButton {

    private List<AbstractMenuButton> items = new ArrayList<>();
    private String name;
    private String icon;
    private Integer level;

    public Menu(String name, String icon, Integer level) {
        this.name = name;
        this.icon = icon;
        this.level = level;
    }

    @Override
    public void add(AbstractMenuButton abstractMenuButton) {
        items.add(abstractMenuButton);
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public String getType() {
        return "菜单";
    }

    @Override
    public String getIcon() {
        return this.icon;
    }

    @Override
    public void print() {
        System.out.println(getIcon() + getName() + "【" + getType() + "】");
        for (AbstractMenuButton item : items) {
            if (this.level != null) {
                for (int i = 0; i < this.level; i++) {
                    System.out.print("    ");
                }
            }
            item.print();
        }
    }
}

菜单包含名称、图标和层级属性,并且菜单可以包含下级(比如下级菜单,下级按钮),所以它包含一个List类型的属性items。

此外,菜单包含添加下级、名称获取、类型获取、图标获取和打印方法。

新建一个客户端,测试菜单按钮的层级结构:

public class Application {

    public static void main(String[] args) {
        Menu userMenu = new Menu("用户管理", "🧑", 2);
        Button createUser = new Button("新增用户");
        Button updateUser = new Button("修改用户");
        Button deleteUser = new Button("删除用户");
        userMenu.add(createUser);
        userMenu.add(updateUser);
        userMenu.add(deleteUser);

        Menu logMenu = new Menu("操作日志", "📃", 2);
        Button export = new Button("导出Excel");
        logMenu.add(export);

        Menu systemMenu = new Menu("系统管理", "🔨", 1);
        systemMenu.add(userMenu);
        systemMenu.add(logMenu);

        systemMenu.print();
    }
}

打印输出如下所示:

🔨系统管理【菜单】
    🧑用户管理【菜单】
        新增用户【按钮】
        修改用户【按钮】
        删除用户【按钮】
    📃操作日志【菜单】
        导出Excel【按钮】

UML图如下所示:

QQ截图20191219190427.png

桥接模式

将抽象部分和具体实现部分分离,使它们都可以独立变化。通过组合的方式建立两个类之间的关系,而不是通过继承。

适用于:

  1. 抽象和实体实现之间增加更多的灵活性;
  2. 一个类存在多个独立变化的维度,并且需要独立拓展;
  3. 不希望使用继承。

优点:

  1. 分离抽象部分和具体实现部分;
  2. 提高了系统可拓展性;
  3. 符合开闭原则和合成复用原则。

缺点:

  1. 增加了系统的理解和设计难度;

举个例子:

现有派的接口类Pie:

public interface Pie {

    Pie makePie();

    void getType();
}

包含制作派和获取派类型抽象方法。

接着创建两个Pie的实现类,苹果派AppliePie:

public class ApplePie implements Pie {
    @Override
    public Pie makePie() {
        System.out.println("制作苹果派🍎🥧");
        return new ApplePie();
    }

    @Override
    public void getType() {
        System.out.println("水果派");
    }
}

胡萝卜派CarrotPie:

public class CarrotPie implements Pie{
    @Override
    public Pie makePie() {
        System.out.println("制作胡萝卜派🥕🥧");
        return new CarrotPie();
    }

    @Override
    public void getType() {
        System.out.println("蔬菜沙拉派");
    }
}

接着创建一个店铺抽象类Store,通过属性的方式和Pie相关联,目的是可以在不同的店铺实现类中灵活地制作各种派:

public abstract class Store {

    protected Pie pie;

    public Store(Pie pie){
        this.pie = pie;
    }

    abstract Pie makePie();
}

Store子类之一,山姆大叔的小店SamStore:

public class SamStore extends Store{

    public SamStore(Pie pie) {
        super(pie);
    }

    @Override
    Pie makePie() {
        System.out.print("山姆大叔的小店💒");
        return pie.makePie();
    }
}

Store子类之二,杰克的小店JackStore:

public class JackStore extends Store {

    public JackStore(Pie pie) {
        super(pie);
    }

    @Override
    Pie makePie() {
        System.out.print("杰克的小店💒");
        return pie.makePie();
    }
}

新建一个客户端,测试Pie的实现类和Store的继承类之间的自由组合:

public class Application {
    public static void main(String[] args) {
        Store samStore = new SamStore(new ApplePie());
        Pie samStorePie = samStore.makePie();
        samStorePie.getType();

        Store jackStore = new JackStore(new CarrotPie());
        Pie jackStorePie = jackStore.makePie();
        jackStorePie.getType();
    }
}

输出如下:

山姆大叔的小店💒制作苹果派🍎🥧
水果派
杰克的小店💒制作胡萝卜派🥕🥧
蔬菜沙拉派

这个例子很好地体现了桥接模式的特点,UML图如下:

QQ截图20191220151943.png

代理模式

为其他对象提供一种代理,以控制对这个对象的访问,代理对象在客户端和目标对象之间起到了中介的作用。

适用于:

  1. 保护目标对象;
  2. 增强目标对象。

优点:

  1. 将代理对象和真实被调用的目标对象分离;
  2. 降低耦合,拓展性好;
  3. 保护目标对象,增强目标对象。

缺点:

  1. 造成类的数目增加,增加复杂度;
  2. 客户端和目标对象增加代理对象,会造成处理速度变慢。

静态代理

通过在代码中显式地定义了一个代理类,在代理类中通过同名的方法对目标对象的方法进行包装,客户端通过调用代理类的方法来调用目标对象的方法。

举个静态代理的例子:

新建一个派的制作接口PieService:

public interface PieServcie {
    void makePie();
}

创建其实现类PieServiceImpl:

public class PieServiceImpl implements PieServcie{
    public void makePie() {
        System.out.println("制作🥗派");
    }
}

要对PieServiceImpl的makePie方法增强,我们需要创建一个代理对象PieServiceProxy:

public class PieServiceProxy {

    private PieServcie pieServcie;

    public void makePie() {
        beforeMethod();
        pieServcie = new PieServiceImpl();
        pieServcie.makePie();
        afterMethod();
    }

    private void beforeMethod() {
        System.out.println("准备材料");
    }

    private void afterMethod() {
        System.out.println("保鲜");
    }

}

在PieServiceProxy中我们创建了一个和PieServcie一致的同名方法makePie,方法内部调用了PieServiceImpl的makePie方法,并且在方法调用前调用了代理类的beforeMethod方法,方法调用后调用了代理类的afterMethod方法。

创建客户端Application,测试:

public class Application {

    public static void main(String[] args) {
        PieServiceProxy proxy = new PieServiceProxy();
        proxy.makePie();
    }
}

输出:

准备材料
制作🥗派
保鲜

这个例子的UML图如下:

QQ20200421-100633@2x

动态代理

JDK的动态代理只能代理接口,通过接口的方法名在动态生成的代理类中调用业务实现类的同名方法。

静态代理的缺点就是每需要代理一个类,就需要手写对应的代理类。这个问题可以用动态代理来解决。举个动态代理的例子:

新建冰淇淋制作接口IceCreamService:

public interface IceCreamService {
    void makeIceCream(String fruit);
}

实现类IceCreamServiceImpl:

public class IceCreamServiceImpl implements IceCreamService {

    public void makeIceCream(String fruit) {
        System.out.println("制作" + fruit + "🍦");
    }
}

现在需要对IceCreamServiceImpl进行代理增强,如果使用静态代理,我们需要编写一个IceCreamServiceImplProxy类,使用动态代理的话,我们可以动态生成对应的代理类。

创建DynamicProxy:

public class DynamicProxy implements InvocationHandler {

    // 代理的目标对象
    private Object object;

    public DynamicProxy(Object object) {
        this.object = object;
    }

    public Object proxy() {
        Class<?> clazz = object.getClass();
        // 生成代理对象
        return Proxy.newProxyInstance(clazz.getClassLoader(),
                clazz.getInterfaces(), this);
    }

    /**
     * @param proxy  动态生成的代理对象
     * @param method 代理方法
     * @param args   代理方法的方法参数
     * @return 结果
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       return null;
    }

}

动态代理类通过实现InvocationHandler的invoke方法实现,proxy用于生成代理对象。剩下的步骤和静态代理类似,完善DynamicProxy:

public class DynamicProxy implements InvocationHandler {

    // 代理的目标对象
    private Object object;

    public DynamicProxy(Object object) {
        this.object = object;
    }

    public Object proxy() {
        Class<?> clazz = object.getClass();
        // 生成代理对象
        return Proxy.newProxyInstance(clazz.getClassLoader(),
                clazz.getInterfaces(), this);
    }

    /**
     * @param proxy  动态生成的代理对象
     * @param method 代理方法
     * @param args   代理方法的方法参数
     * @return 结果
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        beforeMethod(object);
        // 反射执行代理对象的目标方法
        Object result = method.invoke(object, args);
        afterMethod(object);
        return result;
    }

    private void beforeMethod(Object object) {
        if (object instanceof PieServcie) {
            System.out.println("准备派的材料");
        } else if (object instanceof IceCreamService) {
            System.out.println("准备冰淇淋材料");
        } else {
            throw new RuntimeException("暂不支持代理" + object.getClass() + "类型");
        }
    }

    private void afterMethod(Object object) {
        if (object instanceof PieServcie) {
            System.out.println("保鲜派");
        } else if (object instanceof IceCreamService) {
            System.out.println("保鲜冰淇淋");
        } else {
            throw new RuntimeException("暂不支持代理" + object.getClass() + "类型");
        }
    }

}

创建客户端Application测试:

public class Application {

    public static void main(String[] args) {

        PieServcie pieServiceDynamicProxy = (PieServcie) new DynamicProxy(new PieServiceImpl()).proxy();
        pieServiceDynamicProxy.makePie();
        System.out.println("-----------------");
        IceCreamService iceCreamServiceDynamicProxy = (IceCreamService) new DynamicProxy(new IceCreamServiceImpl()).proxy();
        iceCreamServiceDynamicProxy.makeIceCream("🍓");
    }
}

结果:

准备派的材料
制作🥗派
保鲜派
-----------------
准备冰淇淋材料
制作🍓🍦
保鲜冰淇淋

可以看到,通过动态代理我们实现了目标方法增强,并且不需要手写目标类的代理对象。

CGLib代理

通过继承来实现,生成的代理类就是目标对象类的子类,通过重写业务方法来实现代理。

Spring对代理模式的拓展

  1. 当Bean有实现接口时,使用JDK动态代理;
  2. 当Bean没有实现接口时,使用CGLib代理。

可以通过以下配置强制使用CGLib代理:

spring:
  aop:
    proxy-target-class: true
posted @ 2022-10-06 16:45  Cassie_Lee  阅读(49)  评论(0)    收藏  举报