Day 13: Structural patterns - Composite

目的

将程序的组成转换成有上下阶级的结构(或称:树状结构),方便使用者不论从哪个节点、叶子使用,都可以有相似的执行。

说明

假入要撰写一个程序,本身是参考现实世界中有上下阶级的组织时,而且每个组织的上中下游部门,都可以「提供相同的服务」时,就可以采用 Composite 模式,或者称作树状结构(资料结构的树状图)。

现实中常见的范例有:

  • 公家机关的组织单位-财政部国税局
    • 财政部台北国税局
    • 财政部高雄国税局
    • 财政部北区国税局
    • 财政部中区国税局
    • 财政部南区国税局
  • 公家机关的组织单位-行政院联合服务中心
    • 行政院南部联合服务中心
    • 行政院中部联合服务中心
    • 行政院东部联合服务中心
    • 行政院云嘉南区联合服务中心
    • 行政院金马联合服务中心
  • 军事组织
  • 拥有分公司、办事处的大公司。

Composite 模式强调每个节点都可以提供相同的服务,因此作法会是:

  1. 建立节点与叶子的亲代,负责开规格,告诉外界有哪些方法可用。
  2. 建立节点子代,负责构筑树状结构、实作亲代的规格细节。
  3. 建立叶子子代,实作亲代的规格细节。

UML 图

Composite Pattern UML Diagram

使用 Java 实作

节点、叶子亲代:MilitaryUnit

public abstract class MilitaryUnit {
    protected String name;

    protected int unitCounts;

    protected int weaponCounts;

    protected MilitaryUnit(String name, int unitCounts, int weaponCounts) {
        this.name = name;
        this.unitCounts = unitCounts;
        this.weaponCounts = weaponCounts;
    }

    public abstract void add(MilitaryUnit unit);

    public abstract void remove(MilitaryUnit unit);

    public abstract void display(int depth);

    public abstract int reportUnitCounts();

    public abstract int reportWeaponCounts();
}

节点子代:ConcreteMilitaryUnit

public class ConcreteMilitaryUnit extends MilitaryUnit {
    private ArrayList<MilitaryUnit> militaryUnits = new ArrayList<MilitaryUnit>();

    public ConcreteMilitaryUnit(String name) {
        super(name, 0, 0);
    }

    @Override
    public void add(MilitaryUnit unit) {
        militaryUnits.add(unit);
    }

    @Override
    public void remove(MilitaryUnit unit) {
        militaryUnits.remove(unit);
    }

    @Override
    public void display(int depth) {
        char[] title = new char[depth];
        Arrays.fill(title, '-');
        System.out.println(new String(title) + name);

        for (MilitaryUnit unit : militaryUnits) {
            unit.display(depth + 2);
        }
    }

    @Override
    public int reportUnitCounts() {
        for (MilitaryUnit militaryUnit : militaryUnits) {
            unitCounts += militaryUnit.reportUnitCounts();
        }

        return unitCounts;
    }

    @Override
    public int reportWeaponCounts() {
        for (MilitaryUnit militaryUnit : militaryUnits) {
            weaponCounts += militaryUnit.reportWeaponCounts();
        }

        return weaponCounts;
    }
}

叶子子代:NormalSoldierLazySoldierDrunkSoldierDeserterSoldier

public class NormalSoldier extends MilitaryUnit {
    public NormalSoldier(String name) {
        super(name, 1, 1);
    }

    @Override
    public void add(MilitaryUnit unit) {
    }

    @Override
    public void remove(MilitaryUnit unit) {
    }

    @Override
    public void display(int depth) {
        char[] title = new char[depth];
        Arrays.fill(title, '-');
        System.out.println(new String(title) + name);
    }

    @Override
    public int reportUnitCounts() {
        return unitCounts;
    }

    @Override
    public int reportWeaponCounts() {
        return weaponCounts;
    }
}

public class LazySoldier extends MilitaryUnit {
    public LazySoldier(String name) {
        super(name, 1, 0);
    }

    @Override
    public void add(MilitaryUnit unit) {
    }

    @Override
    public void remove(MilitaryUnit unit) {
    }

    @Override
    public void display(int depth) {
        char[] title = new char[depth];
        Arrays.fill(title, '-');
        System.out.println(new String(title) + name);
    }

    @Override
    public int reportUnitCounts() {
        return unitCounts;
    }

    @Override
    public int reportWeaponCounts() {
        return weaponCounts;
    }
}

public class DrunkSoldier extends MilitaryUnit {
    public DrunkSoldier(String name) {
        super(name, 1, 3);
    }

    @Override
    public void add(MilitaryUnit unit) {
    }

