结构型设计模式 (经典结构设计)

意图

复合模式是一种结构型设计模式,它允许您将对象组合成树形结构,然后可以像处理独立对象一样使用这些结构。

经典的设计模式案例,结构型设计模式

问题

仅当应用程序的核心模型可以表示为树形结构时,使用组合模式才有意义。

例如,假设您有两种类型的对象:产品(Products)和盒子(Boxes)。一个盒子可以包含多个产品以及一些较小的盒子。这些小盒子也可以容纳一些产品,甚至更小的盒子,以此类推。

假设您决定创建一个使用这些类的订购系统。订单可以包含没有任何包装的简单产品,以及装满产品...还有其他盒子的盒子。那么,您如何确定这样一个订单的总价格呢?

经典的设计模式案例,结构型设计模式

一个订单包含各种产品,产品被装在盒子中,而这些盒子又被装在更大的盒子中,整个结构看起来就像倒置的树。

您可以尝试直接的方法:拆开所有的盒子,遍历所有的产品,然后计算总价。在现实世界中,这是可行的;但在程序中,它并不像运行一个循环那么简单。您必须预先了解所遍历的产品和盒子的类别、盒子的嵌套级别以及其他繁琐的细节。所有这些使得直接的方法要么非常笨拙,要么甚至不可能实现。

解决方法

复合模式建议通过一个共同的接口来处理产品和盒子,该接口声明了一个计算总价格的方法。

这个方法将如何工作呢?对于一个产品,它将简单地返回产品的价格。对于一个盒子,它将遍历盒子包含的每个物品,询问它们的价格,然后返回这个盒子的总价。如果其中一个物品是一个较小的盒子,那个盒子也会开始遍历它的内容,以此类推,直到计算出所有内部组件的价格。盒子甚至可以在最终价格上添加一些额外的费用,如包装费用。

经典的设计模式案例,结构型设计模式

复合模式允许您对对象树的所有组件递归地运行某个行为。

真实世界类比

经典的设计模式案例,结构型设计模式

军事结构的一个例子。

大多数国家的*队军**都以层级结构组织。一个*队军**由多个师组成;一个师由几个旅组成,而一个旅则包含若干个连,连又可以进一步细分为班。最后,班是由一小组真实的士兵组成。命令从层级的顶端下达,逐级传达到每个层级,直到每个士兵知道需要做什么为止。

结构

经典的设计模式案例,结构型设计模式

1、组件接口描述了树的简单和复杂元素所共有的操作。

2、叶子是树的基本元素,不包含子元素。

通常情况下,叶子组件承担了大部分实际工作,因为它们没有其他组件可以委派工作。

3、容器(也称为组合体)是一个拥有子元素(叶子或其他容器)的元素。容器不知道其子元素的具体类别,只通过组件接口与所有子元素进行交互。

当接收到请求时,容器将工作委托给其子元素,处理中间结果,然后将最终结果返回给客户端。

4、客户端通过组件接口与所有元素进行交互。因此,客户端可以以相同的方式处理树的简单或复杂元素。

伪代码

在这个例子中,组合模式允许您在图形编辑器中实现几何形状的堆叠。

经典的设计模式案例,结构型设计模式

在这个例子中,组合模式允许您在图形编辑器中实现几何形状的堆叠。

CompoundGraphic 类是一个容器,可以包含任意数量的子形状,包括其他复合形状。复合形状具有与简单形状相同的方法。然而,复合形状不会自行执行操作,而是将请求递归地传递给其所有子形状,并“汇总”结果。

客户端代码通过与所有形状类共同的单一接口来处理所有形状。因此,客户端不知道它是在处理简单形状还是复合形状。客户端可以处理非常复杂的对象结构,而无需与构成该结构的具体类耦合。

// The component interface declares common operations for both
// simple and complex objects of a composition.
interface Graphic is
    method move(x, y)
    method draw()

// The leaf class represents end objects of a composition. A
// leaf object can't have any sub-objects. Usually, it's leaf
// objects that do the actual work, while composite objects only
// delegate to their sub-components.
class Dot implements Graphic is
    field x, y

    constructor Dot(x, y) { ... }

    method move(x, y) is
        this.x += x, this.y += y

    method draw() is
        // Draw a dot at X and Y.

// All component classes can extend other components.
class Circle extends Dot is
    field radius

    constructor Circle(x, y, radius) { ... }

    method draw() is
        // Draw a circle at X and Y with radius R.

// The composite class represents complex components that may
// have children. Composite objects usually delegate the actual
// work to their children and then "sum up" the result.
class CompoundGraphic implements Graphic is
    field children: array of Graphic

    // A composite object can add or remove other components
    // (both simple or complex) to or from its child list.
    method add(child: Graphic) is
        // Add a child to the array of children.

    method remove(child: Graphic) is
        // Remove a child from the array of children.

    method move(x, y) is
        foreach (child in children) do
            child.move(x, y)

    // A composite executes its primary logic in a particular
    // way. It traverses recursively through all its children,
    // collecting and summing up their results. Since the
    // composite's children pass these calls to their own
    // children and so forth, the whole object tree is traversed
    // as a result.
    method draw() is
        // 1. For each child component:
        //     - Draw the component.
        //     - Update the bounding rectangle.
        // 2. Draw a dashed rectangle using the bounding
        // coordinates.


