设计模式学习笔记(十一):组合模式

1 概述

1.1 概述

对于树形结构,比如文件目录,一个文件夹中可以包含多个文件夹和文件,而一个文件中不能在包含子文件或者子文件夹,在这里可以称文件夹为容器,称文件为叶子

在树形结构中? C 2 ] p , 0 a E,当容器对象(比如文件夹)的某个方法被调用时,将遍历整个文件夹,寻找也包含这个方法的成员对象(容器对象或叶子对象)并调用执行。由于容器对象以及叶子对象在功能上的区别,使用这些对象的? Y C T P . +代码中必须有区别对待容器对象以及叶子对象,但大多数情况下需要一致性处理它们。

组合模式为解决此类T u ` ; h 0 w G问题而生,它可以让叶子对象以及容器对象的使用具有一致性。

1.2 定义

组合模式:组合多个对象形成树形结构以表示具有“整体-部分”关系的层次结构。组合模式对单个对象(叶子对象)和y { W ! 3 组合对象(容 O A d A W器对象)的使2 M r h & : [用具有一致性。
组合模式f s R i I又叫“部分-整体”模式,它是一种对象结构型模式。

1.3 结构图

设计模式学习笔记(十一):组合模式

1.4 角色

  • Component(抽象构件):可以是接口或者抽象类,为叶子构件和容器构件对象声明接口,在该角色Z T S G F @ v #中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问以及管理它的子构件的方法,例如增加/删除/获取子构件
  • Leaf(叶子构件):表示叶子节点对象,叶子节点没有子节点,它实现了在抽G 4 h象构件中定义的行为,对于访问以及管理子[ ! q构件的方法,通常会抛出异常
  • Composite(容器构件):表示容器节点对象,容器节点包+ x k e r含子节点,其子节点可以是叶子节点x ( 2 . , V ] F W,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括访问以及管理子构件的方法

2 典型实现

2.1 步骤

组合模式的关键是定义w ( m f C ? ? v .了一个抽象构件类,它既可以表示叶子也可以表示容器,客户端针对该抽象构件进行编程,G t c n V z K Y无须知道到底是叶子还是容器,同时容器对象! M 6 x _ A j与抽象构件之间需要建立一W h y $ i 4 !个聚合关联关系,在容器对象中既可以包含叶子也可以包含容器,以此实现递归组合,形成树形结构。

因此首先需要定义抽象构件类,通用步骤如下:

  • 定义抽象构件:定义抽象构件类,添加四个基本方法:增加/删除/获取成员+业务方法,可以将抽象构件类定义为抽象类或者接口
  • 定义叶子构件:继承或实现w [ 4 .抽象构件类,覆盖或实现具体业务方法,; q S )同时对于管理或访问子构件的方法提供异常处理或错误提示
  • 定义容器构件:h 8 1 { m继承或实现抽象构件类,覆盖或实现抽象构件中的所有方法,一般来说容器构件会包含一个集合私有成员用于保存抽象构件,在业务方法中对这个集合进行遍历从而实现递归调用

    2.2 抽象构 M & 3 _ 4 J

    抽象构件一般定义如下:

    abstract class Component
    {
    abstr- ^ vact void add(Component c);
    abstract void remove(i 2 7 ] | jComponent c);
    abstract Component getChi; k Q B j v @ Blh 8 ^ $d(int i);
    abstract void operation();
    }

    2.3 叶子构件

    class Leaf extends Component
    {
    public void add(Component c)
    {
    //叶子构件不能访问该方法
    SL D y 2 z B Y { vystem.out.println("错误# i h 4,不能访问添加构件方法s c d 1 c d / `!");
    }
    public void remove(Component c)
    {
    //叶子构件不能访问该方法
    Syster 7 ` 4 ) Tm.out.println("错误,不能访问删除构件方法!");
    }
    public Component getChild(int i)
    {
    //叶子构件不能访问该方法
    System.out.println("错误,不能访问获取构件方法!");
    return null;
    }
    public void operation()
    {
    System.out.println("叶子业务方法");W t G # f
    }
    }

    叶子构件只j b S 2需要覆盖具体业务方法opeartion,对于管理子构件的方法可以B 9 {提示错误或者抛出异常来处理。

    2.4 容器构件

    class Composite extends Component
    {
    prw h H [ n D /ivate ArrayList<Component> list = new A& 1 R urrayList<>();
    public void add(Component c)
    {
    list.add(c);
    }
    public void remove(Component c)
    {
    list.remove(c);h C a `
    }
    public Component getChild(int i)
    {
    return list.get(i);
    }
    public void operation()
    {
    list.forEach(Component::operation);
    }
    }

    容器构件只需要简单实现管理子构件的方法,对于业R & 5 0 K | Q !务方法一般需要对抽象构件集合进行遍历来实现递归调p 1 c J Q w用。

    3 实例

    开发一个杀毒软件系统,可以对某个文件夹或单个文件进行杀毒,还能根据文件类型的不同提供不同的杀毒方式,比如文本文件和+ k S s P 8 j 6图像 / 4 g 7 4文件的杀毒方式有所差异,使用组合模式对该系统进行设计。

