Java工程师的进阶之路 设计模式篇(三)

Java工程师的进阶之路 设计模式篇(一)
Java工程师的进阶之路 设计模式篇(二)
Java工程师的进阶之路 设计模式篇(三)

行为型模式

行为模式负责对象间的高效沟通和职责委派。

1. 责任链模式

亦称:职责链模式、命令链、CoR、Chain of Command、Chain of Responsibility

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

Java工程师的进阶之路 设计模式篇(三)

1.1. 适合场景

  1. 当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。

该模式能将多个处理者连接成一条链。接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。这样所有处理者都有机会来处理请求。

  1. 当必须按顺序执行多个处理者时, 可以使用该模式。

无论你以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。

  1. 如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。

如果在处理者类中有对引用成员变量的设定方法, 你将能动态地插入和移除处理者, 或者改变其顺序。

1.2. 实现方式

  1. 声明处理者接口并描述请求处理方法的签名。
  • 确定客户端如何将请求数据传递给方法。最灵活的方式是将请求转换为对象, 然后将其以参数的形式传递给处理函数。
  1. 为了在具体处理者中消除重复的样本代码, 你可以根据处理者接口创建抽象处理者基类。
  • 该类需要有一个成员变量来存储指向链上下个处理者的引用。你可以将其设置为不可变类。但如果你打算在运行时对链进行改变, 则需要定义一个设定方法来修改引用成员变量的值。
  • 为了使用方便, 你还可以实现处理方法的默认行为。如果还有剩余对象, 该方法会将请求传递给下个对象。具体处理者还能够通过调用父对象的方法来使用这一行为。
  1. 依次创建具体处理者子类并实现其处理方法。每个处理者在接收到请求后都必须做出两个决定:
  • 是否自行处理这个请求。
  • 是否将该请求沿着链进行传递。
  1. 客户端可以自行组装链, 或者从其他对象处获得预先组装好的链。在后一种情况下, 你必须实现工厂类以根据配置或环境设置来创建链。

  2. 客户端可以触发链中的任意处理者, 而不仅仅是第一个。请求将通过链进行传递, 直至某个处理者拒绝继续传递, 或者请求到达链尾。

  3. 由于链的动态性, 客户端需要准备好处理以下情况:

  • 链中可能只有单个链接。
  • 部分请求可能无法到达链尾。
  • 其他请求可能直到链尾都未被处理。

1.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 处理者 (Handler) 声明了所有具体处理者的通用接口。 该接口通常仅包含单个方法用于请求处理, 但有时其还会包含一个设置链上下个处理者的方法。
  2. 基础处理者 (Base Handler) 是一个可选的类, 你可以将所有处理者共用的样本代码放置在其中。 通常情况下, 该类中定义了一个保存对于下个处理者引用的成员变量。客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。该类还可以实现默认的处理行为:确定下个处理者存在后再将请求传递给它。
  3. 具体处理者 (Concrete Handlers) 包含处理请求的实际代码。 每个处理者接收到请求后, 都必须决定是否进行处理, 以及是否沿着链传递请求。处理者通常是独立且不可变的, 需要通过构造函数一次性地获得所有必要地数据。
  4. 客户端 (Client) 可根据程序逻辑一次性或者动态地生成链。 值得注意的是, 请求可发送给链上的任意一个处理者, 而非必须是第一个处理者。

1.4. 优点和缺点

  1. 你可以控制请求处理的顺序。
  2. 单一职责原则。你可对发起操作和执行操作的类进行解耦。
  3. 开闭原则。你可以在不更改现有代码的情况下在程序中新增处理者。
  • 部分请求可能未被处理。

1.5. 与其他模式的关系

  1. 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
  • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
  • 命令在发送者和请求者之间建立单向连接。
  • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
  • 观察者允许接收者动态地订阅或取消接收请求。
  1. 责任链通常和组合模式结合使用。在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

  2. 责任链的管理者可使用命令模式实现。在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。还有另外一种实现方式, 那就是请求自身就是一个命令对象。在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  3. 责任链和装饰模式的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。但是, 两者有几点重要的不同之处。责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。此外, 装饰无法中断请求的传递。

