Reputation: 843
Me and my colleague have a disagreement about if this follows the strategy pattern or not.
We have a react component List
which expects a "strategy" prop that has the following shape:
interface ListStrategy {
renderItem: (index: number) => React.ReactNode
itemCount: number
}
We have some functions that create a "strategy" for rendering the list in a certain way. For instance we have the following strategy constructors.
createGroupedListStrategy(...args: GroupedListStrategyArgs): ListStrategy
createFlatListStrategy(...args: FlatListStrategyArgs): ListStrategy
createTreeListStrategy(...args: TreeListStrategyArgs): ListStrategy
I found a lot of examples where the constructor of a strategy either expects no arguments, or expects the same arguments for every strategy. But the constructors above each expect different arguments. createGroupedListStrategy
expects as an option a function that can be used inside the strategy to match items to their group. createTreeListStrategy
expects as an option a function that it can use to access the children of an item.
Because the constructors are so different, my colleague started to doubt if these strategies are interchangeable in the sense that the definition of the strategy pattern talks about. But my view is that once the strategies are instantiated, they can be interchanged without a problem.
Can anyone clear this up? I'm really curious.
Upvotes: 0
Views: 864
Reputation: 29086
The constructors for the strategies don't have any relevance for whether something is or isn't a strategy. The goal of the strategy pattern is to extract an operation to be independant from the class and to allow you to determine how a class behaves, without altering it.
Consider the following, we want to make a trivial "calculator" that takes two values and operates with them. It then displays that result in some way. We want to extract the logic for:
This would mean that we can change how the calculator works without altering the class itself. So, we extract two strategies:
interface CalculationStrategy {
doMaths: (a: number, b: number) => number
}
interface DisplayStrategy {
show: (num: number) => void
}
And we could supply multiple implementations:
//calculation strategies
class AddStrategy {
doMaths(a, b) {
return a + b;
}
}
class MultiplyByConstantStrategy {
constructor(x) {
this.x = x;
}
doMaths(a, b) {
return (a + b) * this.x;
}
}
//display strategies
class ConsoleDisplayStrategy {
show(num) {
console.log(num.toFixed(2))
}
}
class HTMLDisplayStrategy {
constructor(elementSelector) {
this.inputElement = document.querySelector(elementSelector);
}
show(num) {
this.inputElement.value = num;
}
}
//calculate class
class Calculate {
constructor(operationHandler, displayHandler) {
this.operationHandler = operationHandler;
this.displayHandler = displayHandler;
}
calculate(a, b) {
const result = this.operationHandler.doMaths(a, b);
this.displayHandler.show(result);
}
}
/* usage */
//calculate the total for a bill + tip
const tip = new Calculate(
new MultiplyByConstantStrategy(1.15),
new HTMLDisplayStrategy("#totalWithTip")
);
document.querySelector("#billTotal")
.addEventListener("click", () => {
const coffee = Number(document.querySelector("#coffeePrice").value);
const bagel = Number(document.querySelector("#bagelPrice").value);
tip.calculate(coffee, bagel);
});
//just display a calculation on the page
const showAdd = new Calculate(
new AddStrategy(),
new HTMLDisplayStrategy("#addResult")
);
showAdd.calculate(2, 8);
//print a sum
const printAdd = new Calculate(
new AddStrategy(),
new ConsoleDisplayStrategy()
);
document.querySelector("#printSum")
.addEventListener("click", () => {
const a = Number(document.querySelector("#a").value);
const b = Number(document.querySelector("#b").value);
printAdd.calculate(a, b);
});
.as-console-wrapper {
/* prevent the console output from covering the page */
position: initial !important;
}
<pre>MultiplyByConstantStrategy + HTMLDisplayStrategy</pre>
<div>
<label for="coffeePrice">Price for coffee:</label>
<input id="coffeePrice" value="2" type="number" />
</div>
<div>
<label for="bagelPrice">Price for bagel:</label>
<input id="bagelPrice" value="8" type="number" />
</div>
<div>
<label for="totalWithTip">You owe:</label>
<input id="totalWithTip" readonly/>
</div>
<button id="billTotal">Bill please!</button>
<hr/>
<pre>AddStrategy + HTMLDisplayStrategy</pre>
<div>
<label for="addResult">2 + 8 = </label>
<input id="addResult" readonly/>
</div>
<hr/>
<pre>AddStrategy + ConsoleDisplayStrategy</pre>
<div>
<input id="a" value="2" type="number" />
+
<input id="b" value="8" type="number" />
</div>
<button id="printSum">print the sum</button>
The goal is reached here. We've successfully decoupled the calculation and display. We can alter each one without having to change the other or the Calculate
class. And this is what the strategy pattern tries to solve. The fact that the strategies are constructed with different parameters is irrelevant for this outcome.
Upvotes: 4