设计模式是什么鬼(组合)
组合,由于事物与事物之间存在某种关系,进而组织起来并形成某种结构并且可以共同发挥作用。组合模式所应用的就是树形结构以表达“部分/整体”的层次结构。相信我们都知道“二叉树”结构吧,根部分出来两个枝杈(左节点,右节点),每个枝杈上又可以继续分叉,直到末端的叶子为止。
当然,二叉树算是最简单的树了,其实大自然中更多的是多叉树结构,我们来观察一些蕨类植物,宏观上看貌似这只是一片简单的叶子。
然而,一花一世界,一叶一菩提。仔细观察我们会发现叶子上又有小的枝叶,一个小的枝叶上又有更小的枝叶。我们不管从宏观还是微观维度上看都是类似的结构,这正取决于植物的DNA,无论在哪个维度上都是相同的生长方式。冥冥之中,好似存在着某种大自然的规律,类似的结构总是在重复、迭代地显现出某种自似性。
这其实牵扯到一个数学概念:分形理论。不管是连绵的山川、飘浮的云朵、岩石的断裂口、树冠、花菜、还是人类的大脑皮层……把这些部分与整体以某种方式相似的形体呈现出来就称为分形。
从简单到复杂,或是复杂到简单,我们抽出任意一个“部分”,其与“整体”的结构是类似的。所以,上面提到的“树”结构,无论是根、枝、还是叶子,我们都统统把他们抽象地称为“节点”,模糊他们的行为差异,这样我们便可以达到模糊简单元素与复杂元素的目的。好了,开始代码部分,这里我们就拿类似树结构的文件系统目录结构来举例吧。
我们可以看到,从根目录开始分支,下面可以包含文件夹或者文件,文件夹下面可以继续存放子文件夹或文件,而文件则属于“叶子”节点,下面不再有延续分支。不管三七二十一,我们笼统地把他们都抽象成”节点“。
1public abstract class Node {
2 protected String name;//节点命名
3
4 public Node(String name) {//构造节点,传入节点名。
5 this.name = name;
6 }
7
8 //增加后续子节点方法
9 protected abstract void add(Node child);
10}
每个文件夹或文件都应该有一个名字,并且新建时必须声明,所以在构造的时候必须传入名字。第9行添加子节点方法我们做成抽象的,模糊其添加行为并留给子类去实现。下面添加文件夹类并继承自抽象节点。
1public class Folder extends Node{
2 //文件夹可以包含子节点(文件夹或者文件)。
3 private List<Node> childrenNodes = new ArrayList<>();
4
5 public Folder(String name) {
6 super(name);//调用父类“节点”的构造方法命名。
7 }
8
9
10 protected void add(Node child) {
11 childrenNodes.add(child);//可以添加子节点。
12 }
13}
作为文件夹类,我们承载着树型结构的重任,所以这里第3行我们的文件夹类封装了一个子节点的List,重点在于这里模糊了其下文件夹或文件的概念,也就是说这个文件夹既可以包含子文件夹,又可以包含文件。第5行的构造方法我们则交给父类构造完成,至于第10行的添加子节点方法,作为文件夹类当然是需要实现的。反之作为叶子节点的文件类,是不具备添加子节点功能的,看代码。
1public class File extends Node{
2
3 public File(String name) {
4 super(name);
5 }
6
7
8 protected void add(Node child) {
9 System.out.println("不能添加子节点。");
10 }
11}
可以看到第9行我们在这里实现了添加子节点方法并打印输出一句错误信息告知用户“不能添加子节点”,其实更合适的做法是在此处抛出异常信息。一切就绪,我们可以构建目录并添加文件了。
1public class Client {
2 public static void main(String[] args) {
3 Node driveD = new Folder("D盘");
4
5 Node doc = new Folder("文档");
6 doc.add(new File("简历.doc"));
7 doc.add(new File("项目介绍.ppt"));
8
9 driveD.add(doc);
10
11 Node music = new Folder("音乐");
12
13 Node jay = new Folder("周杰伦");
14 jay.add(new File("双截棍.mp3"));
15 jay.add(new File("告白气球.mp3"));
16 jay.add(new File("听妈妈的话.mp3"));
17
18 Node jack = new Folder("张学友");
19 jack.add(new File("吻别.mp3"));
20 jack.add(new File("一千个伤心的理由.mp3"));
21
22 music.add(jay);
23 music.add(jack);
24
25 driveD.add(music);
26 }
27}
至此,我们已经告一段落了,我们将目录结构规划的非常好,以便对各种文件进行分类管理以便日后查找。不止于此,我们这里再做一些扩展,比如用户需要列出当前目录下的所有子目录及文件。
为了实现以上这种显示方式,我们需要在名称前加入空格。但需要加入几个空格呢?这个问题上层目录肯定知道,就由它主动传入吧,我们来修改Node节点类并加入ls方法。
1public abstract class Node {
2 protected String name;//节点命名
3
4 public Node(String name) {//构造节点,传入节点名。
5 this.name = name;
6 }
7
8 //增加后续子节点方法
9 protected abstract void add(Node child);
10
11 protected void ls(int space){
12 for (int i = 0; i < space; i++) {
13 System.out.print(" ");//先循环输出n个空格;
14 }
15 System.out.println(name);//然后再打印自己的名字。
16 }
17}
这里从第11行开始加入的ls方法不做抽象,而只实现出文件夹与文件相同的行为片段,至于“不同”的行为片段则在子类中实现。
1public class File extends Node{
2
3 public File(String name) {
4 super(name);
5 }
6
7
8 protected void add(Node child) {
9 System.out.println("不能添加子节点。");
10 }
11
12
13 public void ls(int space){
14 super.ls(space);
15 }
16}
文件类的实现与父类完全一致,第13行开始直接调用父类继承下来的ls方法即可。而文件夹类则比较特殊了,不但要列出自己的名字,还要列出子节点的名字。
1public class Folder extends Node{
2 //文件夹可以包含子节点(文件夹或者文件)。
3 private List<Node> childrenNodes = new ArrayList<>();
4
5 public Folder(String name) {
6 super(name);//调用父类“节点”的构造方法命名。
7 }
8
9
10 protected void add(Node child) {
11 childrenNodes.add(child);//可以添加子节点。
12 }
13
14
15 public void ls(int space){
16 super.ls(space);//调用父类共通的ls方法列出自己的名字。
17 space++;//之后列出的子节点前,空格数要增加一个了。
18 for (Node node : childrenNodes) {
19 node.ls(space);//调用子节点的ls方法。
20 }
21 }
22}
自第15行开始,文件夹的ls方法先调用父类共通的ls方法列出自己的名字,然后再把空格数加1并传给下一级的所有子节点,循环迭代,直至抵达叶子则返回调用之初,完美的抽象递归。
最后,我们的client在任何一级节点上只要调用ls(int space),并传入当前目录的偏移量(空格数)即可出现之前的树形列表了,比如挨着左边框显示:ls(0)。或者我们干脆给用户再增加一个无参数重载方法,内部直接调用ls(0)即可。
1public abstract class Node {
2 protected String name;//节点命名
3
4 public Node(String name) {//构造节点,传入节点名。
5 this.name = name;
6 }
7
8 //增加后续子节点方法
9 protected abstract void add(Node child);
10
11 protected void ls(int space){
12 for (int i = 0; i < space; i++) {
13 System.out.print(" ");//先循环输出n个空格;
14 }
15 System.out.println(name);//然后再打印自己的名字。
16 }
17
18 //无参重载方法,默认从第0列开始显示。
19 protected void ls(){
20 this.ls(0);
21 }
22}
这样用户可以抛开烦扰,直接调用ls()便是。
世界虽是纷繁复杂的,然而混沌中有序,从单细胞动物,细胞分裂,到高级动物;从二进制0与1,再到庞杂的软件系统,再从“道生一,一生二”的阴阳哲学,再到“三生万物的简明玄妙”,分形无不揭示着世界的本质,其部分与整体结构或其行为总是以类似的形式涌现,分形之道如此,组合模式亦是如此。