软件设计模式系列(第二期).md 17 KB


title: 软件设计模式系列(第二期)

软件设计模式系列(第二期)

作者:Tom哥
公众号:微观技术
博客:https://offercome.cn
人生理念:知道的越多,不知道的越多,努力去学

面对复杂的业务场景,千变万化的客户需求,如何以一变应万变,以最小的开发成本快速落地实现,同时保证系统有着较低的复杂度,能够保证系统后续de持续迭代能力,让系统拥有较高的可扩展性。

这些是一个合格的架构师必须修炼的基础内功,但是如何修炼这门神功???

我将常用的软件设计模式,做了汇总,目录如下:

(考虑到内容篇幅较大,为了便于大家阅读,将软件设计模式系列(共23个)拆分成四篇文章,每篇文章讲解六个设计模式,采用不同的颜色区分,便于快速消化记忆)

前文回顾:

本文是主要讲解桥接模式组合模式装饰模式门面模式代理模式责任链模式

1、桥接模式

自然界一般由实体和行为组成。当然为了提升系统的扩展性,它们两个又可以各自抽象,然后在抽象类中描述两者的依赖。

定义:

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

什么场景使用桥接模式?

  • 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
  • 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

核心思路:

  • 抽象实体:定义的一种抽象分类。比如:人
  • 具体实体:继承抽象实体的子类实体。比如:中国人、美国人、韩国人
  • 抽象行为:定义抽象实体中具备的多种行为。比如:学汉语、吃汉堡
  • 具体行为:实现抽象行为的具体算法。比如:中国人学汉语、美国人吃汉堡

代码示例:

/**
 * @author 微信公众号:微观技术
 * 抽象实体
 */
public abstract class AbstractEntity {
    protected AbstractBehavior abstractBehavior;

    public AbstractEntity(AbstractBehavior abstractBehavior) {
        this.abstractBehavior = abstractBehavior;
    }

    public abstract void out();

}

/**
 * 抽象行为
 */
public interface AbstractBehavior {

    public String action(String name);
}

/**
 * 关于食物的行为
 */
public class FoodBehavior implements AbstractBehavior {

    @Override
    public String action(String name) {
        if ("中国人".equals(name)) {
            return "吃 饺子";
        } else if ("美国人".equals(name)) {
            return "吃 汉堡";
        }
        return null;
    }
}

桥接模式是将抽象与抽象之间分离,具体实现类依赖于抽象。抽象的分离间接完成了具体类与具体类之间的解耦,它们之间使用抽象来进行组合或聚合,而不再靠多重继承来实现。本质是将一个对象的实体和行为分离,然后再基于这两个维度进行独立的演化。

适用场景:

  • 拆分复杂的类对象时。 当一个类中包含大量对象和方法时,既不方便阅读,也不方便修改。
  • 希望从多个独立维度上扩展时。 比如,系统功能性和非功能性角度,业务或技术角度等。
  • 运行时,组合不同的组件

2、组合模式

定义:

组合模式也称整体模式,把一组相似的对象当作一个单一的对象,然后将对象组合成树形结构以表示整个层次结构。

这里边有两个关键点:1、树形结构分层 2、业务统一化来简化操作

核心思路:

  • 抽象组件(AbstractNode):定义需要实现的统一操作。
  • 组合节点(CompositeNode):抽象组件的衍生子类,包含了若干孩子节点(其它组合节点或叶子节点)。
  • 叶子节点(LeafNode):抽象组件的子类,但它的下面没有子节点。

代码示例:

public abstract class AbstractNode {
    public abstract void add(AbstractNode abstractNode);
    public abstract void remove(AbstractNode abstractNode);
    public abstract void action();
}

public class CompositeNode extends AbstractNode {
    private Long nodeId;
    private List<AbstractNode> childNodes;  //存放子节点列表
    public CompositeNode(Long nodeId, List<AbstractNode> childNodes) {
        this.nodeId = nodeId;
        this.childNodes = childNodes;
    }
    @Override
    public void add(AbstractNode abstractNode) {
        childNodes.add(abstractNode);
    }
    @Override
    public void remove(AbstractNode abstractNode) {
        childNodes.remove(abstractNode);
    }
    @Override
    public void action() {
        for (AbstractNode childNode : childNodes) {
            childNode.action();
        }
    }
}

