diff --git a/package.json b/package.json index cca54a1..d03b6e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { "name": "js-examples", + "type": "commonjs", "packageManager": "yarn@4.0.2", "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" } } diff --git a/src/patterns/strategy/discount-example/context/context.impl.ts b/src/patterns/strategy/discount-example/context/context.impl.ts new file mode 100644 index 0000000..30a5a3f --- /dev/null +++ b/src/patterns/strategy/discount-example/context/context.impl.ts @@ -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); + } +} diff --git a/src/patterns/strategy/discount-example/context/index.ts b/src/patterns/strategy/discount-example/context/index.ts new file mode 100644 index 0000000..8bf03b8 --- /dev/null +++ b/src/patterns/strategy/discount-example/context/index.ts @@ -0,0 +1,2 @@ +export * from './context.impl'; +export * from './interfaces'; diff --git a/src/patterns/strategy/discount-example/context/interfaces.ts b/src/patterns/strategy/discount-example/context/interfaces.ts new file mode 100644 index 0000000..2f24e92 --- /dev/null +++ b/src/patterns/strategy/discount-example/context/interfaces.ts @@ -0,0 +1,9 @@ +export interface IStrategyContext { + calculateFinalPrice(price: number): number; +} + +export interface IOrderHistory { + history: Array; + isNextOrderMultipleOfThree: boolean; + bonusPercentage: number; +} diff --git a/src/patterns/strategy/discount-example/context/order-history.impl.ts b/src/patterns/strategy/discount-example/context/order-history.impl.ts new file mode 100644 index 0000000..58b6915 --- /dev/null +++ b/src/patterns/strategy/discount-example/context/order-history.impl.ts @@ -0,0 +1,25 @@ +import {IOrderHistory} from "./interfaces"; + +export class OrderHistory implements IOrderHistory { + private readonly _history: Array; + + constructor() { + this._history = []; + } + + get history(): Array { + 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); + } +} diff --git a/src/patterns/strategy/discount-example/example/example.exec.ts b/src/patterns/strategy/discount-example/example/example.exec.ts new file mode 100644 index 0000000..90ed231 --- /dev/null +++ b/src/patterns/strategy/discount-example/example/example.exec.ts @@ -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', +}); diff --git a/src/patterns/strategy/discount-example/example/example.impl.ts b/src/patterns/strategy/discount-example/example/example.impl.ts new file mode 100644 index 0000000..e789583 --- /dev/null +++ b/src/patterns/strategy/discount-example/example/example.impl.ts @@ -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; +}; diff --git a/src/patterns/strategy/discount-example/example/interfaces.ts b/src/patterns/strategy/discount-example/example/interfaces.ts new file mode 100644 index 0000000..957ae0e --- /dev/null +++ b/src/patterns/strategy/discount-example/example/interfaces.ts @@ -0,0 +1,5 @@ +export interface IOrder { + price: number; + discount: number; + discountType: 'FixedDiscount' | 'PercentageDiscount' | 'BonusDiscount'; +} diff --git a/src/patterns/strategy/discount-example/strategies/bonus.impl.ts b/src/patterns/strategy/discount-example/strategies/bonus.impl.ts new file mode 100644 index 0000000..bf817a6 --- /dev/null +++ b/src/patterns/strategy/discount-example/strategies/bonus.impl.ts @@ -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; + + } +} diff --git a/src/patterns/strategy/discount-example/strategies/fixed.impl.ts b/src/patterns/strategy/discount-example/strategies/fixed.impl.ts new file mode 100644 index 0000000..2cbd974 --- /dev/null +++ b/src/patterns/strategy/discount-example/strategies/fixed.impl.ts @@ -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; + } +} diff --git a/src/patterns/strategy/discount-example/strategies/index.ts b/src/patterns/strategy/discount-example/strategies/index.ts new file mode 100644 index 0000000..2ee05d2 --- /dev/null +++ b/src/patterns/strategy/discount-example/strategies/index.ts @@ -0,0 +1,4 @@ +export * from './fixed.impl'; +export * from './percentage.impl'; +export * from './bonus.impl'; +export * from './interfaces'; diff --git a/src/patterns/strategy/discount-example/strategies/interfaces.ts b/src/patterns/strategy/discount-example/strategies/interfaces.ts new file mode 100644 index 0000000..9cb8fce --- /dev/null +++ b/src/patterns/strategy/discount-example/strategies/interfaces.ts @@ -0,0 +1,4 @@ +export interface IDiscountStrategy { + name: string; + calculateDiscount(price: number): number; +} diff --git a/src/patterns/strategy/discount-example/strategies/percentage.impl.ts b/src/patterns/strategy/discount-example/strategies/percentage.impl.ts new file mode 100644 index 0000000..71de9ea --- /dev/null +++ b/src/patterns/strategy/discount-example/strategies/percentage.impl.ts @@ -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; + } +} diff --git a/src/patterns/strategy/strategy.md b/src/patterns/strategy/strategy.md new file mode 100644 index 0000000..19c22c2 --- /dev/null +++ b/src/patterns/strategy/strategy.md @@ -0,0 +1,6 @@ +# Strategy Pattern Explanation: +The Strategy pattern allows you to define a family of algorithms,
+encapsulate each one, and make them replaceable.
+It lets the algorithm vary independently of the clients that use it.
+ +A typical use case would be implementing different **sorting** or **pricing** strategies.
diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..31b2a5e --- /dev/null +++ b/tsconfig.json @@ -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" + ] +} diff --git a/yarn.lock b/yarn.lock index 512281a..24e72c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,14 +5,160 @@ __metadata: version: 8 cacheKey: 10c0 +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40cspotcode%2Fsource-map-support%2F-%2Fsource-map-support-0.8.1.tgz" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40jridgewell%2Fresolve-uri%2F-%2Fresolve-uri-3.1.2.tgz" + checksum: d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40jridgewell%2Fsourcemap-codec%2F-%2Fsourcemap-codec-1.5.0.tgz" + checksum: 2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40jridgewell%2Ftrace-mapping%2F-%2Ftrace-mapping-0.3.9.tgz" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b + languageName: node + linkType: hard + +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40tsconfig%2Fnode10%2F-%2Fnode10-1.0.11.tgz" + checksum: 28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40tsconfig%2Fnode12%2F-%2Fnode12-1.0.11.tgz" + checksum: dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40tsconfig%2Fnode14%2F-%2Fnode14-1.0.3.tgz" + checksum: 67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2F%40tsconfig%2Fnode16%2F-%2Fnode16-1.0.4.tgz" + checksum: 05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb + languageName: node + linkType: hard + +"acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Facorn-walk%2F-%2Facorn-walk-8.3.4.tgz" + dependencies: + acorn: "npm:^8.11.0" + checksum: 76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.4.1": + version: 8.14.0 + resolution: "acorn@npm:8.14.0::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Facorn%2F-%2Facorn-8.14.0.tgz" + bin: + acorn: bin/acorn + checksum: 6d4ee461a7734b2f48836ee0fbb752903606e576cc100eb49340295129ca0b452f3ba91ddd4424a1d4406a98adfb2ebb6bd0ff4c49d7a0930c10e462719bbfd7 + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Farg%2F-%2Farg-4.1.3.tgz" + checksum: 070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a + languageName: node + linkType: hard + +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Fcreate-require%2F-%2Fcreate-require-1.1.1.tgz" + checksum: 157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Fdiff%2F-%2Fdiff-4.0.2.tgz" + checksum: 81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + languageName: node + linkType: hard + "js-examples@workspace:.": version: 0.0.0-use.local resolution: "js-examples@workspace:." dependencies: + ts-node: "npm:^10.9.2" typescript: "npm:^5.6.3" languageName: unknown linkType: soft +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Fmake-error%2F-%2Fmake-error-1.3.6.tgz" + checksum: 171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f + languageName: node + linkType: hard + +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Fts-node%2F-%2Fts-node-10.9.2.tgz" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 + languageName: node + linkType: hard + "typescript@npm:^5.6.3": version: 5.6.3 resolution: "typescript@npm:5.6.3::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Ftypescript%2F-%2Ftypescript-5.6.3.tgz" @@ -32,3 +178,17 @@ __metadata: checksum: ac8307bb06bbfd08ae7137da740769b7d8c3ee5943188743bb622c621f8ad61d244767480f90fbd840277fbf152d8932aa20c33f867dea1bb5e79b187ca1a92f languageName: node linkType: hard + +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Fv8-compile-cache-lib%2F-%2Fv8-compile-cache-lib-3.0.1.tgz" + checksum: bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1::__archiveUrl=https%3A%2F%2Fnpm.dev.wixpress.com%2Fapi%2Fnpm%2Fnpm-repos%2Fyn%2F-%2Fyn-3.1.1.tgz" + checksum: 0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 + languageName: node + linkType: hard