    @Override
    public void remove(MilitaryUnit unit) {
    }

    @Override
    public void display(int depth) {
        char[] title = new char[depth];
        Arrays.fill(title, '-');
        System.out.println(new String(title) + name);
    }

    @Override
    public int reportUnitCounts() {
        return unitCounts;
    }

    @Override
    public int reportWeaponCounts() {
        return weaponCounts;
    }
}

public class DeserterSoldier extends MilitaryUnit {
    public DeserterSoldier(String name) {
        super(name, 0, 1);
    }

    @Override
    public void add(MilitaryUnit unit) {
    }

    @Override
    public void remove(MilitaryUnit unit) {
    }

    @Override
    public void display(int depth) {
        char[] title = new char[depth];
        Arrays.fill(title, '-');
        System.out.println(new String(title) + name);
    }

    @Override
    public int reportUnitCounts() {
        return unitCounts;
    }

    @Override
    public int reportWeaponCounts() {
        return weaponCounts;
    }
}

测试:ArmyCompositeSample

public class ArmyCompositeSample {
    public static void main(String[] args) {
        MilitaryUnit army = new ConcreteMilitaryUnit("陆军");
        army.add(new NormalSoldier("司令"));
        army.add(new NormalSoldier("陆军通讯兵"));

        MilitaryUnit division = new ConcreteMilitaryUnit("第一师");
        division.add(new NormalSoldier("师长"));
        division.add(new NormalSoldier("第一师通讯兵"));
        army.add(division);

        MilitaryUnit brigade = new ConcreteMilitaryUnit("第一旅");
        brigade.add(new NormalSoldier("旅长"));
        brigade.add(new NormalSoldier("第一旅通讯兵"));
        division.add(brigade);

        MilitaryUnit regiment = new ConcreteMilitaryUnit("第一团");
        regiment.add(new NormalSoldier("团长"));
        regiment.add(new NormalSoldier("第一团通讯兵"));
        brigade.add(regiment);

        MilitaryUnit battalion = new ConcreteMilitaryUnit("第一营");
        battalion.add(new NormalSoldier("营长"));
        battalion.add(new NormalSoldier("第一营通讯兵"));
        regiment.add(battalion);

        MilitaryUnit company = new ConcreteMilitaryUnit("第一连");
        company.add(new NormalSoldier("连长"));
        company.add(new NormalSoldier("第一连通讯兵"));
        battalion.add(company);

        MilitaryUnit platoon = new ConcreteMilitaryUnit("第一排");
        platoon.add(new NormalSoldier("排长"));
        platoon.add(new NormalSoldier("第一排通讯兵"));
        company.add(platoon);

        MilitaryUnit squad1 = new ConcreteMilitaryUnit("第一班");
        squad1.add(new NormalSoldier("班长"));
        squad1.add(new NormalSoldier("第一班通讯兵"));
        platoon.add(squad1);

        MilitaryUnit fireTeam11 = new ConcreteMilitaryUnit("第一伍");
        fireTeam11.add(new NormalSoldier("伍长"));
        fireTeam11.add(new NormalSoldier("第一伍通讯兵"));
        fireTeam11.add(new NormalSoldier("Roger"));
        fireTeam11.add(new NormalSoldier("Jason"));
        squad1.add(fireTeam11);

        MilitaryUnit fireTeam12 = new ConcreteMilitaryUnit("第二伍");
        fireTeam12.add(new NormalSoldier("伍长"));
        fireTeam12.add(new LazySoldier("第二伍通讯兵"));
        fireTeam12.add(new DrunkSoldier("Rick"));
        fireTeam12.add(new DeserterSoldier("Jay"));
        squad1.add(fireTeam12);

        MilitaryUnit squad2 = new ConcreteMilitaryUnit("第二班");
        squad2.add(new NormalSoldier("班长"));
        squad2.add(new NormalSoldier("第二班通讯兵"));
        platoon.add(squad2);

        MilitaryUnit fireTeam21 = new ConcreteMilitaryUnit("第三伍");
        fireTeam21.add(new NormalSoldier("伍长"));
        fireTeam21.add(new DrunkSoldier("第三伍通讯兵"));
        fireTeam21.add(new NormalSoldier("Allen"));
        fireTeam21.add(new NormalSoldier("Bill"));
        squad2.add(fireTeam21);

        MilitaryUnit fireTeam22 = new ConcreteMilitaryUnit("第四伍");
        fireTeam22.add(new NormalSoldier("伍长"));
        fireTeam22.add(new LazySoldier("第四伍通讯兵"));
        fireTeam22.add(new DrunkSoldier("Charlie"));
        fireTeam22.add(new DeserterSoldier("Dave"));
        squad2.add(fireTeam22);

        System.out.println("结构图:");
        army.display(1);
        System.out.println("陆军人员回报:" + army.reportUnitCounts());
        System.out.println("陆军武器数量回报:" + army.reportWeaponCounts());
    }
}

