Day 26: Behavioral patterns - Strategy

目的

如果物件本身有负责计算的方法,且该方法依照给予的参数,会有不同的计算结果,那可以将计算的部分封装成独立的物件,彼此可以互相切换,同时不影响原有的功能。

说明

试想一个情境,物件本身负责计算的功能,像是计算总金额、计算最佳路线、计算事件发生的机率等等,如果给予不同的参数,则计算的结果会大相径庭。一段时间过後,方法可能会增加许多,多少增加管理上的麻烦。

为了方便管理,可以将计算的部分封装成独立的物件,而且物件本身都采用相同的规格,对外有一致的方法。如此一来,物件需要计算时,只要根据参数就能选定相关的计算物件,而且,采用相同的方法,呼叫任意计算物件都可以用相同的方法,简化物件之间沟通上的麻烦。

在书上,会称呼计算部分为演算法(Algorithm)。

作法是:

  1. 找出一个物件,其拥有的方法会因为给予的参数不同而有不同的计算结果。(称作 Context
  2. 建立计算物件的虚拟层亲代,负责开规格建立统一的对外窗口。
  3. 将计算部分封装成独立的物件,物件本身继承虚拟层。(称作 Strategy
  4. Context 本身会储存目前使用的 Strategy,有计算需求时则使用 Strategy 的统一方法。
  5. 当要切换时,则是切换 Context 储存的 Strategy

以下范例以「简易动物园门票收费机」为核心制作。

UML 图

Strategy Pattern UML Diagram

使用 Java 实作

制作入园者物件:Person

public class Person {
    private String name;
    private boolean hasStudentID;

    public Person(String name, boolean hasStudentID) {
        this.name = name;
        this.hasStudentID = hasStudentID;
    }

    public String getName() {
        return name;
    }

    public boolean isHasStudentID() {
        return hasStudentID;
    }
}

制作计算物件的虚拟层亲代:Strategy

public interface Strategy {
    int calculateFees(List<Person> people);
}

制作计算物件的子代:StandardStrategyGroupDiscountStrategy(Strategy 物件)

public class StandardStrategy implements Strategy {
    @Override
    public int calculateFees(List<Person> people) {
        int totalFees = 0;

        for (Person person : people) {
            if (person.isHasStudentID()) {
                totalFees += 30;
            } else {
                totalFees += 60;
            }
        }

        return totalFees;
    }
}

public class GroupDiscountStrategy implements Strategy {
    @Override
    public int calculateFees(List<Person> people) {
        int totalFees = 0;

        for (Person person : people) {
            if (person.isHasStudentID()) {
                totalFees += 30;
            } else {
                totalFees += 60;
            }
        }

        return (int) (totalFees * 0.7);
    }
}

制作动物园门票售票机:ZoomTicketVendingMachine(Context 物件)

public class ZoomTicketVendingMachine {
    private Strategy strategy;
    private List<Person> people;

    public ZoomTicketVendingMachine() {
        people = new ArrayList<>();
    }

    public void setStrategy(int peopleCounts) {
        if (peopleCounts >= 30) {
            strategy = new GroupDiscountStrategy();
        } else {
            strategy = new StandardStrategy();
        }
    }

    public void addPerson(Person person) {
        people.add(person);
    }

    public void removePerson(Person person) {
        people.remove(person);
    }

    public int calculateFees() {
        setStrategy(people.size());
        return strategy.calculateFees(people);
    }

    public void clear() {
        people.clear();
    }
}

测试,模拟一家四口以及校外校学买动物园门票:TicketMachineStrategySample

public class TicketMachineStrategySample {
    public static void main(String[] args) throws Exception {
        ZoomTicketVendingMachine ticketMachine = new ZoomTicketVendingMachine();

        System.out.println("---一家四口,两大两小---");
        ticketMachine.addPerson(new Person(NameSelector.exec("m"), false));
        ticketMachine.addPerson(new Person(NameSelector.exec("f"), false));
        ticketMachine.addPerson(new Person(NameSelector.exec("m"), true));
        ticketMachine.addPerson(new Person(NameSelector.exec("f"), true));
        int familyFees = ticketMachine.calculateFees();
        System.out.println("家庭的总金额是: " + familyFees);

        ticketMachine.clear();
        System.out.println("---户外教学,两个导师以及三十八个学生---");
        ticketMachine.addPerson(new Person(NameSelector.exec("m"), false));
        ticketMachine.addPerson(new Person(NameSelector.exec("f"), false));

        for (int i = 0; i < 19; i++) {
            ticketMachine.addPerson(new Person(NameSelector.exec("f"), true));
        }

        for (int i = 0; i < 19; i++) {
            ticketMachine.addPerson(new Person(NameSelector.exec("m"), true));
        }

        int schoolTripFees = ticketMachine.calculateFees();
        System.out.println("校外教学的总金额是: " + schoolTripFees);
    }
}

Utils:NameSelector

public class NameSelector {
    private static Random random = new Random();

    public static String exec(String gender) throws IOException, ParseException, Exception {
        // 读取 JSON Array,内容是字串阵列
        JSONParser parser = new JSONParser();
        Object obj = null;

        if (gender.equals("m")) {
            obj = parser.parse(new FileReader("./src/utils/boyNameList.json"));
        } else if (gender.equals("f")) {
            obj = parser.parse(new FileReader("./src/utils/girlNameList.json"));
        } else {
            throw new Exception("性别代号错误!\n输入的性别参数是: " + gender);
        }

        JSONArray jsonArray = (JSONArray) obj;

        // 0 - 149
        int option = random.nextInt(149 + 0) + 0;
        return (String) jsonArray.get(option);
    }
}

使用 JavaScript 实作

制作入园者物件:Person

class Person {
  /**
   * @param {string} name
   * @param {boolean} hasStudentID
   */
  constructor(name, hasStudentID) {
    this.name = name;
    this.hasStudentID = hasStudentID;
  }

  getName() {
    return this.name;
  }

  isHasStudentID() {
    return this.hasStudentID;
  }
}

制作计算物件的虚拟层亲代:Strategy

/**
 * @abstract
 */
class Strategy {
  /**
   * @abstract
   * @param {Person[]} people
   */
  calculateFees(people) { return 0; }
}

制作计算物件的子代:StandardStrategyGroupDiscountStrategy(Strategy 物件)

class StandardStrategy extends Strategy {
  /**
   * @override
   * @param {Person[]} people
   */
  calculateFees(people) {
    let totalFees = 0;

    for (const person of people) {
      if (person.isHasStudentID()) {
        totalFees += 30;
      } else {
        totalFees += 60;
      }
    }

    return totalFees;
  }
}

class GroupDiscountStrategy extends Strategy {
  /**
  * @override
  * @param {Person[]} people
  */
  calculateFees(people) {
    let totalFees = 0;

    for (const person of people) {
      if (person.isHasStudentID()) {
        totalFees += 30;
      } else {
        totalFees += 60;
      }
    }

    return totalFees * 0.7;
  }
}

制作动物园门票售票机:ZoomTicketVendingMachine(Context 物件)

class ZoomTicketVendingMachine {
  constructor() {
    /** @type {Strategy} */
    this.strategy = null;
    /** @type {Person[]} */
    this.people = [];
  }

  /** @param {number} peopleCounts */
  setStrategy(peopleCounts) {
    if (peopleCounts >= 30) {
      this.strategy = new GroupDiscountStrategy();
    } else {
      this.strategy = new StandardStrategy();
    }
  }

  /** @param {Person} person */
  addPerson(person) {
    this.people.push(person);
  }

  /** @param {Person} person */
  removePerson(person) {
    this.people = this.people.filter(item => item !== person);
  }

  calculateFees() {
    this.setStrategy(this.people.length);
    return this.strategy.calculateFees(this.people);
  }

  clear() {
    this.people = [];
  }
}

测试,模拟一家四口以及校外校学买动物园门票:ticketMachineStrategySample

const ticketMachineStrategySample = () => {
  const ticketMachine = new ZoomTicketVendingMachine();

  console.log("---一家四口,两大两小---");
  ticketMachine.addPerson(new Person(nameSelector("m"), false));
  ticketMachine.addPerson(new Person(nameSelector("f"), false));
  ticketMachine.addPerson(new Person(nameSelector("m"), true));
  ticketMachine.addPerson(new Person(nameSelector("f"), true));
  const familyFees = ticketMachine.calculateFees();
  console.log("家庭的总金额是: " + familyFees);

  ticketMachine.clear();
  console.log("---户外教学,两个导师以及三十八个学生---");
  ticketMachine.addPerson(new Person(nameSelector("m"), false));
  ticketMachine.addPerson(new Person(nameSelector("f"), false));

  for (let i = 0; i < 19; i++) {
    ticketMachine.addPerson(new Person(nameSelector("f"), true));
  }

  for (let i = 0; i < 19; i++) {
    ticketMachine.addPerson(new Person(nameSelector("m"), true));
  }

  const schoolTripFees = ticketMachine.calculateFees();
  console.log("校外教学的总金额是: " + schoolTripFees);
}

ticketMachineStrategySample();

Utils:nameSelector

/** @param {string} gender */
const nameSelector = (gender) => {
  const option = Math.floor(Math.random() * (149 - 0 + 1)) + 0;

  // 读取 JSON Array,内容是字串阵列
  if (gender === "m") {
    const boyNameList = require('../Sample-by-Java/src/utils/boyNameList.json');
    return boyNameList[option];
  } else if (gender === "f") {
    const girlNameList = require('../Sample-by-Java/src/utils/girlNameList.json');
    return girlNameList[option];
  } else {
    throw new Error(`性别参数错误,输入的参数是: ${gender}`);
  }
}

总结

Strategy 模式跟 Simple Factory Method 十分类似,皆拥有 if - else if - elseswitch case 而有多个可能的选择,两者最大的不同在於前者专注在将 Business Logic 抽出;後者专注在如何「产出」需要的物件。当然,两者的类别不同,理所当然在乎不同的点。

实作上要注意的,与 State 模式相似,什麽样的情境下,需要将 if - else if - elseswitch case 抽出、封装成独立物件?就我工作经验来说,如果之後在开发上会建立许多负责计算的物件,那可以提早封装,节省之後要套用 Strategy 模式的时间。反之,如果不会建立许多负责计算的物件,那不用套用 Strategy 模式,维持 if - else if - elseswitch case 也很好。

跟你说,维持if或是switch也不错

明天将介绍 Behavioural patterns 的第十个模式:Template Method 模式。


<<:  Day26 Gin with Logger

>>:  Day26 - 云端交易主机 - GCP云端平台申请&架设(Ubuntu)

Day28 - 部属到正式环境 (3)

今天的实作内容主要根据教学网站进行。 将应用程序安装到Heroku (接续Day27) 使用GIT将...

中阶魔法 - 范围链 Scope Chain

前情提要 上回与艾草玩游戏输了要接受处罚。 「都躲这麽远了,她应该找不到我了吧!」 艾草:「啊哈,原...

ISO 27001 机房管理部份之三

ISO 27001 机房管理部份之三 稽核分三种 : 内部稽核 (例如 : 稽核组长、稽核小组) 外...

[Day18] - 真值与假值

什麽是真值与假值 在 JavaScript 中,除了布林值本身就是真值或假值外,其他型别会在布林的执...

第 13 天 坚持刷题持续进步( leetcode 016 )

https://leetcode.com/problems/3sum-closest/ 3Sum ...