首先定义抽象构件类AbstractFileFolder} $ & X N R作为容器构件类,ImageFileTextFileVideoFile作为叶子构件类,L K 7 e代码如下:

public class Test
{
public static void main(String[] args) {
Abstraw W 3 7 # H ] _ )ctFile file1,fV W q 5ile2,file3,file4,folder1,folder2;
file1 = new ImageFil^ ] ( D I (e("图像文件1号");
file2 = new VideoFile("视频文件1号");
fileb c , u3 = new TextFile("文本文件1号");
file4 = new ImageFile("图像文件2号");N 7 , n i R - z
folder1 = new Folder("文件夹1");
folder2 = new Folder(= | : _ A S A a P"文件夹2");
try
{
folder2.add(file1);
folder* 0 * l N X H2s F 3 w P ( ~ Q K.add(file2);
folder2.add(file3);
folder1.add(file4);
folder1.add(fX _ $ % tolder2a t v W =);
}
catch(IllegalAcce2 x jssException e)} 3 - N y 7 Q /
{
e.printStackTrace();
}
folder1.killVirus();
System.out.println();
folderH @ P T  J2.killVs G e eirus();
}
}
//抽象构件K W n n }类
abstract class AbstractFile
{
protected String name;
abstract void add(AbstractFile file) throws: # p IllegalAccessException;
abstract void remove(AbstractFile file) throws IllegalAccessException;
abstract AbstractFile getChild(int i) throwsM W u % Illef k C o VgalAcceA J Z 5 L ssExceptG x ; c R P S 0ion;
public void killVirus()
{
System.out.println(name+" 杀毒");
}
}S y [ S ~ h Q B
//叶子构C S L 8 L _ r件类
class ImageFile extends AbstractFile
{
public ImaC 7 h }geFile(String name)
{
this.na& j Y r Fme = name;
}
publiO # gc void add(AbstractFile c)
{
throw nj 6 # M x | $ gew IllegalAccessExcep1 { J V ) . c Ytion("错误,不能访问添加文件方法!");
}
public v7  2oiz x V U  0 . D jd remove(AbstractFile c)
{
throw new IllegalAccessException(7 S : }"错误,不能访问删除文件方法!");
}
publi9 Y = T M ) K %c AbstractFile getChild(int i)
{
throw new IllegalAccessException("错误,不能X f | 7访问获取文件方法!");
}
}
//叶子构5 $ = b I L件类
class Tex6 X 2 x R # qtFile extends AbstractFile
{
public TextFile(String name)
{
this.name = name;
}
public void add(AbstractFilR 1 0 0 9 [ u 6e c)
{
throw new IllegalAccessException("错误,不能访J 0 T Y 4问添加文件方法!");
}
public void remove(AbsE D - 4 - C & 5tractFile c)
{
throw new IllegalAccessE[ m ] Q 4xception("错误,不能访问删除文件方法!");
}
public AbstractFile getChild(int i)
{
throw new IllegalAccessExc@ 6 ] v J z Seption("错误,不能访问获取文件方法!");
}
}
//叶子构件类y g { g k h i P 8
class VideoFile extends AbstractFile
{
public VideoFile(String name)
{
this.name$ ( ( I ) ` O H / = name;
}
publiW Q l Oc void add(T J x TAbstractFile c)
{
throw new IllegalAccessExceptiol 3 M z ? k q gn("错误,不能G % l p } b }访问添加7 ~ C u V ~ : H )文件方法!");
}
public void remove(AbstractFile c)
{
throw new IllegalAcce8 0 fssExcept: 8 u g ` C @ 1ion("错误,不能访问删除文件方法!");
}
public AbstractFile getChild(int i)
{
throw new IllegalAccessException("错误,不能访问` ; D 2 u T .获取文件方法!");
}
}
//容器构件类
class Folder extends AbstractFile
{H Q T 3 X + + x
private As + w y A [ M /rrayList<AbstractFile&g3 - : l Tt; list =l G 9 1 @ new ArrayList<>z O u b O ,;()F j , R 1 U N;
pN  g 6  l +ublic Folder(String name)
{
this.name = name;
}
public void add(AbstractFile c)
{
li / :st.add(c);
}
publ: { +ic void remove(AbstractFile c)
{
list.removS , s z . D W ee(c);
}
public AbstractFileT 9 j C U & x e getC3 K f 0 e Y Lhild(int i)
{
return list.get(i);
}
public void killVirus()
{
System_ ` i j :.out.println("对 "+name+" 进行杀毒");
list.forEach(AbstractFile::killVirus);
}
}

输入如下:
设计模式学习笔记(十一):组合模式

4 透明组合模式与安全组合模式

4.1 如何简化代q M 5 w ( { t J u

尽管组合模式的扩展性好,在上面的例6 e m i f e ; H子中增加新的文件类型无须修改原有代码,但是,由于抽象构件类AbstractFile声明了与叶子构件无关的构件管理方法,因此 需要实现这些方法,这样就会带来很多重复性的工作。

解决方案有两个:

  • 抽象构件提供默认实现L { O:叶子构件中的构件管理方法转移到抽象构件中提供默认实现
  • 抽象构件删除方法R 3 1 ; # r I q:在抽象构件中不提供管理构件的方法

4.2 默认实现

t _ F D h l F k果使用抽象构件提供默认实现的方法,则上述例d { U v + Z子代码简化如下:

abstract class AQ n hbstractFile
{
protected St_ 0 M r ~ )ring name;
public AbstractFile(String name)
{V X ! g $
this.name = name;
}
public void add(AbstractFile file) throws IllegalAccessExcepW X !  u %tion
{
throw new IllegalAccessExceptC . A 0 ? uion("2 } 0 O错误,不能访问添加文件方法!");
}
public void remove(AbstractFile file) throws IllegalAccessExcep= % d xtion
{
throw n~ v s ! j 6 ! 1ew IllegalAccessExcept* A C 5ion("错误,不能访问删除文件方法!");
}
public Abstra+ R E s C RctFile getChild(int i) throws IllegalAccessException
{
throw new IllegalAc, N o p F g Z f QcessException("错误,不能访问获取文件方法!");
}
public void killVirus()
{
System.out.println(name+" 杀毒");
}
}
class ImageFile extends AbstractFile
{
publ@ * = S R 9 w r Tic ImageFile(String name)
{
super(name);
}
}
class TextFile extends AbstractFile
{
public TextFile(String name)
{
super(name);
}
}
class VideoFile extends A- ) 3 ! @ )bstractFile
{
public VideoFile(String name)
{
super(name);
}
}

在叶子构件中只有构造方法(_ # P a实际上业务方法应该是抽象的,在叶子构件中实现业务方法,这里的业务方法是killVirus(),这里是进行了简化),这样修改虽然简化了代码,但是总的来说为叶子构件提供这些方法是没有意义R 7 o d u i . 3 G的,因为叶子不会再下一个层次的对象,这在编译阶段不会出错 ,但是在运行阶段可能会出错

4.3 删除方法- U | _ ? l B ? E

如果使用抽象构件删除方M * 2 7 }法的方式进行简化代码,则上述例子简化如下:

abstract clH . 9ass AbstractFile
{
protected Stringk % d ^ G name;
publ* : ?  8ic AbstractFile(String name)
{
this.name = name;
}
abstract void kiD / k $ K 7 y 3llVirus();
}
class ImageFile extends AbstractFile/ e z
{
public ImageFile(String name)
{
super(name);
}
public void killVirus()
{
System.out.println("图像文件"+name+"杀毒");
}
}
clasl  g * } { 7 Os TextFile extends AbstractFile
{
public TextFile(String( ( { name)
{
super(name);
}
publV  Q ; h $ Q k Oic voi~ ` $ + md kk E _ W 8 KillVirus()
{
System.out.prif L _ i B q e =ntln("文本文件"+name+, b j _"杀毒");
}
}
class Vi: 9 } 8deoFile extends AbstractFile
{
public VideoFile(String name)
{
super(name);
}
public void killVirus()
{
System.out.println("视频文件"+name+"杀毒");
}
}

这样做叶- 4 { - 8 . a e子构件就无6 n u G 7法访问管理构件的方法了,但是带来的坏处是客户端无法统一针对抽象构件类AbstractFile进行编程,修改之前代码如下:

AbstractFil0 l  w n e file1,file2,file3,file4,folder1,folder2;

由于Absts ^ = V s R & . $ractFile中删除了管理构件方法,因此客户端需要修改代码如下:

AbstractFile file1,file2,f& ? t % f S X x tin z ` a * hle3,file4;
Folder folder1,- 5 c i ? ] 9 D $folder2A R X;

4.4 透明组合模式

透明组合模式就是第一种解h . f A 2 m E t *决方案中的方法,在抽象构件中声明所有用于管理构件的方法,` 3 A W这样做的好处是确保所有的构件类都具有相同的接口,客户端可以针对抽象构件进行统一编程,结构图如下:

设计模式学习笔记(十一):组合模式

透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一层次的对象,提供管理构件的方法是没有意义的,在编译阶. P + ) n ,段不会报错,但是在运行阶段可能会出错

4.5 安全组合模式

安全组合模式就是9 n K第二种方法的办法,安全组合模式中,抽象构件没有声明管理构件的方法,而是在容器构件中添加管理构件的方法,这种做法是安全的因为叶子对象不可能调用到这些方法。结构图如下:

设计模式学习笔记(十一):组合模式

安全组合模式的缺点是不/ a k = n够透明,因为叶子构件与容器构件具有不同的方法,管理构E i t ~ C件的方法在容器构件中定义,客户端& = 2 c : `不能完全针对抽象构件进行编程,必须有区别地对待叶子构件与容器构件。

5 主要优点

  • 层次控制:组合模式可以清楚定义分层次的复杂对象,表示对象的全部或者部分层次,- 5 } q G E它让客户端忽略了层次的差异,方便对整个层次结构进行控制| U M F
  • 一致使用构件:客户端可以一致地使用容器构件或者叶子构件,也就是能针对构件抽象层一致性编程
  • 扩展性好:增加新的容器构件或者叶子构件都很方便,符合开闭原则
  • 有效针对树形结构:组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过_ k C o = ) h叶子构件与容器构件的递归组合,可以形成复杂的树形结构,但控制树形结构却很简单

6 主要缺点

  • 难以限制构件类型:增加新构件时o m C { ] `o y i g R / a L V以限制构件类型,比如希望容= s i U e 5 J器构件中u _ $只有某一特定类型的叶子构件,例如一个只能包含图片的文件夹,使用组合模式时不能依赖类型系统来~ N j y 6施加这些约束,需要再运行时进行类型检查来实现,过程较为复杂

7 适用场景

  • 具有整H 3 4 8 H n @体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致性对待它们
  • 处理树形结构
  • 系统中能够分离出叶子构件以及容器) A x ( S . H构件,而且类型不固定,需要增加新的叶子构件或者容器构件

8 总结

设计模式学习笔记(十一):组合模式