Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strategy Pattern #1

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{
"name": "js-examples",
"type": "commonjs",
"packageManager": "[email protected]",
"dependencies": {
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
},
"scripts": {
"run-strategy-pattern-example": "ts-node ./src/patterns/strategy/discount-example/example/example.exec.ts"
}
}
29 changes: 29 additions & 0 deletions src/patterns/strategy/discount-example/context/context.impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {FixedDiscount, IDiscountStrategy} from "../strategies";
import {IStrategyContext} from "./interfaces";
import {OrderHistory} from "./order-history.impl";

export class StrategyContext implements IStrategyContext {
private _orderHistory: OrderHistory;

constructor() {
// Initial default
this._strategy = new FixedDiscount(0);
this._orderHistory = new OrderHistory();
}

private _strategy: IDiscountStrategy;

get strategy(): IDiscountStrategy {
return this._strategy;
}

set strategy(strategy: IDiscountStrategy) {
this._strategy = strategy;
}

calculateFinalPrice(price: number): number {
this._orderHistory.addToHistory(price);

return this._strategy.calculateDiscount(price);
}
}
2 changes: 2 additions & 0 deletions src/patterns/strategy/discount-example/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './context.impl';
export * from './interfaces';
9 changes: 9 additions & 0 deletions src/patterns/strategy/discount-example/context/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface IStrategyContext {
calculateFinalPrice(price: number): number;
}

export interface IOrderHistory {
history: Array<unknown>;
isNextOrderMultipleOfThree: boolean;
bonusPercentage: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {IOrderHistory} from "./interfaces";

export class OrderHistory implements IOrderHistory {
private readonly _history: Array<unknown>;

constructor() {
this._history = [];
}

get history(): Array<unknown> {
return this._history;
}

get isNextOrderMultipleOfThree(): boolean {
return (this.history.length + 1) % 3 === 0;
}

get bonusPercentage(): number {
return this.history.length;
}

public addToHistory(orderDetails: unknown): void {
this._history.push(orderDetails);
}
}
45 changes: 45 additions & 0 deletions src/patterns/strategy/discount-example/example/example.exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {clientApp} from "./example.impl";

const RANDOM_PRICE = 1000;

// Calculating a final price using a fixed amount discount
clientApp({
price: RANDOM_PRICE,
discount: 350,
discountType: 'FixedDiscount',
});

// Calculating a final price using a percentage discount
clientApp({
price: RANDOM_PRICE,
discount: 15,
discountType: 'PercentageDiscount',
});

// Calculating a final price using a bonus discount
clientApp({
price: RANDOM_PRICE,
discount: 0,
discountType: 'BonusDiscount',
});

clientApp({
price: RANDOM_PRICE,
discount: 1000000,
// @ts-expect-error
discountType: 'non-existent discount strategy',
});

// Calculating a final price using a bonus discount
clientApp({
price: RANDOM_PRICE,
discount: 0,
discountType: 'BonusDiscount',
});

// Calculating a final price using a bonus discount
clientApp({
price: RANDOM_PRICE,
discount: 0,
discountType: 'BonusDiscount',
});
39 changes: 39 additions & 0 deletions src/patterns/strategy/discount-example/example/example.impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {BonusDiscount, FixedDiscount, PercentageDiscount} from "../strategies";
import {StrategyContext} from "../context";
import {IOrder} from "./interfaces";

const DiscountContext = new StrategyContext();

export const clientApp = (order: IOrder): number => {
let {discountType, price, discount} = order;

switch (discountType) {
case 'FixedDiscount':
DiscountContext.strategy = new FixedDiscount(discount);
break;
case 'PercentageDiscount':
DiscountContext.strategy = new PercentageDiscount(discount);
break;
case "BonusDiscount":
DiscountContext.strategy = new BonusDiscount(
DiscountContext.isNextOrderMultipleOfThree ? DiscountContext.bonusPercentage : 0
);
break;
default:
console.log('Unknown discount type. Using default strategy with 0 discount');
discount = 0;
DiscountContext.strategy = new FixedDiscount(discount);
break;
}

const priceWithDiscount = DiscountContext.calculateFinalPrice(price);

console.log({
currentStrategy: DiscountContext.strategy.name,
discount: discount,
beforeDiscount: price,
afterDiscount: priceWithDiscount,
});

return priceWithDiscount;
};
5 changes: 5 additions & 0 deletions src/patterns/strategy/discount-example/example/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IOrder {
price: number;
discount: number;
discountType: 'FixedDiscount' | 'PercentageDiscount' | 'BonusDiscount';
}
15 changes: 15 additions & 0 deletions src/patterns/strategy/discount-example/strategies/bonus.impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {IDiscountStrategy} from "./interfaces";

export class BonusDiscount implements IDiscountStrategy {
public name = 'BonusDiscount';

constructor(public bonus: number) {
};

// Every 3rd order gets a discount based on: dynamic percentage plus fixed amount, both equal to history length.
// Non-multiple of 3 orders get the original price.
calculateDiscount(price: number): number {
return price - ((price * this.bonus) / 100) - this.bonus;

}
}
12 changes: 12 additions & 0 deletions src/patterns/strategy/discount-example/strategies/fixed.impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {IDiscountStrategy} from "./interfaces";

export class FixedDiscount implements IDiscountStrategy {
public name = 'FixedDiscount';

constructor(public amount: number) {
};

calculateDiscount(price: number): number {
return this.amount > 0 ? price - this.amount : price;
}
}
4 changes: 4 additions & 0 deletions src/patterns/strategy/discount-example/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './fixed.impl';
export * from './percentage.impl';
export * from './bonus.impl';
export * from './interfaces';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IDiscountStrategy {
name: string;
calculateDiscount(price: number): number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {IDiscountStrategy} from "./interfaces";

export class PercentageDiscount implements IDiscountStrategy {
public name = 'PercentageDiscount';

constructor(public percentage: number) {
};

calculateDiscount(price: number): number {
return price - (price * this.percentage) / 100;
}
}
6 changes: 6 additions & 0 deletions src/patterns/strategy/strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Strategy Pattern Explanation:
The Strategy pattern allows you to define a family of algorithms, </br>
encapsulate each one, and make them replaceable. </br>
It lets the algorithm vary independently of the clients that use it.</br>

A typical use case would be implementing different **sorting** or **pricing** strategies.</br>
18 changes: 18 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Loading