複合模式 (Composite pattern)|在實戰案例下的 OOAD 詳盡設計推導教學

複合模式 (Composite pattern)|在實戰案例下的 OOAD 詳盡設計推導教學

大家好,我是水球潘。今天想和大家聊一下在四人帮设计模式(Gang of Four 23 Design Patterns)中的结构型模式之最——复合模式(Composite Pattern)。

复合模式最棒的性质是什么?
想想公司服务的客户⋯⋯
客户这种毫不在乎你公司的组织结构,对任何层级的人都能发号施令的特性,就是复合模式最棒的性质。

四人帮的每一个模式其实都不是什么相当困难的模式,常人都能通过自己的创意思考得出相关解法,或是在不自觉的情况下套用复合模式。

但是呀,其实复合模式的运用却在一个框架中举足轻重。我在阅读一个框架的源代码时,第一时间一定都是先分析它复合模式的形式长什么样子,就能差不多了解它框架的「核心竞争力」摆在哪里。

所以可以说呀,只要是大型框架多少都会套爆复合模式,已经可以说是套到那种「杞人忧天」的程度了。

既然复合模式有着举足轻重的应用地位,那么就让我用一篇文章的时间,借由一个实战案例来展示复合模式的基本运用和面向对象分析/设计上的推导。

这篇文章有一定的长度,是来自我制作的在线课程:「软件设计模式精通之旅」中的其中一篇章,由于学员们很喜欢这个篇章的内容,因此我将它公开分享出来。

我所教授的「设计模式」是结合了Christopher Alexander - The Timeless way of Building(设计模式起源著作)思想,和我多年面向对象大型系统项目下的实战梳理心得的结果。

所以各位此文章相较一般的文来得长上许多,请读者将重点放在「思路的推导」,我会在其中带领各位走一遍更可靠的思路:「需求 → Context (Forces → Problem) → Form (Solution) → Resulting Context」的完整探讨,如此一来才能让大家感受到设计理论与实务上之间的紧密配合。

具体来说,我会遵照以下步骤来介绍复合模式的落地运用:

  1. 提出「初版需求」,交代情境
  2. 对需求进行「面向对象分析 (OOA)」,绘制出初版类图
  3. 参考初版类图快速实现出初版代码
  4. 新增令人困扰的「第二版需求」
  5. 分析新的需求带来哪些困扰,察觉 Forces
  6. 对 Forces 进行分析、分类,釐清 Context 并定义我们要解决的 Problem
  7. 寻找对应的软件设计模式
  8. 应用设计模式来解决 Problem (Forces),绘制第二版类图
  9. 参考第二版类图重构出第二版代码
  10. 软件设计模式六大要素总结(使用Christopher Alexander的描述方式)

所以请各位在阅读我的模式系列文时,可将文章视为是「水球的设计模式思路的展示」- 对新手而言是一个非常完整的出发点,而对老手而言,你仍能从中刷新认知。

你收到了一份需求!!!!!

  1. 你要开发一个用来模拟文件系统的命令行程序(CLI),用户向CLI输入指令来存取文件系统。
  2. 文件系统由目录(Directory)和文件(File)以层次结构组成:
  3. 目录中可以存放多个子目录及文件。
  4. 目录和文件都以名称辨识,名称中的合法字符为大小写字母、数字以及. - 和_符号。
  5. 每一个文件中都会携带着二进制内容(如文字、图片等)。
    文件系统会有一个根目录的存在(Root Directory),代表着文件系统的最外层目录,是文件系统的访问入口。
  6. 除了根目录,所有目录和文件都必须存在于一个父目录之中。
  7. CLI在执行时会指定一个当前目录(current directory),用户和CLI互动来对当前目录下达指令。CLI在启动时的默认当前目录为根目录。
  8. 初版的CLI只支持三道指令:
    • cd :cd指令其实是change directory的意思。指定一个name参数,将当前目录底下名称为的子目录设置为CLI的当前目录。
      输入cd …将当前目录设置为「当前目录的父目录」。
    • size :找到当前目录底下名称为子目录或文件,并打印出它所占用的内存大小size,计算方式如下:
      • 如果是子目录,其大小为底下所有子目录和文件大小的总和。
      • 如果是文件,其大小为其携带二进制内容的字节数量。
    • search :计算当前目录底下「所有子子孙孙中(即不只往下看一层,看的是当前目录底下的每一层)」名字包含的子目录或子文件的数量。