2. 命令模式

亦称:动作、事务、Action、Transaction、Command

命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

Java工程师的进阶之路 设计模式篇(三)

2.1. 适合场景

  1. 如果你需要通过操作来参数化对象, 可使用命令模式。

命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用:你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。

举个例子:你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。

  1. 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。

同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。一段时间后, 该字符串可被恢复成为最初的命令对象。因此, 你可以延迟或计划命令的执行。但其功能远不止如此!使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。

  1. 如果你想要实现操作回滚功能, 可使用命令模式。

尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。

为了能够回滚操作, 你需要实现已执行操作的历史记录功能。命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。

这种方法有两个缺点。首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。你可以使用备忘录模式来在一定程度上解决这个问题。

其次, 备份状态可能会占用大量内存。因此, 有时你需要借助另一种实现方式:命令无需恢复原始状态, 而是执行反向操作。反向操作也有代价:它可能会很难甚至是无法实现。

2.2. 实现方式

  1. 声明仅有一个执行方法的命令接口。

  2. 抽取请求并使之成为实现命令接口的具体命令类。每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。所有这些变量的数值都必须通过命令构造函数进行初始化。

  3. 找到担任发送者职责的类。在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。发送者自身通常并不创建命令对象, 而是通过客户端代码获取。

  4. 修改发送者使其执行命令, 而非直接将请求发送给接收者。

  5. 客户端必须按照以下顺序来初始化对象:

  • 创建接收者。
  • 创建命令, 如有需要可将其关联至接收者。
  • 创建发送者并将其与特定命令关联。

2.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。注意, 发送者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。
  2. 命令 (Command) 接口通常仅声明一个执行命令的方法。
  3. 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。但为了简化代码, 这些类可以进行合并。接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。
  4. 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。
  5. 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后, 生成的命令就可以与一个或多个发送者相关联了。

2.4. 优点和缺点

  1. 单一职责原则。你可以解耦触发和执行操作的类。
  2. 开闭原则。你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
  3. 你可以实现撤销和恢复功能。
  4. 你可以实现操作的延迟执行。
  5. 你可以将一组简单命令组合成一个复杂命令。
  • 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。

2.5. 与其他模式的关系

  1. 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
  • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
  • 命令在发送者和请求者之间建立单向连接。
  • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
  • 观察者允许接收者动态地订阅或取消接收请求。
  1. 责任链的管理者可使用命令模式实现。在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

还有另外一种实现方式, 那就是请求自身就是一个命令对象。在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  1. 你可以同时使用命令和备忘录模式来实现 “撤销”。在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  2. 命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。但是, 它们的意图有非常大的不同。

  • 你可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
  • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
  1. 原型模式可用于保存命令的历史记录。

  2. 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。

3. 迭代器模式

亦称:Iterator

迭代器模式是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。

Java工程师的进阶之路 设计模式篇(三)

3.1. 适合场景

  1. 当集合背后为复杂的数据结构, 且你希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑), 可以使用迭代器模式。

迭代器封装了与复杂数据结构进行交互的细节, 为客户端提供多个访问集合元素的简单方法。这种方式不仅对客户端来说非常方便, 而且能避免客户端在直接与集合交互时执行错误或有害的操作, 从而起到保护集合的作用。

  1. 使用该模式可以减少程序中重复的遍历代码。

重要迭代算法的代码往往体积非常庞大。当这些代码被放置在程序业务逻辑中时, 它会让原始代码的职责模糊不清, 降低其可维护性。因此, 将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁。

  1. 如果你希望代码能够遍历不同的甚至是无法预知的数据结构, 可以使用迭代器模式。

