组合模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-86JxiUa2-1660313923766)(C:/Users/86158/AppData/Roaming/Typora/typora-user-images/image-20220812215343211.png)]

组合模式针对于特定场景,如文件管理、组织管理等,使用该模式能简化管理,使代码变得非常简洁。

组合模式:将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

UML

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-70LYbG9K-1660313923767)(https://shidawuhen.github.io/2021/06/20/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-16-%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F/image-20210610101011712.png)]

分析

单看UML图可能不清晰,举个栗子会容易一些。Composite是目录,Leaf是目录下的文件,目录和文件都继承自Component。目录能够增加、删除文件,可以展示目录所在位置,文件只能展示文件所在位置。

对于目录这种需求,有两种实现方式。

第一种不使用组合模式,只用一个类,有2个核心变量

  • 一个成员变量表明对象是文件还是目录
  • 一个成员变量存放目录下文件列表,如果对象为文件,则该变量为空
type FileSystemNode struct {
   isFile   bool             //表明是文件还是目录
   subNodes []FileSystemNode //目录下包含的内容
}

第二种方案使用组合模式。虽然第一种方案能够实现文件管理的功能,但并不优雅。因为文件和目录是不同的,各自有各自的特性,将特有的内容放到一个类里,不满足单一职责原则

所以我们可以将其拆分为两个类:文件类和目录类。两个类必须继承自同一个父类,除了重复的功能可以复用外,更重要的一点是消除了两个类调用上的区别,subNodes不需要做任何区分。而且这两个类可以独立进化,相互不影响,何乐而不为呢。

使用场景

组合模式在使用上,特别像深度优先遍历或者广度优先遍历,一般用于组织结构、文件管理上,这些功能都有共通点:个体和集体无论在功能上还是认知上都极为相似。

代码实现

这次代码简单实现一下目录和文件的添加、显示功能吧。

package main

import "fmt"

const Separator = "--"

/**
 * @Description: 文件系统接口,文件和目录都要实现该接口
 */
type FileSystemNode interface {
   Display(separator string)
}

/**
 * @Description: 文件通用功能
 */
type FileCommonFunc struct {
   fileName string
}

/**
 * @Description: 设置文件名称
 * @receiver f
 * @param fileName
 */
func (f *FileCommonFunc) SetFileName(fileName string) {
   f.fileName = fileName
}

/**
 * @Description: 文件类
 */
type FileNode struct {
   FileCommonFunc
}

/**
 * @Description: 文件类显示文件内容
 * @receiver f
 */
func (f *FileNode) Display(separator string) {
   fmt.Println(separator + f.fileName + "   文件内容为:Hello,world")
}

/**
 * @Description: 目录类
 */
type DirectoryNode struct {
   FileCommonFunc
   nodes []FileSystemNode
}

/**
 * @Description: 目录类展示文件名
 * @receiver d
 */
func (d *DirectoryNode) Display(separator string) {
   fmt.Println(separator + d.fileName)
   for _, node := range d.nodes {
      node.Display(separator + Separator)
   }
}

/**
 * @Description: 添加目录或者文件
 * @receiver d
 * @param f
 */
func (d *DirectoryNode) Add(f FileSystemNode) {
   d.nodes = append(d.nodes, f)
}
func main() {
   //初始化
   biji := DirectoryNode{}
   biji.SetFileName("笔记")

   huiyi := DirectoryNode{}
   huiyi.SetFileName("会议")

   chenhui := FileNode{}
   chenhui.SetFileName("晨会.md")

   zhouhui := FileNode{}
   zhouhui.SetFileName("周会.md")
   //组装
   biji.Add(&huiyi)
   huiyi.Add(&chenhui)
   huiyi.Add(&zhouhui)
   //显示
   biji.Display(Separator)
}

➜ myproject go run main.go

–笔记

—-会议

——晨会.md 文件内容为:Hello,world

——周会.md 文件内容为:Hello,world

文件类和目录类都实现了FileSystemNode接口,所以目录类管理文件类如同管理自己一样。两者都组合了FileCommonFunc类,可以复用相同功能。最后就是两者可以独立变化,如目录类有Add功能,但文件类没有。

实例

公司的人员组织就是一个典型的树状的结构,现在假设我们现在有部分,和员工,两种角色,一个部门下面可以存在子部门和员工,员工下面不能再包含其他节点。
我们现在要实现一个统计一个部门下员工数量的功能

代码

package composite

// IOrganization 组织接口,都实现统计人数的功能
type IOrganization interface {
	Count() int
}

// Employee 员工
type Employee struct {
	Name string
}

// Count 人数统计
func (Employee) Count() int {
	return 1
}

// Department 部门
type Department struct {
	Name string

	SubOrganizations []IOrganization
}

// Count 人数统计
func (d Department) Count() int {
	c := 0
	for _, org := range d.SubOrganizations {
		c += org.Count()
	}
	return c
}

// AddSub 添加子节点
func (d *Department) AddSub(org IOrganization) {
	d.SubOrganizations = append(d.SubOrganizations, org)
}

// NewOrganization 构建组织架构 demo
func NewOrganization() IOrganization {
	root := &Department{Name: "root"}
	for i := 0; i < 10; i++ {
		root.AddSub(&Employee{})
		root.AddSub(&Department{Name: "sub", SubOrganizations: []IOrganization{&Employee{}}})
	}
	return root
}

单元测试

package composite

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewOrganization(t *testing.T) {
	got := NewOrganization().Count()
	assert.Equal(t, 20, got)
}

总结

组合模式是对指定场景有用,所以大家能不能用到,完全看运气。这个设计模式满足单一职责原则、开闭原则、里氏替换原则。