// The client code works with all the components via their base
// interface. This way the client code can support simple leaf
// components as well as complex composites.
class ImageEditor is
    field all: CompoundGraphic

    method load() is
        all = new CompoundGraphic()
        all.add(new Dot(1, 2))
        all.add(new Circle(5, 3, 10))
        // ...

    // Combine selected components into one complex composite
    // component.
    method groupSelected(components: array of Graphic) is
        group = new CompoundGraphic()
        foreach (component in components) do
            group.add(component)
            all.remove(component)
        all.add(group)
        // All components will be drawn.
        all.draw()

适用范围

当你需要实现一个类似树形结构的对象结构时,可以使用组合模式。

组合模式提供了两种基本元素类型,它们共享一个公共接口:简单叶子和复杂容器。容器可以由叶子和其他容器组成。这使得你可以构建一个嵌套的递归对象结构,类似于树形结构。

当你希望客户端代码以统一的方式处理简单和复杂元素时,可以使用该模式。

组合模式中定义的所有元素都共享一个公共接口。通过使用这个接口,客户端代码不需要关心它所处理的对象的具体类。

如何实现

1、确保你的应用程序的核心模型可以表示为树形结构。尝试将其拆分为简单元素和容器。记住,容器必须能够包含简单元素和其他容器。

2、使用一组对于简单和复杂组件都有意义的方法声明组件接口。

3、创建一个叶子类来表示简单元素。一个程序可能会有多个不同的叶子类。

4、创建一个容器类来表示复杂元素。在这个类中,提供一个数组字段来存储对子元素的引用。这个数组必须能够存储叶子和容器,所以确保它是用组件接口类型声明的。

在实现组件接口的方法时,记住容器应该将大部分工作委托给子元素。

5、最后,定义容器中添加和删除子元素的方法。

请记住,这些操作可以在组件接口中声明。这可能会违反接口隔离原则,因为叶子类中的方法将为空。然而,客户端在组合树时仍然能够以相同的方式对待所有元素。

Python示例

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Component(ABC):
    """
    The base Component class declares common operations for both simple and
    complex objects of a composition.
    """

    @property
    def parent(self) -> Component:
        return self._parent

    @parent.setter
    def parent(self, parent: Component):
        """
        Optionally, the base Component can declare an interface for setting and
        accessing a parent of the component in a tree structure. It can also
        provide some default implementation for these methods.
        """

        self._parent = parent

    """
    In some cases, it would be beneficial to define the child-management
    operations right in the base Component class. This way, you won't need to
    expose any concrete component classes to the client code, even during the
    object tree assembly. The downside is that these methods will be empty for
    the leaf-level components.
    """

    def add(self, component: Component) -> None:
        pass

    def remove(self, component: Component) -> None:
        pass

    def is_composite(self) -> bool:
        """
        You can provide a method that lets the client code figure out whether a
        component can bear children.
        """

        return False

    @abstractmethod
    def operation(self) -> str:
        """
        The base Component may implement some default behavior or leave it to
        concrete classes (by declaring the method containing the behavior as
        "abstract").
        """

        pass


class Leaf(Component):
    """
    The Leaf class represents the end objects of a composition. A leaf can't
    have any children.

    Usually, it's the Leaf objects that do the actual work, whereas Composite
    objects only delegate to their sub-components.
    """

    def operation(self) -> str:
        return "Leaf"


class Composite(Component):
    """
    The Composite class represents the complex components that may have
    children. Usually, the Composite objects delegate the actual work to their
    children and then "sum-up" the result.
    """

    def __init__(self) -> None:
        self._children: List[Component] = []

    """
    A composite object can add or remove other components (both simple or
    complex) to or from its child list.
    """

    def add(self, component: Component) -> None:
        self._children.append(component)
        component.parent = self

    def remove(self, component: Component) -> None:
        self._children.remove(component)
        component.parent = None

    def is_composite(self) -> bool:
        return True

    def operation(self) -> str:
        """
        The Composite executes its primary logic in a particular way. It
        traverses recursively through all its children, collecting and summing
        their results. Since the composite's children pass these calls to their
        children and so forth, the whole object tree is traversed as a result.
        """

        results = []
        for child in self._children:
            results.append(child.operation())
        return f"Branch({'+'.join(results)})"


def client_code(component: Component) -> None:
    """
    The client code works with all of the components via the base interface.
    """

    print(f"RESULT: {component.operation()}", end="")


def client_code2(component1: Component, component2: Component) -> None:
    """
    Thanks to the fact that the child-management operations are declared in the
    base Component class, the client code can work with any component, simple or
    complex, without depending on their concrete classes.
    """

    if component1.is_composite():
        component1.add(component2)

    print(f"RESULT: {component1.operation()}", end="")


if __name__ == "__main__":
    # This way the client code can support the simple leaf components...
    simple = Leaf()
    print("Client: I've got a simple component:")
    client_code(simple)
    print("\n")

    # ...as well as the complex composites.
    tree = Composite()

    branch1 = Composite()
    branch1.add(Leaf())
    branch1.add(Leaf())

    branch2 = Composite()
    branch2.add(Leaf())

    tree.add(branch1)
    tree.add(branch2)

    print("Client: Now I've got a composite tree:")
    client_code(tree)
    print("\n")

    print("Client: I don't need to check the components classes even when managing the tree:")
    client_code2(tree, simple)