该模式为集合和迭代器提供了一些通用接口。如果你在代码中使用了这些接口, 那么将其他实现了这些接口的集合和迭代器传递给它时, 它仍将可以正常运行。

3.2. 实现方式

  1. 声明迭代器接口。该接口必须提供至少一个方法来获取集合中的下个元素。但为了使用方便, 你还可以添加一些其他方法, 例如获取前一个元素、 记录当前位置和判断迭代是否已结束。
  2. 声明集合接口并描述一个获取迭代器的方法。其返回值必须是迭代器接口。如果你计划拥有多组不同的迭代器, 则可以声明多个类似的方法。
  3. 为希望使用迭代器进行遍历的集合实现具体迭代器类。迭代器对象必须与单个集合实体链接。链接关系通常通过迭代器的构造函数建立。
  4. 在你的集合类中实现集合接口。其主要思想是针对特定集合为客户端代码提供创建迭代器的快捷方式。集合对象必须将自身传递给迭代器的构造函数来创建两者之间的链接。
  5. 检查客户端代码, 使用迭代器替代所有集合遍历代码。每当客户端需要遍历集合元素时都会获取一个新的迭代器。

3.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 迭代器 (Iterator) 接口声明了遍历集合所需的操作: 获取下一个元素、 获取当前位置和重新开始迭代等。
  2. 具体迭代器 (Concrete Iterators) 实现遍历集合的一种特定算法。 迭代器对象必须跟踪自身遍历的进度。这使得多个迭代器可以相互独立地遍历同一集合。
  3. 集合 (Collection) 接口声明一个或多个方法来获取与集合兼容的迭代器。 请注意, 返回方法的类型必须被声明为迭代器接口, 因此具体集合可以返回各种不同种类的迭代器。
  4. 具体集合 (Concrete Collections) 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。 你可能会琢磨, 剩下的集合代码在什么地方呢?不用担心, 它也会在同一个类中。只是这些细节对于实际模式来说并不重要, 所以我们将其省略了而已。
  5. 客户端 (Client) 通过集合和迭代器的接口与两者进行交互。 这样一来客户端无需与具体类进行耦合, 允许同一客户端代码使用各种不同的集合和迭代器。客户端通常不会自行创建迭代器, 而是会从集合中获取。但在特定情况下, 客户端可以直接创建一个迭代器 (例如当客户端需要自定义特殊迭代器时)。

3.4. 优点和缺点

  1. 单一职责原则。通过将体积庞大的遍历算法代码抽取为独立的类, 你可对客户端代码和集合进行整理。
  2. 开闭原则。你可实现新型的集合和迭代器并将其传递给现有代码, 无需修改现有代码。
  3. 你可以并行遍历同一集合, 因为每个迭代器对象都包含其自身的遍历状态。
  4. 相似的, 你可以暂停遍历并在需要时继续。
  • 如果你的程序只与简单的集合进行交互, 应用该模式可能会矫枉过正。
  • 对于某些特殊集合, 使用迭代器可能比直接遍历的效率低。

3.5. 与其他模式的关系

  1. 你可以使用迭代器模式来遍历组合模式树。
  2. 你可以同时使用工厂方法模式和迭代器来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。
  3. 你可以同时使用备忘录模式和迭代器来获取当前迭代器的状态, 并且在需要的时候进行回滚。
  4. 可以同时使用访问者模式和迭代器来遍历复杂数据结构, 并对其中的元素执行所需操作, 即使这些元素所属的类完全不同。

4. 中介者模式

亦称:调解人、控制器、Intermediary、Controller、Mediator

中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

Java工程师的进阶之路 设计模式篇(三)

4.1. 适合场景

  1. 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。

该模式让你将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。

  1. 当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。

应用中介者模式后, 每个组件不再知晓其他组件的情况。尽管这些组件无法直接交流, 但它们仍可通过中介者对象进行间接交流。如果你希望在不同应用中复用一个组件, 则需要为其提供一个新的中介者类。

  1. 如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时, 可使用中介者模式。

