设计模式之美笔记11

记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步

门面模式

原理和实现都很简单,应用场景也很明确,主要是接口设计方面使用。

平时开发涉及到接口时,可能会遇到接口粒度的问题。为保证接口的可复用性(或者叫通用性),需要将接口尽量设计的细粒度些,职责单一点。但如果接口的粒度过小,在接口的使用者开发一个业务功能时,会导致需要调用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));
        }
    }
}

再拿组合模式的定义和这个例子对照:将一组对象(员工和部门)组织成树形结构,以表示一种“部分-整体”的层次结构(部门和子部门的嵌套结构)。组合模式让客户端可统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值