2233 字
11 分钟
HIT-软件构造 | 设计模式选讲

装饰器模式#

假设我们要渲染一段 HTML 文本,HTML 可以标记文本的附加效果,比如一段加粗、加删除线的文本的 HTML 实现可以是:

<del><b>Hello World</b></del>

效果: Hello World

当然,现代 Web 前端技术主张使用 CSS 实现这些样式。

实现类似附加效果最 naive 的方法可以是直接写一个类来实现:

interface TextNode {
    String getText();
}

class BlackNode implements TextNode {
    private String text;
    public void BlackNode(String text) {
        this.text = text;
    }
    public String getText() {
        return "<b>" + text + "</b>";
    }
}

public class Main {
    public static void main(String[] args) {
        TextNode textNode = new BlackNode("Hello World");
        System.out.println(textNode.getText());
    }
}

如果我们想给黑体文本加上下划线,可以新建一个类:

class DeleteBlackNode implements TextNode {
    private String text;
    public void DeleteBlackNode(String text) {
        this.text = text;
    }
    public String getText() {
        return "<del><b>" + text + "</b></del>";
    }
}

但是这样将导致一个严重的问题,一段文本可以添加的效果有粗体、斜体、下划线、删除线、字号、颜色等,这些效果可以任意组合,如果给任意一种组合编写一个类,子类数量会爆炸性增长。

装饰器模式的目的是,一个子类只负责一个效果,通过委托的方式将这种效果累加到已经添加好其他效果的文本上。

为了更好地理解装饰器模式,以上述例子为例,我们希望写出的类是这样的(其中 target 是一个已经添加好部分效果的 TextNode):

class DelTextNode implements TextNode {
    DelTextNode(TextNode textNode) {
        this.target = textNode;
    }
    public String getText() {
        return "<del>" + target.getText() + "</del>";
    }
}

public class Main {
    public static void main(String[] args) {
        TextNode textNode = ...
        ...
        // textNode 是已经添加好一些效果的 TextNode
        // 假设是 <b>Hello World</b>
        // 现在我们想要往上面添加删除线效果
        TextNode delTextNode = new DelTextNode(textNode);
        System.out.println(delTextNode.getText());
        // 将输出 <del><b>Hello World</b></del>
    }
}

我们发现每一个效果类对外提供的接口都是一样的,不妨将其设成一个装饰器类:

public abstract class NodeDecorator implements TextNode {
    protected final TextNode target;

    protected NodeDecorator(TextNode target) {
        this.target = target;
    }
}

去除所有效果后剩下的是一段文本,这需要一个类实现,我们称为核心类:

public class TextNodeCore implements TextNode {
    private String text;

    public void TextNodeCore(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

然后将之前提到的所有效果类修改为 NodeDecorator 装饰器的子类:

class DelTextNode extends NodeDecorator {
    DelTextNode(TextNode target) {
        super(target)
    }
    public String getText() {
        return "<del>" + target.getText() + "</del>";
    }
}
class BlackTextNode extends NodeDecorator {
    DelTextNode(TextNode target) {
        super(target)
    }
    public String getText() {
        return "<b>" + target.getText() + "</b>";
    }
}

要渲染一段加粗、加删除线效果的文本可以这样写:

public class Main {
    public static void main(String[] args) {
        TextNode textNode = new TextNodeCore("Hello World");
        TextNode delBlackTextNode = 
            new DelTextNode(new BlackTextNode(textNode));
        System.out.println(delBlackTextNode.getText());
        // <del><b>Hello World</b></del>
    }
}

访问者模式#

访问者模式是最复杂的设计模式之一,概念十分抽象,我们直接从例子切入:

一家公司有工程师(Engineer)和经理(Manager)两种职务,他们实现了 Staff 接口。

工程师的工作指标有代码量和 KPI;经理的工作指标有产品数量和 KPI,假设它们的类如下所示:


public abstract class Staff { // 员工

    private String name;
    private int kpi;// 员工KPI
    ...
}
public class Engineer extends Staff { // 工程师
    private int codeAmount;
    ...
}
public class Manager extends Staff { // 经理
    private int productionAmount;
    ...
}

年底绩效考核时,公司的 CEO 只关注工程师的代码量和经理的产品数量,CTO 只关注工程师和经理的KPI。

所有工程师和经理都放在一个 List<Staff> 中,CEO 和 CTO 对这些数据的关注点是不一样的。

一种 naive 的做法是:

public class Main {
    public static void main(String[] args) {
        List<Staff> staffList = new ArrayList<>();
        ...
        for(Staff staff : staffList) {
            cto.visit(staff);
            ceo.visit(staff);
        }
    }
}
interface ChiefOfficer {
    void visit(Staff staff);
}
class CEO implements ChiefOfficer {
    @Override
    private void visit(Staff staff) {
        if (staff instanceof Manager) {
            Manager manager = (Manager) staff;
            // CEO 看经理数据
        } else if (staff instanceof Engineer) {
            Engineer engineer = (Engineer) staff;
            // CEO 看工程师数据
        }
    }
}
class CTO implements ChiefOfficer {
    @Override
    private void visit(Staff staff) {
        if (staff instanceof Manager) {
            // CTO 看经理数据
        } else if (staff instanceof Engineer) {
            // CTO 看工程师数据
        }
    }
}

这种方法的痛点在于 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护。如果用户类型很多,Chief Officer 也很多,每个人的 visit 方法内的逻辑就会非常复杂。

访问者(Visitor)模式能解决这个问题。其中 visit 指的是对数据的,本例中的 Staff 就是数据。

本例中的 Chief Officer 就是所谓的访问者,我们只需要对其稍加修改:

public interface Visitor {
    void visit(Engineer engineer); // 访问工程师类型
    void visit(Manager manager); // 访问经理类型
}
// CTO访问者
public class CTOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
        // CTO 看工程师数据
    }
    @Override
    public void visit(Manager manager) {
        // CTO 看经理数据
    }
}
// CEO访问者
public class CEOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
        // CEO 看工程师数据
    }
    @Override
    public void visit(Manager manager) {
        // CEO 看经理数据
    }
}

