组合模式(结构型模式)
模式概述
组合模式是一种结构型设计模式,它可以将对象组合成树状结构,并且能像使用独立对象一样使用它们。
问题
假如存在两类对象:产品和盒子。一个盒子中可以包含多个产品或者几个较小的盒子。而小盒子中同样可以包含一些产品或更小的盒子,以此类推。

如果希望在这些类的基础上开发一个定购系统,订单中可以包含无包装的简单产品,也可以包含装满产品的盒子,以及其他盒子。
那么此时该如何计算每张订单的总价呢?
方案
组合模式建议使用一个通用接口来与产品和盒子交互,并且在该接口中声明一个计算总价的方法。
对于一个产品,该方法直接返回其价格;对于一个盒子,该方法遍历盒子中的所有项目,询问每个项目的价格,然后返回该盒子的总价格。
如果其中某个项目是小一号的盒子,那么当前盒子也会遍历其中的所有项目,以此类推,直到计算出所有内部组成部分的价格。
甚至可以在盒子的最终价格中增加额外费用,作为该盒子的包装费用。
结构

实现
Java
/**
* 通用形状接口
*
*/
public interface Shape {
int getX();
int getY();
int getWidth();
int getHeight();
}
/**
* 提供基本功能的抽象形状
*
*/
abstract class BaseShape implements Shape {
public int x;
public int y;
public Color color;
BaseShape(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public int getWidth() {
return 0;
}
@Override
public int getHeight() {
return 0;
}
}
/**
* 圆形
*
*/
public class Circle extends BaseShape {
public int radius;
public Circle(int x, int y, int radius, Color color) {
super(x, y, color);
this.radius = radius;
}
@Override
public int getWidth() {
return radius * 2;
}
@Override
public int getHeight() {
return radius * 2;
}
}
/**
* 三角形
*
*/
public class Rectangle extends BaseShape {
public int width;
public int height;
public Rectangle(int x, int y, int width, int height, Color color) {
super(x, y, color);
this.width = width;
this.height = height;
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
}
/**
* 复合形状
*
*/
public class CompoundShape extends BaseShape {
protected List<Shape> children = new ArrayList<>();
public CompoundShape(Shape... components) {
super(0, 0, Color.BLACK);
add(components);
}
public void add(Shape component) {
children.add(component);
}
public void add(Shape... components) {
children.addAll(Arrays.asList(components));
}
public void remove(Shape child) {
children.remove(child);
}
public void remove(Shape... components) {
children.removeAll(Arrays.asList(components));
}
public void clear() {
children.clear();
}
@Override
public int getX() {
if (children.size() == 0) {
return 0;
}
int x = children.get(0).getX();
for (Shape child : children) {
if (child.getX() < x) {
x = child.getX();
}
}
return x;
}
@Override
public int getY() {
if (children.size() == 0) {
return 0;
}
int y = children.get(0).getY();
for (Shape child : children) {
if (child.getY() < y) {
y = child.getY();
}
}
return y;
}
@Override
public int getWidth() {
int maxWidth = 0;
int x = getX();
for (Shape child : children) {
int childsRelativeX = child.getX() - x;
int childWidth = childsRelativeX + child.getWidth();
if (childWidth > maxWidth) {
maxWidth = childWidth;
}
}
return maxWidth;
}
@Override
public int getHeight() {
int maxHeight = 0;
int y = getY();
for (Shape child : children) {
int childsRelativeY = child.getY() - y;
int childHeight = childsRelativeY + child.getHeight();
if (childHeight > maxHeight) {
maxHeight = childHeight;
}
}
return maxHeight;
}
}
/**
* 形状编辑器
*
*/
public class ImageEditor {
private EditorCanvas canvas;
private CompoundShape allShapes = new CompoundShape();
public ImageEditor() {
canvas = new EditorCanvas();
}
public void loadShapes(Shape... shapes) {
allShapes.clear();
allShapes.add(shapes);
}
private class EditorCanvas extends Canvas {
JFrame frame;
private static final int PADDING = 10;
EditorCanvas() {
createFrame();
refresh();
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
allShapes.selectChildAt(e.getX(), e.getY());
e.getComponent().repaint();
}
});
}
void createFrame() {
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
JPanel contentPanel = new JPanel();
Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);
contentPanel.setBorder(padding);
frame.setContentPane(contentPanel);
frame.add(this);
frame.setVisible(true);
frame.getContentPane().setBackground(Color.LIGHT_GRAY);
}
public int getWidth() {
return allShapes.getX() + allShapes.getWidth() + PADDING;
}
public int getHeight() {
return allShapes.getY() + allShapes.getHeight() + PADDING;
}
void refresh() {
this.setSize(getWidth(), getHeight());
frame.pack();
}
}
}
/**
* 客户端
*
*/
public class Client {
public static void main(String[] args) {
ImageEditor editor = new ImageEditor();
editor.loadShapes(
new Circle(10, 10, 10, Color.BLUE),
new CompoundShape(
new Circle(110, 110, 50, Color.RED),
),
new CompoundShape(
new Rectangle(250, 250, 100, 100, Color.GREEN),
)
);
}
}
Go
package main
import "fmt"
/**
* 组件接口
*
*/
type Component interface {
search(string)
}
/**
* 文件夹
*
*/
type Folder struct {
components []Component
name string
}
func (f *Folder) search(keyword string) {
fmt.Printf("文件夹 %s 的递归搜索关键字是 %s\n", f.name, keyword)
for _, composite := range f.components {
composite.search(keyword)
}
}
func (f *Folder) add(c Component) {
f.components = append(f.components, c)
}
/**
* 文件
*
*/
type File struct {
name string
}
func (f *File) search(keyword string) {
fmt.Printf("文件 %s 搜索关键字是 %s\n", f.name, keyword)
}
func (f *File) getName() string {
return f.name
}
/**
* 客户端
*
*/
func main() {
file1 := &File{name: "File1"}
file2 := &File{name: "File2"}
file3 := &File{name: "File3"}
folder1 := &Folder{
name: "Folder1",
}
folder1.add(file1)
folder2 := &Folder{
name: "Folder2",
}
folder2.add(file2)
folder2.add(file3)
folder2.add(folder1)
folder2.search("rose")
}
适用场景
如果需要实现树型嵌套递归对象结构,就可以使用组合模式来构建。
如果希望客户端以相同方式处理简单和复杂元素,可以使用组合模式。
优缺点
组合模式的优点。
可以利用多态和递归机制方便地构建和使用复杂的树型结构。
无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分,这符合
开闭原则
。
组合模式的缺点。
- 如果某些类功能差异较大,提供公共接口或许会有困难,也会让它变得难以理解。
相关性
组合模式
和修饰器模式
的结构图相似,因为两者都依赖递归和组合来组织无限数量的对象。可以使用
享元模式
实现组合模式
树的共享叶节点以节省内存。可以使用
访问者模式
对整个组合模式
树执行操作。可以使用
迭代器模式
来遍历组合模式
构建的树。桥接模式
、状态模式
、策略模式
和适配器模式
较为相似,它们都基于组合模式
机制——将工作委派给其他对象来解决问题。可以在创建
组合模式
时使用构建器模式
,这可使其以递归的方式构造运行。责任链模式
通常和组合模式
结合使用。当叶组件接收到请求后,可以将请求沿包含全体父组件的链一直传递至对象树的底部。可以通过
原型模式
来控制组合模式
和修饰器模式
的复杂结构。
感谢支持
更多内容,请移步《超级个体》。