由于所有组件间关系都被包含在中介者中, 因此你无需修改组件就能方便地新建中介者类以定义新的组件合作方式。

4.2. 实现方式

  1. 找到一组当前紧密耦合, 且提供其独立性能带来更大好处的类 (例如更易于维护或更方便复用)。
  2. 声明中介者接口并描述中介者和各种组件之间所需的交流接口。在绝大多数情况下, 一个接收组件通知的方法就足够了。如果你希望在不同情景下复用组件类, 那么该接口将非常重要。只要组件使用通用接口与其中介者合作, 你就能将该组件与不同实现中的中介者进行连接。
  3. 实现具体中介者类。该类可从自行保存其下所有组件的引用中受益。
  4. 你可以更进一步, 让中介者负责组件对象的创建和销毁。此后, 中介者可能会与工厂或外观类似。
  5. 组件必须保存对于中介者对象的引用。该连接通常在组件的构造函数中建立, 该函数会将中介者对象作为参数传递。
  6. 修改组件代码, 使其可调用中介者的通知方法, 而非其他组件的方法。然后将调用其他组件的代码抽取到中介者类中, 并在中介者接收到该组件通知时执行这些代码。

4.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 组件 (Component) 是各种包含业务逻辑的类。 每个组件都有一个指向中介者的引用, 该引用被声明为中介者接口类型。组件不知道中介者实际所属的类, 因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。
  2. 中介者 (Mediator) 接口声明了与组件交流的方法, 但通常仅包括一个通知方法。 组件可将任意上下文 (包括自己的对象) 作为该方法的参数, 只有这样接收组件和发送者类之间才不会耦合。
  3. 具体中介者 (Concrete Mediator) 封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理, 甚至有时会对其生命周期进行管理。
  4. 组件并不知道其他组件的情况。 如果组件内发生了重要事件, 它只能通知中介者。中介者收到通知后能轻易地确定发送者, 这或许已足以判断接下来需要触发的组件了。对于组件来说, 中介者看上去完全就是一个黑箱。发送者不知道最终会由谁来处理自己的请求, 接收者也不知道最初是谁发出了请求。

4.4. 优点和缺点

  1. 单一职责原则。你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护。
  2. 开闭原则。你无需修改实际组件就能增加新的中介者。
  3. 你可以减轻应用中多个组件间的耦合情况。
  4. 你可以更方便地复用各个组件。
  • 一段时间后, 中介者可能会演化成为上帝对象。

4.5. 与其他模式的关系

  1. 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
  • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
  • 命令在发送者和请求者之间建立单向连接。
  • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
  • 观察者允许接收者动态地订阅或取消接收请求。
  1. 外观模式和中介者的职责类似:它们都尝试在大量紧密耦合的类中组织起合作。
  • 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
  • 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象, 无法直接相互交流。
  1. 中介者和观察者之间的区别往往很难记住。在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。让我们来看看如何做到这一点。
  • 中介者的主要目标是消除一系列系统组件之间的相互依赖。这些组件将依赖于同一个中介者对象。观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
  • 有一种流行的中介者模式实现方式依赖于观察者。中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。当中介者以这种方式实现时, 它可能看上去与观察者非常相似。
  • 当你感到疑惑时, 记住可以采用其他方式来实现中介者。例如, 你可永久性地将所有组件链接到同一个中介者对象。这种实现方式和观察者并不相同, 但这仍是一种中介者模式。
  • 假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

5. 备忘录模式

亦称:快照、Snapshot、Memento

备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

Java工程师的进阶之路 设计模式篇(三)

5.1. 适合场景

  1. 当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。

备忘录模式允许你复制对象中的全部状态 (包括私有成员变量), 并将其独立于对象进行保存。尽管大部分人因为 “撤销” 这个用例才记得该模式, 但其实它在处理事务 (比如需要在出现错误时回滚一个操作) 的过程中也必不可少。

  1. 当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。