这就是初版的需求了,一般我们接收到的需求也是通过这种自然语言文字组成。而在收到需求的当下,我们是不可能立刻想到最适当的设计的,因此从需求作为出发点来讨论软件设计模式才会接近实战的感受。

面向对象分析 (OOA)

收到一份需求之后,我会先大致走一趟OOA来将需求中涉及的「概念」,以面向对象的元素捕捉起来,所以才能慢慢浮现出概念的「结构面」。

从需求中我们可以意识到CLI主要存取两类对象:「目录(Directory)」和「文件(File)」,捕捉好目录和文件身上的属性和彼此之间的关系之后,可以绘制出以下类图:
ooa

面向对象分析 (OOA) 其实是有一套流程的,在此不赘述。

在类图中我们可以见到,这份需求涉及的概念大致上都已经被捕捉到图中。比如:从需求中我们可以读到「目录中可以存放子目录和子文件」,因此萃取出了「Directory」和「File」两个类别,并在Directory类别身上拉出两道关联(实线),一道关联着自身(代表包含子目录),另一道关联着File(代表包含子文件)。

而由于需求提出了要支持cd指令功能,因此我们从中捕捉了「当前目录」的概念,以CLI和Directory之间的实线关联来表现。

最后需求提出了size指令功能,我们将size指令大体上的行为用粉红色的便签纸描述在CLI类别上。

OOA做到这个程度之后,其实就算是建立好了60%的设计基础,我们可以紧接着去实作初版的代码!

初版程式

首先看File 類別的代碼:

public class File {
   
    private Directory parent;
    private final String name;
    private final byte[] content;

    public File(String name, String content) {
   
        this.name = shouldMatch("[A-Za-z0-9.\\\\-_]+", name);
        this.content = content.getBytes();
    }
    public String getName() {
   
        return name;
    }
    public long bytes() {
   
        return content.length;
    }
    public Directory getParent() {
   
        return parent;
    }
    public void setParent(Directory parent) {
   
        this.parent = parent;
    }
}

这是一个非常简单的类别,没有太多行为,但会验证File属性的规范。
接着来看看Directory类别的代码:

public class Directory {
   
    private Directory parent;
    private final String name;		
    // 子檔案們
    private final List<File> fileChildren = new ArrayList<>();
    // 子目錄們
    private final List<Directory> dirChildren = new ArrayList<>();

    public static Directory root() {
   
        return new Directory("root");
    }

    public Directory(String name) {
   
        this.name = shouldMatch("[A-Za-z0-9.\\\\-_]+", name);
    }

    public void addFile(File file) {
   
        fileChildren.add(file);
    }

    public void addDirectory(Directory directory) {
   
        dirChildren.add(directory);
        directory.setParent(this);
    }

    public File getFile(String name) {
   
        for (File file : fileChildren) {
   
            if (file.getName().equals(name)) {
   
                return file;
            }
        }
        return null;
    }

    public Directory getDirectory(String name) {
   
        for (Directory dir : dirChildren) {
   
            if (dir.name.equals(name)) {
   
                return dir;
            }
        }
        return null;
    }

    public long totalBytes() {
   
        long total = 0;
        for (Directory dir : dirChildren) {
   
            total += dir.totalBytes();
        }
        for (File file : fileChildren) {
   
            total += file.bytes();
        }
        return total;
    }

    public List<File> searchFiles(String keyword) {
   
        List<File> files 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值