public class LeafNode extends AbstractNode {
    private Long nodeId;
    public LeafNode(Long nodeId) {
        this.nodeId = nodeId;
    }
    @Override
    public void add(AbstractNode abstractNode) {
        // 无子节点,无需处理
        return;
    }
    @Override
    public void remove(AbstractNode abstractNode) {
        // 无子节点,无需处理
        return;
    }
    @Override
    public void action() {
        System.out.println("叶子节点编号:" + nodeId);
    }
}

叶子节点不能新增、删除子节点,所以对应的方法为空。

组合模式本质上封装了复杂结构的内在变化,让使用者通过一个统一的整体来使用对象之间的结构。数据结构方面支持树形结构环形结构网状结构。如我们常见的 深度优先搜索广度优先搜索都是采用这种模式。

适用场景:

  • 一组对象按照某种层级结构进行管理。如:管理文件夹和文件,管理订单下的商品。
  • 需要按照统一的行为来处理复杂结构中的对象
  • 快速扩展对象组合。

手机开始是按品牌来归属分类,现在业务增加价格维度分类,我们只需要引入新的分支节点,按新的维度构建组合关系。

3、装饰模式

定义:

动态地向一个现有对象添加新的职责和行为,同时又不改变其结构,相当于对现有的对象进行包装。

核心思路:

  • 抽象组件(Component):装饰器基类,定义组件的基本功能
  • 具体组件(ConcreteComponent):抽象组件的具体实现
  • 抽象装饰器(Decorator):包含抽象组件的引用
  • 具体装饰器(ConcreteDecorator):抽象装饰器的子类,并重写组件接口方法,同时可以添加附加功能。

代码示例:

public abstract class Component {
    public abstract void execute();
}

public class ConcreteComponent extends Component {
    @Override
    public void execute() {
        System.out.println("具体子类 ConcreteComponent invoke !");
    }
}

public class Decorator extends Component {
    protected Component component;
    public Decorator(Component component) {
        this.component = component;
    }
    @Override
    public void execute() {
        component.execute();
    }
}

public class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }
    @Override
    public void execute() {
        System.out.println("装饰器子类 ConcreteDecorator invoke !");
        super.execute();
    }
}

装饰模式本质上就是给已有不可修改的类附加新的功能,同时还能很方便地撤销。

适用场景:

  • 无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的功能
  • 将业务逻辑组织为层次结构,可以为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
  • 不支持继承扩展类的场景。如:final 关键字限制了某个类的进一步扩展,可以通过装饰器对其进行封装,从而具备扩展能力。

4、门面模式

定义:

门面模式提供一个高层次的接口,要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行,使得子系统更易于使用。

门面模式要求我们使用统一的标准与系统交互,比如:我们打印日志基本会选择slf4j框架,其内部统一了log4jlog4j2CommonLog等日志框架,简化了我们的开发成本。

核心思路:

  • 门面系统。接收外部请求,并将请求转发给适当的子系统进行处理
  • 子系统。表示某个领域内的功能实现、或者具体子接口实现,比如,订单、支付等,专门处理由门面系统指派的任务。

简单来讲,引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口。

可能很多人有疑问,这个不就是代理模式吗?

门面模式可能代理的是多个接口,而代理模式通常只是代理一个接口。

业务场景:

移动互联网,我们都习惯了在线支付,相信很多人在付款时都听过这么一句话,”微信支付还是支付宝“,商户根据用户反馈再针对性选择收款渠道。

是不是很繁琐,为了解决这个问题,市面就有了聚合支付(该领域做非常棒的是收钱吧),整个业务模式就是这节要讲的门面模式,不管你用什么软件支付,只要打开付款二维码即可,收钱吧底层识别解析二维码,并根据扫描结果自动适配对应的收款渠道,完成用户的扣款动作,确实带来不错的用户体验。

优点:

  • 简化复杂系统,提供统一接口规范。比如:JPA提供了统一Java持久层API,底层适配多样化的存储系统。
  • 复杂的业务逻辑由内部子系统消化,只要对外接口规范不变,外部调用方不需要频繁修改
  • 扩展性较好,类似于SPI架构一样,支持水平扩展。
  • 较高的平滑过渡性。比如:我们要对老的系统架构升级,开发一系列新接口来替换原来的老接口,过渡期需要新老灰度测试、流量切换、平滑升级,可以采用该模式。门面模式在兼容多套系统、系统重构方面是把利器。

5、代理模式

定义:

