iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Software Development

也該是時候學學 Design Pattern 了系列 第 13

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 12: Structural patterns - Bridge
下一篇
Day 14: Structural patterns - Decorator
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言