如果物件本身有負責計算的方法,且該方法依照給予的參數,會有不同的計算結果,那可以將計算的部分封裝成獨立的物件,彼此可以互相切換,同時不影響原有的功能。
試想一個情境,物件本身負責計算的功能,像是計算總金額、計算最佳路線、計算事件發生的機率等等,如果給予不同的參數,則計算的結果會大相徑庭。一段時間過後,方法可能會增加許多,多少增加管理上的麻煩。
為了方便管理,可以將計算的部分封裝成獨立的物件,而且物件本身都採用相同的規格,對外有一致的方法。如此一來,物件需要計算時,只要根據參數就能選定相關的計算物件,而且,採用相同的方法,呼叫任意計算物件都可以用相同的方法,簡化物件之間溝通上的麻煩。
在書上,會稱呼計算部分為演算法(Algorithm)。
作法是:
Context
)Strategy
)Context
本身會儲存目前使用的 Strategy
,有計算需求時則使用 Strategy
的統一方法。Context
儲存的 Strategy
。以下範例以「簡易動物園門票收費機」為核心製作。
製作入園者物件: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);
}
製作計算物件的子代:StandardStrategy
、GroupDiscountStrategy
(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);
}
}
製作入園者物件: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; }
}
製作計算物件的子代:StandardStrategy
、GroupDiscountStrategy
(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 - else
或 switch case
而有多個可能的選擇,兩者最大的不同在於前者專注在將 Business Logic 抽出;後者專注在如何「產出」需要的物件。當然,兩者的類別不同,理所當然在乎不同的點。
實作上要注意的,與 State 模式相似,什麼樣的情境下,需要將 if - else if - else
或 switch case
抽出、封裝成獨立物件?就我工作經驗來說,如果之後在開發上會建立許多負責計算的物件,那可以提早封裝,節省之後要套用 Strategy 模式的時間。反之,如果不會建立許多負責計算的物件,那不用套用 Strategy 模式,維持 if - else if - else
或 switch case
也很好。
明天將介紹 Behavioural patterns 的第十個模式:Template Method 模式。