记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步
文章目录
门面模式
原理和实现都很简单,应用场景也很明确,主要是接口设计方面使用。
平时开发涉及到接口时,可能会遇到接口粒度的问题。为保证接口的可复用性(或者叫通用性),需要将接口尽量设计的细粒度些,职责单一点。但如果接口的粒度过小,在接口的使用者开发一个业务功能时,会导致需要调用n多细粒度的接口才能完成,调用者就会抱怨接口不好用。
相反,如果粒度过大,一个接口返回n多数据,要做n多事情,会导致接口不够通用,可复用性不好。那针对不同的调用者的业务需求,需要开发不同的接口来满足,导致接口的无限膨胀。
如何解决接口的可复用性和易用性之间的矛盾呢?采用门面模式
门面模式的原理和实现
门面模式,也叫外观模式,facade design pattern,定义:provide a unified interface to a set of interfaces in a subsytem.Facade Pattern defines a higher-level interface that makes the subsystem easier to use.
也即是:门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。子系统有多种理解方式,既可以是一个完整的系统,也可以是更细粒度的类或者模块。
举例说明,假设有个系统A,提供了a、b、c、d四个接口,系统B完成某个业务功能时,需要调用A系统的a、b、d接口,利用门面模式,提供一个包裹a、b、d接口调用的门面接口x给系统B直接使用。
为啥不让系统B直接调用a、b、d接口呢?假设系统A是个后端服务器,系统B是app客户端,客户端通过后端服务器提供的接口来获取数据。app和服务器之间通过移动网络通信的,网络通信耗时较长,为提高app的响应速度,要尽量减少app和服务器的网络通信次数。
假设完成某个业务功能(如显示某个页面信息)需要“依次”调用a、b、d接口,因自身业务特点,不支持并发调用这三个接口。
如果发现app客户端的响应速度较慢,排查后发现,是过多的接口调用过多的网络通信,针对该情况,就可以利用门面模式,让后端服务器提供一个包裹a、b、d接口的接口x,app客户端调用一次接口x,来获取所有想要的数据,将网络通信的次数从3次减少到1次,提高app的响应速度。
这只是门面模式的一个意图,就是解决性能问题。实际上,不同应用场景,使用门面模式的意图也不同。
门面模式的应用场景举例
1. 解决易用性问题
门面模式可用来封装系统的底层实现,隐藏系统的复杂性,提供一组简单易用、更高层的接口。如linux系统调用函数就可以看做一种门面。是linux系统暴露给开发者的一组“特殊”的编程接口,封装底层更基础的linux内核调用。
2. 解决性能问题
刚才的例子已经说明,通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高app客户端的响应速度。从代码实现的角度,如何组织门面接口和非门面接口呢?
如果门面接口不多,可以将其和非门面接口放到一起,当做普通接口即可。如果门面接口很多,可以在已有的接口之上,重新抽象出一层,专门放置门面接口,从类、包的命名上跟原来的接口层做区分。如果门面太多,并且很多是跨多个子系统的,可放到一个新的子系统中。
3. 解决分布式事务问题
举例说明,在一个金融系统中,有两个业务领域模型,用户和钱包,这两个domain都对外暴露一系列接口,如用户的增删改查接口、钱包的增删改查接口,假设有这样一个业务场景:在用户注册时,不仅会创建用户(在数据库User表),还会给用户创建一个钱包(在数据库Wallet表)。
对这样一个简单的业务需求,通过依次调用用户的创建接口和钱包的创建接口来完成。但是,用户注册需要主持事务,也就是说,要么都成功,要么都失败。要支持两个接口调用在一个事务中执行,较难实现。虽然可通过引入分布式事务框架或者事后补偿的机制解决,但代码实现较为复杂。最简单的就是,利用数据库事务或者spring框架提供的事务,在一个事务中,执行创建用户和创建钱包两个SQL操作,要求两个SQL在一个接口中完成,因此,需要借鉴门面模式,再设计一个包裹两个操作的接口,让新接口在一个事务中执行两个SQL操作。
享元模式
原理
flyweight design pattern,享元,也即是被共享的单元。意图是复用对象,节省内存,前提是享元对象是不可变对象。
具体说,当一个系统中存在大量重复对象时,如果这些重复的对象是不可变对象,可利用享元模式将对象设计为享元,在内存中只保留一份,供多处代码引用。可以减少内存中对象的数量,节省内存。实际上,不仅相同对象可以设计为享元,对于相似对象,也可将对象相同的部分(字段)提取出来,设计为享元。
“不可变对象”指的是,一旦通过构造函数初始化完成后,它的状态(对象的成员变量或者属性)就不会再被修改。不可变对象不能暴露任何set等修改内部状态的方法。之所以要求享元是不可变对象,因为会被多处代码共享使用。
举例说明。假设在开发一个棋牌游戏(如象棋),一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,如棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,可以显示一个完整的棋盘给玩家。具体代码如下,其中ChessPiece表示棋子,ChessBoard表示一个棋局,里面保存象棋中30个棋子的信息。
public class ChessPiece {
private int id;
private String text;
private Color color;
private int positionX;
private int positionY;
public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
this.id = id;
this.text = text;
this.color = color;
this.positionX = positionX;
this.positionY = positionY;
}
public static enum Color{
RED, BLACK
}
//...省略其他属性和getter/setter方法...
}
public class ChessBoard {
private Map<Integer, ChessPiece> ChesePiece = new HashMap<>();
public ChessBoard(){
init();
}
private void init(){
ChesePiece.put(1,new ChessPiece(1,"车", ChessPiece.Color.BLACK,0,0));
ChesePiece.put(2,new ChessPiece(2,"车", ChessPiece.Color.BLACK,0,1));
//...省略摆放其他棋子的代码...
}
public void move(int chessPieceId,int toPositionX,int toPositionY){
//...省略...
}
}
为了记录每个房间当前的棋局情况,需要给每个房间都创建一个ChessBoard棋局对象。因为游戏大厅有成千上万个房间(实际上,百万人同时在线的游戏大厅有很多),那保存这么多棋局对象就会消耗大量的内存。如何节省内存呢?
利用享元模式,像刚才的实现方式,在内存中有大量的相似对象。他们的id、text、color都相同,只有positionX、positionY不同。可以将棋子的id、text、color拆分出来,设计为独立的类,并作为享元供多个棋盘复用。棋盘只需要记录每个棋子的位置信息即可。
public class ChessPieceUnit {
private int id;
private String text;
private Color color;
public ChessPieceUnit(int id, String text, Color color) {
this.id = id;
this.text = text;
this.color = color;
}
public static enum Color{
RED, BLACK
}
//...省略其他属性和getter/setter方法...
}
public class ChessPieceUnitFactory {
private static final Map<Integer,ChessPieceUnit> pieces = new HashMap<>();
static {
pieces.put(1,new ChessPieceUnit(1,"车", ChessPieceUnit.Color.BLACK));
pieces.put(2,new ChessPieceUnit(2,"马", ChessPieceUnit.Color.BLACK));
//...省略摆放其他棋子的代码...
}
public static ChessPieceUnit getChessPiece(int chessPieceId){
return pieces.get(chessPieceId);
}
}
public class ChessPiece {
private ChessPieceUnit chessPieceUnit;
private int positionX;
private int positionY;
public ChessPiece(ChessPieceUnit chessPieceUnit, int positionX, int positionY) {
this.chessPieceUnit = chessPieceUnit;
this.positionX = positionX;
this.positionY = positionY;
}
//省略getter、setter方法
}
public class ChessBoard {
private Map<Integer,ChessPiece> chessPieces = new HashMap<>();
public ChessBoard(){
init();
}
private void init(){
chessPieces.put(1,new ChessPiece(ChessPieceUnitFactory.getChessPiece(1),0,0));
chessPieces.put(1,new ChessPiece(ChessPieceUnitFactory.getChessPiece(2),1,0));
//...省略摆放其他棋子的代码...
}
public void move(int chessPieceId,int toPositionX,int toPositionY){
//...省略...
}
}
上述代码,利用工厂类缓存ChessPieceUnit信息(也就是id、text、color),通过工厂类获取的ChessPieceUnit就是享元。所有的ChessBoard对象共享这30个ChessPieceUnit对象(因为象棋中只有30个棋子)。使用享元之前,记录1万个棋局,创建30万个棋子的ChessPieceUnit对象,而利用享元模式,只需要创建30个享元对象即可。
享元模式的原理讲完了,总结下代码结构。实际上,代码结构非常简单,主要是通过工厂模式,在工厂类中,通过一个map来缓存已经创建过的享元对象,达到复用的目的。
享元模式在文本编辑器中的应用
如何利用享元模式优化文本编辑器的内存占用?
可以把文本编辑器想象为office的Word,不过,假设只实现文字编辑功能,不包含图片、表格等复杂的编辑功能。简化后的文本编辑器,要在内存中表示一个文本文件,只需记录文字和格式两部分信息。格式又包含文字的字体、大小、颜色等信息。
尽管实际编写文档分为标题、正文等文本类型来设置文字的格式,但从理论上说,可给文本文件中的每个文字都设置不同的格式,为实现如此灵活的格式设置,并且代码实现又不过于复杂,把每个文字都看做一个独立的对象看待,并在其中包含它的格式信息。具体代码:
public class Character {//文字
private char c;
private Font font;
private int size;
private int colorRGB;
public Character(char c, Font font, int size, int colorRGB) {
this.c = c;
this.font = font;
this.size = size;
this.colorRGB = colorRGB;
}
}
public class Editor {
private List<Character> chars = new ArrayList<>();
public void appendCharacter(char c, Font font,int size,int colorRGB){
Character character = new Character(c,font,size,colorRGB);
chars.add(character);
}
}
在文字编辑器中,每敲一个字,都会调用Editor类的appendCharacter()方法,创建一个新的Character对象,保存到chars数组中,如果有几十万个文字,要在内存中存储这么多Character对象,如何节省内存?
实际上,在一个文本文件中,用到的字体格式不会太多,毕竟不可能把每个文字都设置为不同的格式。对应字体格式,可设计为享元,让不同的文字共享使用。按这个设计思路,对代码重构:
public class CharacterStyle {
private Font font;
private int size;
private int colorRGB;
public CharacterStyle(Font font, int size, int colorRGB) {
this.font = font;
this.size = size;
this.colorRGB = colorRGB;
}
@Override
public boolean equals(Object obj) {
CharacterStyle otherStyle = (CharacterStyle) obj;
return font.equals(otherStyle.font)
&& size==otherStyle.size
&& colorRGB==otherStyle.colorRGB;
}
}
public class CharacterStyleFactory {
private static final List<CharacterStyle> styles = new ArrayList<>();
public static CharacterStyle getStyle(Font font,int size,int colorRGB){
CharacterStyle newStyle = new CharacterStyle(font,size,colorRGB);
for (CharacterStyle style:styles){
if (style.equals(newStyle)){
return style;
}
}
styles.add(newStyle);
return newStyle;
}
}
public class Character {
private char c;
private CharacterStyle style;
public Character(char c, CharacterStyle style) {
this.c = c;
this.style = style;
}
}
public class Editor {
private List<Character> chars = new ArrayList<>();
public void appendCharacter(char c, Font font, int size, int colorRGB){
Character character = new Character(c,CharacterStyleFactory.getStyle(font, size, colorRGB));
chars.add(character);
}
}
享元模式vs单例、缓存、对象池
和单例的区别
单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式类似于之前的单例的变体:多例。
区别两种设计模式,不能只看代码实现,而是看设计意图,也就是要解决的问题,尽管从代码实现上看,享元模式和多例有很多相似之处,但从设计意图上,完全不同。应用享元模式是为了对象复用,节省内存,而多例模式是为了限制对象的个数。
和缓存的去呗
享元模式的实现中,通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟平时说的数据库缓存、CPU缓存等是两回事。平时的缓存,是为了提高访问效率,而非复用。
和对象池的区别
对象池、连接池、线程池也是为了复用,和享元模式的区别在哪里?
简单解释下对象池,像C++这样的编程语言,内存管理由程序员自己负责,为避免频繁的进行对象创建和释放导致内存碎片,可预先申请一片连续的内存空间,也就是对象池。每次创建对象时,从对象池取出一个空闲对象使用,对象使用完成再放回对象池供复用,而非直接释放掉。
池化技术的复用,可理解为重复使用,目的是节省时间(如从数据库连接池中取一个连接,不用重新创建),在任意时刻,每个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成,放回池中,由其他使用者复用。享元模式的复用可理解为共享使用,整个生命周期,都被所有使用者共享,目的是节省空间。
享元模式在java Integer的应用
先看代码,思考输出的结果
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1==i2);
System.out.println(i3==i4);
要正确分析,首先搞清楚两个事情:
- 如何判定两个java对象是否相等(也就是代码中的“==”操作符的含义)
- 什么是自动装箱autoboxing和自动拆箱unboxing
自动装箱,就是自动将基本数据类型转换为包装器类型,自动拆箱就是自动将包装器类型转换为基本数据类型。
Integer i = 56;//自动装箱
int j = i;//自动拆箱
数值56是基本数据类型int,赋值给包装器类型Integer变量时,触发自动装箱操作,创建一个Integer类型的对象,复制给变量i,底层相当于执行下面这条语句:
Integer i = 56; 底层执行:Integer i = Integer.valueOf(56);
反过来
int j=i; 底层执行了: int j = i.intValue();
那如何判定两个对象是否相等?实际就是判定两个局部变量存储的地址是否相同,也即是判定两个局部变量是否指向相同的对象。
再看之前的代码。前4行复制语句都会触发自动装箱操作,也即是创建Integer对象并赋值给i1、i2、i3、i4四个变量,根据刚才的理解,i1、i2指向的是不同的Integer对象,通过“”判定时返回false,同理,i3i4也是false。
但实际上,答案并不是两个false,而是一个true,一个false。为什么呢?因为Integer利用了享元模式复用对象,导致上述的结果。自动装箱时,也即是调用valueOf()创建Integer对象,如果创建的Integer对象的值在-128到127之间,会从IntegerCache类中直接返回,否则才调用new方法创建,对应的源码:
public static Integer valueOf(int i){
if(i>=IntegerCache.low && i<=IntegerCache.high){
return IntegerCache.cache[i+(-IntegerCache.low)];
}
return new Integer(i);
}
实际上,这个IntegerCache相当于生成享元对象的工厂类,只是名字没有factory,是Integer的内部类,具体代码实现:
private static class IntegerCache{
static final int low = -128;
static final int high;
static final Integer cache[];
static {
//high value may be configured by property
int h = 127;
String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if(integerCacheHighPropValue != null){
try{
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i,127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i,Integer.MAX_VALUE - (-low) -1);
}catch(NumberFormatException nfe){
// if the property cannot be parsed into an int,ignore it
}
}
high = h;
cache = new Integer[(high-low)+1];
int j = low;
for(int k=0;k<cache.length;k++){
cache[k] = new Integer(j++);
}
//range[-128,127] must be interned (JLS7 5.1.7)
assert IntegerCache.high>=127;
}
private IntegerCache(){}
}
IntegerCache为何只缓存-128到127之间的整型值?这区间的最常用,也即是一个字节的大小。
因此,平时开发时,对于下面三种创建整型对象的方式,优先使用后两种:
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);
享元模式在java String的应用
还是先看一段代码:
String s1 = "aaa";
String s2 = "aaa";
String s3 = new String("aaa");
System.out.println(s1==s2);
System.out.println(s1==s3);
运行结果,一个true一个false,和Integer设计思路类似,String类利用享元模式复用相同的字符串常量,jvm会专门开辟一块存储区来存储字符串常量,这块存储区叫“字符串常量池”。和Integer的不同之处是,Integer要共享的对象,是在类加载的时候,集中一次性创建好,但对字符串来说,无法预知共享哪些字符串常量,只能在某个字符串常量第一次被用到时,存储到常量池,之后再用到时,直接引用常量池中已存在的即可。
组合模式
组合模式的原理和实现
GoF的《设计模式》中定义:compose objects into tree structure to represent part-whole hierarchies. Composite lets client treat individual objects and compositions of objects uniformly. 将一组对象组织(compose)成树形结构,以表示一种“部分-整体”的层次结构,组合让客户端(代指代码的使用者)可以统一单个对象和组合对象的处理逻辑。
举例说明,假设有这样一个需求:设计一个类来表示文件系统中的目录,能方便的实现下面的功能:
- 动态的添加、删除某个目录下的子目录或文件
- 统计指定目录下的文件个数
- 统计指定目录下的文件总大小
给出骨架代码,核心逻辑并未实现,代码实现中,把文件和目录统一用FileSystemNode类表示,通过isFile属性区分。
public class FileSystemNode {
private String path;
private boolean isFile;
private List<FileSystemNode> subNodes = new ArrayList<>();
public FileSystemNode(String path, boolean isFile) {
this.path = path;
this.isFile = isFile;
}
public int countNumOfFiles(){
//todo
}
public long countSizeOfFile(){
//todo
}
public String getPath(){
return path;
}
public void addSubNode(FileSystemNode fileOrDir){
subNodes.add(fileOrDir);
}
public void removeSubNode(FileSystemNode fileOrDir){
int size = subNodes.size();
int i = 0;
for (;i<size;i++){
if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())){
break;
}
}
if (i<size){
subNodes.remove(i);
}
}
}
具体补全countNumOfFiles()和countSizeOfFiles()这两个函数,其实就是树上的递归遍历算法。对于文件,直接返回文件的个数(返回1)或大小。对于,目录,遍历目录中的每个子目录或者文件,递归计算他们的个数或大小,然后求和,就是目录下的文件个数和文件大小。
具体实现:
public int countNumOfFiles(){
if (isFile){
return 1;
}
int numOfFiles = 0;
for (FileSystemNode fileOrDir:subNodes){
numOfFiles += fileOrDir.countNumOfFiles();
}
return numOfFiles;
}
public long countSizeOfFile(){
if (isFile){
File file = new File(path);
if (!file.exists()) return 0;
return file.length();
}
long sizeOfFiles = 0;
for (FileSystemNode fileOrDir:subNodes){
sizeOfFiles += fileOrDir.countSizeOfFile();
}
return sizeOfFiles;
}
单纯从功能实现的角度,代码没问题,但是如果开发的是大型系统,从扩展性(文件或目录可能会对应不同的操作)、业务建模(文件和目录从业务上是两个概念)、代码的可读性(文件和目录区分对待更符合人们对业务的认知)的角度,最好对文件和目录区分设计,定义为File和Directory两个类。重构后
public abstract class FileSystemNode {
protected String path;
public FileSystemNode(String path) {
this.path = path;
}
public abstract int countNumOfFiles();
public abstract long countSizeOfFile();
public String getPath(){
return path;
}
}
public class File extends FileSystemNode {
public File(String path) {
super(path);
}
@Override
public int countNumOfFiles() {
return 1;
}
@Override
public long countSizeOfFile() {
java.io.File file = new java.io.File(path);
if (!file.exists()) return 0;
return file.length();
}
}
public class Directory extends FileSystemNode {
private List<FileSystemNode> subNodes = new ArrayList<>();
public Directory(String path) {
super(path);
}
@Override
public int countNumOfFiles() {
int numOfFiles = 0;
for (FileSystemNode fileOrDir:subNodes){
numOfFiles += fileOrDir.countNumOfFiles();
}
return numOfFiles;
}
@Override
public long countSizeOfFile() {
long sizeOfFiles = 0;
for (FileSystemNode fileOrDir:subNodes){
sizeOfFiles += fileOrDir.countSizeOfFile();
}
return sizeOfFiles;
}
public void addSubNode(FileSystemNode fileOrDir){
subNodes.add(fileOrDir);
}
public void removeSubNode(FileSystemNode fileOrDir){
int size = subNodes.size();
int i = 0;
for (;i<size;i++){
if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())){
break;
}
}
if (i<size){
subNodes.remove(i);
}
}
}
再看如何用他们表示一个文件系统中的目录树结构。
public class Demo {
public static void main(String[] args) {
/*
/
/wz/
/wz/a.txt
/wz/b.txt
/wz/movies/
/wz/movies/c.avi
/xzg/
/xzg/docs/
/xzg/docs/d.txt
*/
Directory fileSytemTree = new Directory("/");
Directory node_wz = new Directory("/wz/");
Directory node_xzg = new Directory("/xzg/");
fileSytemTree.addSubNode(node_wz);
fileSytemTree.addSubNode(node_xzg);
File node_wz_a = new File("/wz/a.txt");
File node_wz_b = new File("/wz/b.txt");
Directory node_wz_movies = new Directory("/wz/movies/");
node_wz.addSubNode(node_wz_a);
node_wz.addSubNode(node_wz_b);
node_wz.addSubNode(node_wz_movies);
File node_wz_movies_c = new File("/wz/movies/c.avi");
node_wz_movies.addSubNode(node_wz_movies_c);
Directory node_xzg_docs = new Directory("/xzg/docs/");
node_xzg.addSubNode(node_xzg_docs);
File node_xzg_docs_d = new File("/xzg/docs/d.txt");
node_xzg_docs.addSubNode(node_xzg_docs_d);
System.out.println("/ files num:"+ fileSytemTree.countNumOfFiles());
System.out.println("/wz/ files num:"+node_wz.countNumOfFiles());
}
}
对照着例子,再看组合模式的定义:将一组对象(文件和目录)组织成树形结构,以表示一种“部分-整体”的层次结构(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(目录)的处理逻辑(递归遍历)。
与其说组合模式是设计模式,不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示为树种种数据结构,业务需求可以通过在树上的递归遍历算法实现。
组合模式的应用场景举例
再举个例子,假设在开发一个OA系统(办公自动化系统),公司的组织结构包含部门和员工两种数据类型,其中,部门又包含子部门和员工。在数据库的表结构如下。
希望在内存中构建整个公司的人员架构图(部门、子部门、员工的隶属关系),并且提供接口计算出部门的薪资成本(隶属该部门的所有员工的薪资和)。
部门包含子部门和员工,这是种嵌套结构,可表示为树这种数据结构,计算需求,也可在树上的遍历算法来实现。从这个角度,该应用场景可使用组合模式设计和实现。
其代码结构和上个例子很相似。
public abstract class HumanResource {
protected long id;
protected double salary;
public HumanResource(long id) {
this.id = id;
}
public long getId(){
return id;
}
public abstract double calculateSalary();
}
public class Employee extends HumanResource {
public Employee(long id,double salary) {
super(id);
this.salary = salary;
}
@Override
public double calculateSalary() {
return salary;
}
}
public class Department extends HumanResource {
private List<HumanResource> subNodes = new ArrayList<>();
public Department(long id) {
super(id);
}
@Override
public double calculateSalary() {
double totalSalary = 0;
for (HumanResource hr: subNodes){
totalSalary += hr.calculateSalary();
}
this.salary = totalSalary;
return salary;
}
public void addSunNode(HumanResource hr){
subNodes.add(hr);
}
}
//组织架构的代码
public class Demo {
private static final long ORGANIZATION_ROOT_ID = 1001;
private DepartmentRepo departmentRepo;//依赖注入
private EmployeeRepo employeeRepo;//依赖注入
public void buildOrganization(){
Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
buildOrganization(rootDepartment);
}
private void buildOrganization(Department department){
List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department);
for (Long subDepartmentId:subDepartmentIds){
Department subDepartment = new Department(subDepartmentId);
department.addSunNode(subDepartment);
buildOrganization(subDepartment);
}
List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
for (Long employeeId:employeeIds){
double salary = employeeRepo.getEmployeeSalary(employeeId);
department.addSunNode(new Employee(employeeId,salary));
}
}
}
再拿组合模式的定义和这个例子对照:将一组对象(员工和部门)组织成树形结构,以表示一种“部分-整体”的层次结构(部门和子部门的嵌套结构)。组合模式让客户端可统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。