备忘录让对象自行负责创建其状态的快照。任何其他对象都不能读取快照, 这有效地保障了数据的安全性。

5.2. 实现方式

  1. 确定担任原发器角色的类。重要的是明确程序使用的一个原发器中心对象, 还是多个较小的对象。
  2. 创建备忘录类。逐一声明对应每个原发器成员变量的备忘录成员变量。
  3. 将备忘录类设为不可变。备忘录只能通过构造函数一次性接收数据。该类中不能包含设置器。
  4. 如果你所使用的编程语言支持嵌套类, 则可将备忘录嵌套在原发器中;如果不支持, 那么你可从备忘录类中抽取一个空接口, 然后让其他所有对象通过接口来引用备忘录。你可在该接口中添加一些元数据操作, 但不能暴露原发器的状态。
  5. 在原发器中添加一个创建备忘录的方法。原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。该方法返回结果的类型必须是你在上一步中抽取的接口 (如果你已经抽取了)。实际上, 创建备忘录的方法必须直接与备忘录类进行交互。
  6. 在原发器类中添加一个用于恢复自身状态的方法。该方法接受备忘录对象作为参数。如果你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。在这种情况下, 你需要将输入对象强制转换为备忘录, 因为原发器需要拥有对该对象的完全访问权限。
  7. 无论负责人是命令对象、 历史记录或其他完全不同的东西, 它都必须要知道何时向原发器请求新的备忘录、 如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。
  8. 负责人与原发器之间的连接可以移动到备忘录类中。在本例中, 每个备忘录都必须与创建自己的原发器相连接。恢复方法也可以移动到备忘录类中, 但只有当备忘录类嵌套在原发器中, 或者原发器类提供了足够多的设置器并可对其状态进行重写时, 这种方式才能实现。

5.3. 模式结构

5.3.1. 基于嵌套类的实现

Java工程师的进阶之路 设计模式篇(三)
  1. 原发器 (Originator) 类可以生成自身状态的快照, 也可以在需要时通过快照恢复自身状态。
  2. 备忘录 (Memento) 是原发器状态快照的值对象 (value object)。 通常做法是将备忘录设为不可变的, 并通过构造函数一次性传递数据。
  3. 负责人 (Caretaker) 仅知道 “何时” 和 “为何” 捕捉原发器的状态, 以及何 恢复状态。 负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时, 负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的恢复 (restoration) 方法。
  4. 在该实现方法中, 备忘录类将被嵌套在原发器中。 这样原发器就可访问备忘录的成员变量和方法, 即使这些方法被声明为私有。另一方面, 负责人对于备忘录的成员变量和方法的访问权限非常有限:它们只能在栈中保存备忘录, 而不能修改其状态。

5.3.2. 基于中间接口的实现

Java工程师的进阶之路 设计模式篇(三)
  1. 在没有嵌套类的情况下, 你可以规定负责人仅可通过明确声明的中间接口与备忘录互动。 该接口仅声明与备忘录元数据相关的方法, 限制其对备忘录成员变量的直接访问权限。
  2. 另一方面, 原发器可以直接与备忘录对象进行交互, 访问备忘录类中声明的成员变量和方法。 这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。

5.3.3. 封装更加严格的实现

Java工程师的进阶之路 设计模式篇(三)
  1. 这种实现方式允许存在多种不同类型的原发器和备忘录。每种原发器都和其相应的备忘录类进行交互。原发器和备忘录都不会将其状态暴露给其他类。
  2. 负责人此时被明确禁止修改存储在备忘录中的状态。但负责人类将独立于原发器, 因为此时恢复方法被定义在了备忘录类中。
  3. 每个备忘录将与创建了自身的原发器连接。原发器会将自己及状态传递给备忘录的构造函数。由于这些类之间的紧密联系, 只要原发器定义了合适的设置器 (setter), 备忘录就能恢复其状态。