使用 JavaScript 实作

节点、叶子亲代:MilitaryUnit

/** @abstract */
class MilitaryUnit {
  constructor(name, unitCounts, weaponCounts) {
    this.name = name;
    this.unitCounts = unitCounts | 0;
    this.weaponCounts = weaponCounts | 0;
  }

  /** @abstract */
  add(unit) { return; }

  /** @abstract */
  remove(unit) { return; }

  /** @abstract */
  display(depth) { return; }

  /** @abstract */
  reportUnitCounts() { return; }

  /** @abstract */
  reportWeaponCounts() { return; }
}

节点子代:ConcreteMilitaryUnit

class ConcreteMilitaryUnit extends MilitaryUnit {
  constructor(name) {
    super(name);
    /** @type MilitaryUnit[] */
    this.militaryUnits = [];
  }

  /** @override */
  add(unit) {
    this.militaryUnits.push(unit);
  }

  /** @override */
  remove(unit) {
    this.militaryUnits = this.militaryUnits.filter((curUnit) => curUnit !== unit);
  }

  /** @override */
  display(depth) {
    console.log('-'.repeat(depth) + this.name);

    for (const unit of this.militaryUnits) {
      unit.display(depth + 2);
    }
  }

  /** @override */
  reportUnitCounts() {
    for (const militaryUnit of this.militaryUnits) {
      this.unitCounts += militaryUnit.reportUnitCounts();
    }

    return this.unitCounts;
  }

  /** @override */
  reportWeaponCounts() {
    for (const militaryUnit of this.militaryUnits) {
      this.weaponCounts += militaryUnit.reportWeaponCounts();
    }

    return this.weaponCounts;
  }
}

叶子子代:NormalSoldierLazySoldierDrunkSoldierDeserterSoldier

class NormalSoldier extends MilitaryUnit {
  constructor(name) {
    super(name, 1, 1);
  }

  /** @override */
  add(unit) {
    return;
  }

  /** @override */
  remove(unit) {
    return;
  }

  /** @override */
  display(depth) {
    console.log('-'.repeat(depth) + this.name);
  }

  /** @override */
  reportUnitCounts() {
    return this.unitCounts;
  }

  /** @override */
  reportWeaponCounts() {
    return this.weaponCounts;
  }
}

class LazySoldier extends MilitaryUnit {
  constructor(name) {
    super(name, 1, 0);
  }

  /** @override */
  add(unit) {
    return;
  }

  /** @override */
  remove(unit) {
    return;
  }

  /** @override */
  display(depth) {
    console.log('-'.repeat(depth) + this.name);
  }

  /** @override */
  reportUnitCounts() {
    return this.unitCounts;
  }

  /** @override */
  reportWeaponCounts() {
    return this.weaponCounts;
  }
}

class DrunkSoldier extends MilitaryUnit {
  constructor(name) {
    super(name, 1, 3);
  }

  /** @override */
  add(unit) {
    return;
  }

  /** @override */
  remove(unit) {
    return;
  }

  /** @override */
  display(depth) {
    console.log('-'.repeat(depth) + this.name);
  }

  /** @override */
  reportUnitCounts() {
    return this.unitCounts;
  }

  /** @override */
  reportWeaponCounts() {
    return this.weaponCounts;
  }
}

class DeserterSoldier extends MilitaryUnit {
  constructor(name) {
    super(name, 0, 1);
  }

  /** @override */
  add(unit) {
    return;
  }

  /** @override */
  remove(unit) {
    return;
  }

  /** @override */
  display(depth) {
    console.log('-'.repeat(depth) + this.name);
  }

  /** @override */
  reportUnitCounts() {
    return this.unitCounts;
  }

  /** @override */
  reportWeaponCounts() {
    return this.weaponCounts;
  }
}

测试:armyCompositeSample

