设计模式-组合模式

eve2333 发布于 4 小时前 3 次阅读


这是一个新的工程。我们创建一个接口叫 `PopulationNode`,它有一个方法 `computePopulation()`,表示可以计算某个区域的人口数量。

哪些对象可以具有“人口”这个概念?国家、城市、区、小区、甚至一栋楼,都可以是一个人口节点。我们用组合模式来统一处理这种树形结构。

我们先写一个 `City` 类,实现 `PopulationNode` 接口。`City` 有名字,并且包含多个“区”(`District`)。每个 `District` 也实现了 `PopulationNode`,表示一个行政区域,它也有名字和固定的人口数。

```java
public class District implements PopulationNode {
    private final String name;
    private final int population;

    public District(String name, int population) {
        this.name = name;
        this.population = population;
    }

    @Override
    public int computePopulation() {
        return population;
    }
}
```

`City` 持有一个 `List<District>`,并提供 `addDistrict(District)` 方法来添加区。它的 `computePopulation()` 实现为所有区的人口之和:

```java
public class City implements PopulationNode {
    private final String name;
    private final List<District> districts = new ArrayList<>();

    public void addDistrict(District district) {
        districts.add(district);
    }

    @Override
    public int computePopulation() {
        return districts.stream()
                       .mapToInt(PopulationNode::computePopulation)
                       .sum();
    }
}
```

同理,我们再写一个 `Province`(省)类,它不直接管理区,而是管理多个城市:

```java
public class Province implements PopulationNode {
    private final String name;
    private final List<City> cities = new ArrayList<>();

    public void addCity(City city) {
        cities.add(city);
    }

    @Override
    public int computePopulation() {
        return cities.stream()
                     .mapToInt(PopulationNode::computePopulation)
                     .sum();
    }
}
```

在 `main` 函数中,我们可以这样组装数据:

```java
Province heilongjiang = new Province("黑龙江");
City harbin = new City("哈尔滨");
harbin.addDistrict(new District("南岗区", 1000000));
harbin.addDistrict(new District("道里区", 800000));
heilongjiang.addCity(harbin);

City jiamusi = new City("佳木斯");
jiamusi.addDistrict(new District("向阳区", 300000));
heilongjiang.addCity(jiamusi);

System.out.println(heilongjiang.computePopulation()); // 输出总人口
```

这就是组合模式的典型应用:**无论是叶子节点(如 `District`)还是容器节点(如 `City`、`Province`),都实现同一个接口 `PopulationNode`**。客户端无需关心当前操作的是单个对象还是组合对象,统一调用 `computePopulation()` 即可。

我们打个断点观察数据结构:`Province` 包含多个 `City`,每个 `City` 又包含多个 `District`。整个结构像一棵树,根节点是省,中间节点是城市,叶子节点是区。这正是组合模式的核心思想——树形结构的统一处理。

---

我们再看另一个例子:计算器。我们想计算表达式 `1 + 2 * 3 + 4`,甚至更复杂的带括号的表达式。

我们可以把表达式看作一棵树。比如 `1 + 2` 是一个加法节点,左右子节点分别是数字 1 和 2。`2 * 3` 是乘法节点,以此类推。

我们定义一个接口 `Expression`,它有一个方法 `getValue()`:

```java
public interface Expression {
    int getValue();
}
```

叶子节点是 `NumberExpression`,表示一个数字:

```java
public class NumberExpression implements Expression {
    private final int value;

    public NumberExpression(int value) {
        this.value = value;
    }

    @Override
    public int getValue() {
        return value;
    }
}
```

非叶子节点是操作符表达式。由于加减乘除都是二元操作符,我们先写一个抽象基类 `BinaryOperatorExpression`:

```java
public abstract class BinaryOperatorExpression implements Expression {
    protected final Expression left;
    protected final Expression right;

    protected BinaryOperatorExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
}
```

然后具体实现 `AddExpression`、`SubtractExpression`、`MultiplyExpression`、`DivideExpression`:

```java
public class AddExpression extends BinaryOperatorExpression {
    public AddExpression(Expression left, Expression right) {
        super(left, right);
    }

    @Override
    public int getValue() {
        return left.getValue() + right.getValue();
    }
}
```

其他操作符类似。

现在我们可以手动构建表达式树:

```java
Expression one = new NumberExpression(1);
Expression two = new NumberExpression(2);
Expression three = new NumberExpression(3);
Expression four = new NumberExpression(4);

Expression add1 = new AddExpression(one, two);           // 1 + 2
Expression mul = new MultiplyExpression(two, three);      // 2 * 3
Expression add2 = new AddExpression(add1, mul);           // (1+2) + (2*3)
Expression total = new AddExpression(add2, four);         // 上述结果 + 4

System.out.println(total.getValue());
```