5.4. 优点和缺点

  1. 你可以在不破坏对象封装情况的前提下创建对象状态快照。
  2. 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。
  • 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。
  • 负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用的备忘录。
  • 绝大部分动态编程语言 (例如 PHP、 PythonJavaScript) 不能确保备忘录中的状态不被修改。

5.5. 与其他模式的关系

  1. 你可以同时使用命令模式和备忘录模式来实现 “撤销”。在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
  2. 你可以同时使用备忘录和迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。
  3. 有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

6. 观察者模式

亦称:事件订阅者、监听者、Event-Subscriber、Listener、Observer

观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

Java工程师的进阶之路 设计模式篇(三)

6.1. 适合场景

  1. 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。

当你使用图形用户界面类时通常会遇到一个问题。比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。

  1. 当应用中的一些对象必须观察其他对象时, 可使用该模式。但仅能在有限时间内或特定情况下使用。

订阅列表是动态的, 因此订阅者可随时加入或离开该列表。

6.2. 实现方式

  1. 仔细检查你的业务逻辑, 试着将其拆分为两个部分:独立于其他代码的核心功能将作为发布者;其他代码则将转化为一组订阅类。

  2. 声明订阅者接口。该接口至少应声明一个 update方法。

  3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。记住发布者必须仅通过订阅者接口与它们进行交互。

  4. 确定存放实际订阅列表的位置并实现订阅方法。通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。具体发布者会扩展该类从而继承所有的订阅行为。但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式:将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。

  5. 创建具体发布者类。每次发布者发生了重要事件时都必须通知所有的订阅者。

  6. 在具体订阅者类中实现通知更新的方法。绝大部分订阅者需要一些与事件相关的上下文数据。这些数据可作为通知方法的参数来传递。但还有另一种选择。订阅者接收到通知后直接从通知中获取所有数据。在这种情况下, 发布者必须通过更新方法将自身传递出去。另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。

  7. 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。

6.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
  2. 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。
  3. 订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个 update更新方法。该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。
  4. 具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。
  5. 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。
  6. 客户端 (Client) 会分别创建发布者和订阅者对象, 然后为订阅者注册发布者更新。

6.4. 优点和缺点

  1. 开闭原则。你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
  2. 你可以在运行时建立对象之间的联系。
  • 订阅者的通知顺序是随机的。

6.5. 与其他模式的关系

  1. 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
  • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
  • 命令在发送者和请求者之间建立单向连接。
  • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
  • 观察者允许接收者动态地订阅或取消接收请求。
  1. 中介者和观察者之间的区别往往很难记住。在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。让我们来看看如何做到这一点。
  • 中介者的主要目标是消除一系列系统组件之间的相互依赖。这些组件将依赖于同一个中介者对象。观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
  • 有一种流行的中介者模式实现方式依赖于观察者。中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。当中介者以这种方式实现时, 它可能看上去与观察者非常相似。
  • 当你感到疑惑时, 记住可以采用其他方式来实现中介者。例如, 你可永久性地将所有组件链接到同一个中介者对象。这种实现方式和观察者并不相同, 但这仍是一种中介者模式。
  • 假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

7. 状态模式

亦称:State

状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。

Java工程师的进阶之路 设计模式篇(三)

7.1. 适合场景

  1. 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。

模式建议你将所有特定于状态的代码抽取到一组独立的类中。这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。

  1. 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。

状态模式会将这些条件语句的分支抽取到相应状态类的方法中。同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。

  1. 当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。

状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。

7.2. 实现方式

  1. 确定哪些类是上下文。它可能是包含依赖于状态的代码的已有类;如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。

  2. 声明状态接口。虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。

  3. 为每个实际状态创建一个继承于状态接口的类。然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。你可以采用以下几种变通方式:

  • 将这些成员变量或方法设为公有。
  • 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。这种方式简陋却便捷, 你可以稍后再对其进行修补。
  • 将状态类嵌套在上下文类中。这种方式需要你所使用的编程语言支持嵌套类。
  1. 在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。

  2. 再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。

  3. 为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。你可以在上下文、 各种状态或客户端中完成这项工作。无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。