const armyCompositeSample = () => {
  const army = new ConcreteMilitaryUnit("陆军");
  army.add(new NormalSoldier("司令"));
  army.add(new NormalSoldier("陆军通讯兵"));

  const division = new ConcreteMilitaryUnit("第一师");
  division.add(new NormalSoldier("师长"));
  division.add(new NormalSoldier("第一师通讯兵"));
  army.add(division);

  const brigade = new ConcreteMilitaryUnit("第一旅");
  brigade.add(new NormalSoldier("旅长"));
  brigade.add(new NormalSoldier("第一旅通讯兵"));
  division.add(brigade);

  const regiment = new ConcreteMilitaryUnit("第一团");
  regiment.add(new NormalSoldier("团长"));
  regiment.add(new NormalSoldier("第一团通讯兵"));
  brigade.add(regiment);

  const battalion = new ConcreteMilitaryUnit("第一营");
  battalion.add(new NormalSoldier("营长"));
  battalion.add(new NormalSoldier("第一营通讯兵"));
  regiment.add(battalion);

  const company = new ConcreteMilitaryUnit("第一连");
  company.add(new NormalSoldier("连长"));
  company.add(new NormalSoldier("第一连通讯兵"));
  battalion.add(company);

  const platoon = new ConcreteMilitaryUnit("第一排");
  platoon.add(new NormalSoldier("排长"));
  platoon.add(new NormalSoldier("第一排通讯兵"));
  company.add(platoon);

  const squad1 = new ConcreteMilitaryUnit("第一班");
  squad1.add(new NormalSoldier("班长"));
  squad1.add(new NormalSoldier("第一班通讯兵"));
  platoon.add(squad1);

  const fireTeam11 = new ConcreteMilitaryUnit("第一伍");
  fireTeam11.add(new NormalSoldier("伍长"));
  fireTeam11.add(new NormalSoldier("第一伍通讯兵"));
  fireTeam11.add(new NormalSoldier("Roger"));
  fireTeam11.add(new NormalSoldier("Jason"));
  squad1.add(fireTeam11);

  const fireTeam12 = new ConcreteMilitaryUnit("第二伍");
  fireTeam12.add(new NormalSoldier("伍长"));
  fireTeam12.add(new LazySoldier("第二伍通讯兵"));
  fireTeam12.add(new DrunkSoldier("Rick"));
  fireTeam12.add(new DeserterSoldier("Jay"));
  squad1.add(fireTeam12);

  const squad2 = new ConcreteMilitaryUnit("第二班");
  squad2.add(new NormalSoldier("班长"));
  squad2.add(new NormalSoldier("第二班通讯兵"));
  platoon.add(squad2);

  const fireTeam21 = new ConcreteMilitaryUnit("第三伍");
  fireTeam21.add(new NormalSoldier("伍长"));
  fireTeam21.add(new DrunkSoldier("第三伍通讯兵"));
  fireTeam21.add(new NormalSoldier("Allen"));
  fireTeam21.add(new NormalSoldier("Bill"));
  squad2.add(fireTeam21);

  const fireTeam22 = new ConcreteMilitaryUnit("第四伍");
  fireTeam22.add(new NormalSoldier("伍长"));
  fireTeam22.add(new LazySoldier("第四伍通讯兵"));
  fireTeam22.add(new DrunkSoldier("Charlie"));
  fireTeam22.add(new DeserterSoldier("Dave"));
  squad2.add(fireTeam22);

  console.log("结构图:");
  army.display(1);
  console.log("陆军人员回报:" + army.reportUnitCounts());
  console.log("陆军武器数量回报:" + army.reportWeaponCounts());
}

armyCompositeSample();

总结

Composite 是个好理解但是不容易实作的模式,原因有两点:

  1. 拥有上下阶级,这在设计上不常见,除了特地目的才会使用到。
  2. 提供相同的服务,可能该节点、叶子没有实作该服务的必要时,仍然要将该方法列出来,好维持继承的格式,进而导致有不少程序码是没必要的。

符合需求的话,建构起来不麻烦,使用者只要参考规格就可以安心呼叫。

参考以上几点,理解 Composite 是特殊要求下的解。

明天将介绍 Structural patterns 的第四个模式:Decorator 模式。


<<:  [Day 23] 究竟AI能不能预测股价?

>>:  【Day 15】反向传播(Backpropagation)

Day 26 - Palindrome Number

大家好,我是毛毛。ヾ(´∀ ˋ)ノ 废话不多说开始今天的解题Day~ 9. Palindrome N...

[27] 30 天从 Swift 学会 Objective-C:Swift friendly 的 Unavailable 与 convenience init

物件导向的设计中,关於建构物件的方式我们成为建构器(constructor),这关系到物件使用的方式...

Day5 Type

Background 对於变数的Type,能够依据他们的特性分为两种,分别为不可变的Static t...

Day18:18 - 结帐服务(2) - 前端 - 结帐、订单新增、结帐成功画面

Hei,我是Charlie! 在Day17当中,我们完成了後端的结帐功能,在今天我们将完成前端的结帐...

DeBug Day 26

修正Bug日 [ ] 修正首页的排版问题 [ ] 修正书本细节页面的排版问题 [ ] 修正新增照片到...