为其他对象提供一种代理以控制对这个对象的访问

现实场景:

  • 房产中介
  • 包工头

核心思路:

  • 抽象主题类(AbstractSubject):定义接口方法,供客户端使用
  • 主题实现类(RealSubject):实现了抽象主题类的接口方法
  • 代理类(Proxy):实现了抽象主题类的接口方法,内部包含主题实现类的逻辑, 同时还包含一些自身的扩展操作。

代理模式与适配器模式相似。但适配器模式是转换为新的接口,而代理模式不会改变原有接口。

代码示例:

/**
 * @author 微信公众号:微观技术
 */
public interface AbstractSubject {
    void execute();
}

public class RealSubject implements AbstractSubject {
    @Override
    public void execute() {
        System.out.println("我是Tom哥,我要努力工作!");
    }
}

public class Proxy implements AbstractSubject {

    private AbstractSubject abstractSubject;

    public Proxy(AbstractSubject abstractSubject) {
        this.abstractSubject = abstractSubject;
    }

    @Override
    public void execute() {
        System.out.println("老板给Tom哥分配工作了。。。");
        abstractSubject.execute();
    }
}

按使用职责分为静态代理和动态代理。

  • 静态代理,代理类需要自己编写代码完成。
  • 动态代理,代理类通过 Proxy#newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法生成。
  • JDK实现的代理中不管是静态代理还是动态代理,都是面向接口编程。CGLib可以不限制一定是接口。

优点:

  • 职责清晰
  • 高扩展,只要实现了接口,都可以用代理
  • 智能化,动态代理
  • 降低了对象的直接耦合

适用场景:

  • 远程代理。无法直接操作远程对象。比如:Dubbo、gRPC,提供远程服务,客户端调用时需要走参数组装、序列化、网络传输等操作,这些通用逻辑都可以封装到代理中,客户端调用代理对象访问远程服务,就像调用本地对象一样方便。
  • 保护代理。当客户端通过代理对象访问原始对象时,代理对象会根据规则判断客户端是否有权限访问。比如:防火墙
  • 日志代理。比如:日志监控,正常业务访问时,调用代理,增加一些额外的日志记录功能。
  • 虚拟代理,适用于延迟初始化,用小对象表示大对象的场景,减少资源损耗,提升运行速度。
  • 不希望改变原对象,但需要增加类似于权限控制、日志、流控等附加功能时,可以使用代理模式。

6、责任链模式

定义:

责任链模式是一种行为设计模式,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链。收到请求后,每个处理者均可对请求进行处理,或将其传递给链中的下个处理者。

责任链模式是对数据结构中的链表结构的具体应用。

核心思路:

  • 抽象处理者(Handler):定义一个接口,内部包含处理方法和下一个节点的引用对象
  • 具体处理者(ConcreteHandler):抽象处理者的实现子类,判断本次请求是否处理,如果需要则处理,否则跳过,然后将请求转发给下一个节点。

优点:

  • 降低了对象之间的耦合度。链上各个节点各司其职,通过上下文传递数据,避免直接依赖。
  • 增强系统的可扩展性。如果有新的业务需求,只需要在合适的位置增加一个链节点即可,满足开闭原则。
  • 灵活性强。如果业务有变化,需要对工作流程做调整,只需要动态调整链上节点的次序即可。甚至为了满足多元化业务的多样化需求,我们可以为不同的业务类型定义自己的专属执行顺序。
  • 简化了对象之间的连接。每个对象只需保存下一个节点的引用,而不需保持所有节点。
  • 责任明确。每个节点只需处理自己的工作,如果不处理则传递给下一个对象。明确各类的责任范围,符合类的单一职责原则。

像我们常见的网关架构推荐使用该模式,通过服务编排,可以自由地在任意位置添加或移除节点,满足一系列个性化功能。

写在最后

设计模式很多人都学习过,但项目实战时总是晕晕乎乎,原因在于没有了解其核心是什么,底层逻辑是什么,《设计模式:可复用面向对象的基础》有讲过,

在设计中思考什么应该变化,并封装会发生变化的概念。

软件架构的精髓:找到变化,封装变化。

业务千变万化,没有固定的编码答案,千万不要硬套设计模式。无论选择哪一种设计模式,尽量要能满足SOLID原则,自我review是否满足业务的持续扩展性。有句话说的好,“不论白猫黑猫,能抓老鼠就是好猫。”

参考资料