但是这样还不够,我们不希望在客户端直接调用 visit 方法,而是转移到被访问的对象的 accept 方法里使用:

public abstract class Staff {
    private String name;
    private int kpi;// 员工KPI
    public abstract void accept(Visitor visitor);
    ...
}
public class Engineer extends Staff {
    private int codeAmount;
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    ...
}
public class Manager extends Staff {
    private int productionAmount;
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    ...
}

当被访问对象存在特殊的数据结构时,客户端并不关心这些结构,只有被访问者才知道以何种方式调用 visit 方法。也就是说,实际环境中 accept 方法中可能不止 visitor.visit(this); 这样简单的逻辑,可能有其他操作,如果不将调用 visit 方法的责任交给被访问对象,这些逻辑就会写在客户端中,要知道我们可能有很多种被访问对象,届时客户端会变得非常复杂。

观察者模式#

在前端领域常用的 MVC 架构模式中,有三个重要的组件:Model、View 和 Controller。图中实线箭头表示模块间的依赖关系,虚线箭头表示数据流向。用户点击 UI 上的某个按钮时,Controller 将相应这个点击事件,调用 Model 更新 UI 状态,然后 Controller 重新渲染页面。

图 2

此处有一个重要问题:Controller 如何监听用户对 View 的操作?即如何让用户在点击某个按钮或做出其他操作时,Controller 能做出相应反应?有两种 Naive 的做法:

  1. View 维护 UI 组件的状态,如用户点击按钮时标记被点击的组件;Controller 定期持续轮询所有 UI 组件,当发现被组件被标记点击时,调用相应处理函数 callbackFunction
  2. View 直接依赖 Controller ,持有 Controller 实例,触发 UI 事件时,View 直接调用对应函数。

这两种做法都有严重问题:

  • 做法 11 :Controller 持续轮询 UI 组件性能太差,且如果用户在一个轮询周期内做出两个相互依赖的操作,将导致时序问题。
  • 做法 22 :这严重违背 MVC 模式的设计初衷,导致 Controller 和 View 循环依赖,相互耦合。

解决方案是使用观察者模式,对于一个 UI 组件,View 只持有 callbackFunction 的引用,而不是整个 Controller ,使用时只需要确定好 callbackFunction 的接口规约,就能实现 Controller 对 View 的单向依赖,即 View 并不知道 Controller 的存在,实现解耦。

以一个 Web 前端菜谱管理页面为例,我们要实现通过一个按钮添加菜谱的功能,点击 Add Recipe 按钮,AddRecipeView 组件将获得用户输入的菜谱数据 data ,我们需要让 Controller 监听这一事件,并让 Controller 将原始 data 传递给 Model 。

View 部分:

// view.js
class View {
    ...
    // View 基类的实现省略
}

class AddRecipeView extends View {
    ...
    // AddRecipeView 其他部分省略

    addHandlerUpload(handler) {
        this._paretElement.addEventListener("submit", function(e) {
            // 只保留 data 传递的业务逻辑
            ...
            const data = ...
            ...
            handler(data);
        });
    }
}

export default new AddRecipeView();

Controller 部分

// controller.js

import * as model from "./model.js";
import { addRecipeView } from "view.js";

...

const controlAddRecipe = async function (newRecipe) {
    // Controller 响应事件,上传、更新数据、重新渲染页面
    // 省略部分逻辑
    try {
        console.log(newRecipe);
        // Show loading spinner
        addRecipeView.renderSpinner();

        // Upload new recipe data
        await model.uploadRecipe(newRecipe);
        console.log(model.state.recipe);

        // Render recipe
        recipeView.render(model.state.recipe);

        // Succes message
        addRecipeView.renderMessage();

        // Render bookmarksView
        bookmarksView.render(model.state.bookmarks);

        ...

    } catch (err) {
        console.error(err);
        addRecipeView.renderError(err.message);
    }
};


const init = function () {

    ...

    addRecipeView.addHandlerUpload(controlAddRecipe);
};

init();

Model 部分

// model.js

export const uploadRecipe = async function (newRecipe) {
    ...
    // 上传菜谱的逻辑
}

责任链模式#

责任链模式(Chain of Responsibility)是一种处理请求的模式。它使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

HIT-软件构造 | 设计模式选讲
https://blog.vonbrank.com/posts/hit-software-construction-design-pattern/
作者
Von Brank
发布于
2022-05-10
许可协议
CC BY-NC-SA 4.0