7.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。
  2. 状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。
  3. 具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。
  4. 上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。

7.4. 优点和缺点

  1. 单一职责原则。将与特定状态相关的代码放在单独的类中。
  2. 开闭原则。无需修改已有状态类和上下文就能引入新状态。
  3. 通过消除臃肿的状态机条件语句简化上下文代码。
  • 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。

7.5. 与其他模式的关系

  1. 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

  2. 状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

8. 策略模式

亦称:Strategy

策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。

Java工程师的进阶之路 设计模式篇(三)

8.1. 适合场景

  1. 当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。

策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。

  1. 当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。

策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。

  1. 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。

策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。

  1. 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。

策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。

8.2. 实现方式

  1. 从上下文类中找出修改频率较高的算法 (也可能是用于在运行时选择某个算法变体的复杂条件运算符)。

  2. 声明该算法所有变体的通用策略接口。

  3. 将算法逐一抽取到各自的类中, 它们都必须实现策略接口。

  4. 在上下文类中添加一个成员变量用于保存对于策略对象的引用。然后提供设置器以修改该成员变量。上下文仅可通过策略接口同策略对象进行交互, 如有需要还可定义一个接口来让策略访问其数据。

  5. 客户端必须将上下文类与相应策略进行关联, 使上下文可以预期的方式完成其主要工作。

8.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 上下文 (Context) 维护指向具体策略的引用, 且仅通过策略接口与该对象进行交流。
  2. 策略 (Strategy) 接口是所有具体策略的通用接口, 它声明了一个上下文用于执行策略的方法。
  3. 具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。
  4. 当上下文需要运行算法时, 它会在其已连接的策略对象上调用执行方法。 上下文不清楚其所涉及的策略类型与算法的执行方式。
  5. 客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。 上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。

8.4. 优点和缺点

  1. 你可以在运行时切换对象内的算法。
  2. 你可以将算法的实现和使用算法的代码隔离开来。
  3. 你可以使用组合来代替继承。
  4. 开闭原则。你无需对上下文进行修改就能够引入新的策略。
  • 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。
  • 客户端必须知晓策略间的不同——它需要选择合适的策略。
  • 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。

8.5. 与其他模式的关系

  1. 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

  2. 命令模式和策略看上去很像, 因为两者都能通过某些行为来参数化对象。但是, 它们的意图有非常大的不同。

  • 你可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
  • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
  1. 装饰模式可让你更改对象的外表, 策略则让你能够改变其本质。

  2. 模板方法模式基于继承机制:它允许你通过扩展子类中的部分内容来改变部分算法。策略基于组合机制:你可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作, 因此它是静态的。策略在对象层次上运作, 因此允许在运行时切换行为。

  3. 状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

9. 模板方法模式

亦称:Template Method

模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

Java工程师的进阶之路 设计模式篇(三)

9.1. 适合场景

  1. 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。

模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。

  1. 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。

在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。子类间各不同的代码可继续保留在子类中。

9.2. 实现方式

  1. 分析目标算法, 确定能否将其分解为多个步骤。从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。

  2. 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。在模板方法中根据算法结构依次调用相应步骤。可用 final最终修饰模板方法以防止子类对其进行重写。

  3. 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。

  4. 可考虑在算法的关键步骤之间添加钩子。

  5. 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。

9.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 抽象类 (Abstract­Class) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象类型, 也可以提供一些默认实现。
  2. 具体类 (Concrete­Class) 可以重写所有步骤, 但不能重写模板方法自身。