打个断点可以看到,整个表达式是一棵由 `Expression` 节点构成的树。组合模式让这种结构天然成立。

---

但问题来了:如何把一个字符串表达式(如 `"1 + 2 * (3 + 4)"`)自动解析成这棵树?

这就涉及到表达式解析。我们知道:

- **中缀表达式**:运算符在中间,如 `1 + 2`,人类易读,计算机难解析。
- **后缀表达式(逆波兰式)**:运算符在后面,如 `1 2 +`,计算机易处理。

计算机通常将中缀转为后缀,再计算。例如:

中缀:`(1 + 2) * (3 + 4)`  
后缀:`1 2 + 3 4 + *`

后缀表达式的计算很简单:用一个栈,从左到右扫描:

- 遇到数字,入栈;
- 遇到操作符,弹出两个数进行运算,结果再入栈;
- 最后栈中只剩一个数,就是结果。

那么如何将中缀转后缀?规则如下:

1. 遇到操作数,直接加入输出;
2. 遇到 `(`,入栈;
3. 遇到 `)`,将栈中元素弹出到输出,直到遇到 `(`;
4. 遇到操作符,比较优先级:若栈顶操作符优先级 ≥ 当前操作符,则弹出栈顶到输出,直到不满足;然后将当前操作符入栈;
5. 扫描结束后,将栈中剩余操作符全部弹出到输出。

我们实现一个 `ExpressionParser` 类:

```java
public class ExpressionParser {
    private final String infixExpression;

    public ExpressionParser(String expr) {
        this.infixExpression = expr.replaceAll("\\s+", ""); // 去空格
    }

    public List<String> toPostfix() {
        List<String> output = new ArrayList<>();
        Deque<String> stack = new ArrayDeque<>();
        int i = 0;

        while (i < infixExpression.length()) {
            char c = infixExpression.charAt(i);

            if (Character.isDigit(c)) {
                StringBuilder num = new StringBuilder();
                while (i < infixExpression.length() && Character.isDigit(infixExpression.charAt(i))) {
                    num.append(infixExpression.charAt(i++));
                }
                output.add(num.toString());
                i--; // 因为 for 循环会 ++i
            } else if (c == '(') {
                stack.push(String.valueOf(c));
            } else if (c == ')') {
                while (!stack.isEmpty() && !stack.peek().equals("(")) {
                    output.add(stack.pop());
                }
                stack.pop(); // 移除 '('
            } else if (isOperator(c)) {
                while (!stack.isEmpty() && precedence(stack.peek()) >= precedence(String.valueOf(c))) {
                    output.add(stack.pop());
                }
                stack.push(String.valueOf(c));
            } else {
                throw new IllegalArgumentException("非法字符: " + c);
            }
            i++;
        }

        while (!stack.isEmpty()) {
            output.add(stack.pop());
        }

        return output;
    }

    private boolean isOperator(char c) {
        return c == '+' || c == '-' || c == '*' || c == '/';
    }

    private int precedence(String op) {
        return switch (op) {
            case "+", "-" -> 1;
            case "*", "/" -> 2;
            default -> 0;
        };
    }
}
```

接下来,我们将后缀表达式转换为 `Expression` 对象树:

```java
public Expression parse() {
    List<String> postfix = toPostfix();
    Deque<Expression> stack = new ArrayDeque<>();

    for (String token : postfix) {
        if (isOperator(token.charAt(0))) {
            Expression right = stack.pop();
            Expression left = stack.pop();
            Expression expr = switch (token) {
                case "+" -> new AddExpression(left, right);
                case "-" -> new SubtractExpression(left, right);
                case "*" -> new MultiplyExpression(left, right);
                case "/" -> new DivideExpression(left, right);
                default -> throw new IllegalArgumentException();
            };
            stack.push(expr);
        } else {
            int value = Integer.parseInt(token);
            stack.push(new NumberExpression(value));
        }
    }

    return stack.pop();
}
```

使用方式:

```java
ExpressionParser parser = new ExpressionParser("1 + 2 * (3 + 4)");
Expression expr = parser.parse();
System.out.println(expr.getValue()); // 输出 15
```

打个断点可以看到,最终生成的是一棵完整的表达式树,完全由组合模式构建。

---

思考题:  
你能实现一个支持“上下文”的计算器吗?比如支持变量定义(`x = 5`)、函数调用(`sqrt(16)`),甚至记忆上次计算结果?这需要扩展 `Expression` 接口,引入环境上下文(`Map<String, Integer>`)作为 `getValue(Context context)` 的参数。