9.4. 优点和缺点

  1. 你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。
  2. 你可将重复代码提取到一个超类中。
  • 部分客户端可能会受到算法框架的限制。
  • 通过子类抑制默认步骤实现可能会导致违反_里氏替换原则_。
  • 模板方法中的步骤越多, 其维护工作就可能会越困难。

9.5. 与其他模式的关系

  1. 工厂方法模式是模板方法模式的一种特殊形式。同时, 工厂方法可以作为一个大型模板方法中的一个步骤。

  2. 模板方法基于继承机制:它允许你通过扩展子类中的部分内容来改变部分算法。策略模式基于组合机制:你可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作, 因此它是静态的。策略在对象层次上运作, 因此允许在运行时切换行为。

10. 访问者模式

亦称:Visitor

访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。

Java工程师的进阶之路 设计模式篇(三)

10.1. 适合场景

  1. 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。

访问者模式通过在访问者对象中为多个目标类提供相同操作的变体, 让你能在属于不同类的一组对象上执行同一操作。

  1. 可使用访问者模式来清理辅助行为的业务逻辑。

该模式会将所有非主要的行为抽取到一组访问者类中, 使得程序的主要类能更专注于主要的工作。

  1. 当某个行为仅在类层次结构中的一些类中有意义, 而在其他类中没有意义时, 可使用该模式。

你可将该行为抽取到单独的访问者类中, 只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。

10.2. 实现方式

  1. 在访问者接口中声明一组 “访问” 方法, 分别对应程序中的每个具体元素类。

  2. 声明元素接口。如果程序中已有元素类层次接口, 可在层次结构基类中添加抽象的 “接收” 方法。该方法必须接受访问者对象作为参数。

  3. 在所有具体元素类中实现接收方法。这些方法必须将调用重定向到当前元素对应的访问者对象中的访问者方法上。

  4. 元素类只能通过访问者接口与访问者进行交互。不过访问者必须知晓所有的具体元素类, 因为这些类在访问者方法中都被作为参数类型引用。

  5. 为每个无法在元素层次结构中实现的行为创建一个具体访问者类并实现所有的访问者方法。你可能会遇到访问者需要访问元素类的部分私有成员变量的情况。在这种情况下, 你要么将这些变量或方法设为公有, 这将破坏元素的封装;要么将访问者类嵌入到元素类中。后一种方式只有在支持嵌套类的编程语言中才可能实现。

  6. 客户端必须创建访问者对象并通过 “接收” 方法将其传递给元素。

10.3. 模式结构

Java工程师的进阶之路 设计模式篇(三)
  1. 访问者 (Visitor) 接口声明了一系列以对象结构的具体元素为参数的访问者方法。 如果编程语言支持重载, 这些方法的名称可以是相同的, 但是其参数一定是不同的。
  2. 具体访问者 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。
  3. 元素 (Element) 接口声明了一个方法来 “接收” 访问者。 该方法必须有一个参数被声明为访问者接口类型。
  4. 具体元素 (Concrete Element) 必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意, 即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法。
  5. 客户端 (Client) 通常会作为集合或其他复杂对象 (例如一个组合树) 的代表。 客户端通常不知晓所有的具体元素类, 因为它们会通过抽象接口与集合中的对象进行交互。

10.4. 优点和缺点

  1. 开闭原则。你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。
  2. 单一职责原则。可将同一行为的不同版本移到同一个类中。
  3. 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助。
  • 每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者。
  • 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。

10.5. 与其他模式的关系

  1. 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。

  2. 你可以使用访问者对整个组合模式树执行操作。

  3. 可以同时使用访问者和迭代器模式来遍历复杂数据结构, 并对其中的元素执行所需操作, 即使这些元素所属的类完全不同。

Java工程师的进阶之路 设计模式篇(一)
Java工程师的进阶之路 设计模式篇(二)
Java工程师的进阶之路 设计模式篇(三)

原文始发于微信公众号(白菜说技术):Java工程师的进阶之路 设计模式篇(三)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/172963.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!