-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from vahidvdn/feat/template-method
feat: add template method bad practice
- Loading branch information
Showing
16 changed files
with
391 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
![template-method-pattern](../../assets/template-method.jpg) | ||
|
||
## 💡 Use Case | ||
|
||
Let's say we have some similar tasks that has some identical steps and some different steps. For example, we want to fetch the list of products from different providers (e.g. Amazon, Alibaba, etc). Some steps are the same, like sending request, some steps are optional, like pagination and some steps are different for each one. Let's see how it works. | ||
|
||
## ❌ Bad Practice | ||
|
||
One way is to have seperate functions for each one, which is not the best approach. The more you add different providers, the more it becomes unmaintainable. | ||
|
||
```ts | ||
export const getProducts = async (data) => { | ||
const amazonProducts = await makePostRequest(data); | ||
const paginatedAmazonProducts = await paginateAmazonData(amazonProducts); | ||
const parsedAmazon = await parseAmazonData(paginatedAmazonProducts); | ||
|
||
const alibabaProducts = await makePostRequest(data); | ||
const paginatedAlibabaProducts = await paginateAlibabaData(alibabaProducts); | ||
const parsedAlibaba = await parseAlibabaData(paginatedAlibabaProducts); | ||
|
||
return [parsedAmazon, parsedAlibaba]; | ||
} | ||
``` | ||
|
||
## ✅ Good Practice | ||
|
||
Now we can implement template method. Basically, template method is a parent class that you define some of the identical steps and also indicate abstract methods when they are different for each child class. So each child has it's own implementation for that specific method. | ||
|
||
Also you have a method in parent class that runs all the steps one by one, which is usually the same for all the children. | ||
|
||
```ts | ||
import { IBaseHandler, Paginated } from "./interface"; | ||
import axios, { AxiosRequestConfig } from "axios"; | ||
|
||
export abstract class BaseHandler<T, K> implements IBaseHandler<T, K> { | ||
protected baseUrl = 'https://jsonplaceholder.typicode.com'; | ||
protected api = 'posts'; | ||
|
||
abstract parseData(data: T | Paginated<T>): number[]; | ||
|
||
async fetchData(data: K): Promise<T> { | ||
const url = `${this.baseUrl}/${this.api}` | ||
const config: AxiosRequestConfig = { | ||
headers: { | ||
'Authorization': 'Bearer your-access-token', | ||
'Content-Type': 'application/json', | ||
}, | ||
}; | ||
const response = await axios.post<T>(url, data, config); | ||
return response.data; | ||
} | ||
|
||
async paginate(data: T): Promise<T | Paginated<T>> { | ||
return data; | ||
} | ||
|
||
async handle(data: K) { | ||
const result = await this.fetchData(data); | ||
const paginated = await this.paginate(result); | ||
const parsed = this.parseData(paginated); | ||
return parsed; | ||
} | ||
|
||
} | ||
|
||
``` | ||
|
||
As you see in the above code we also used typescript generic types. Then each child pass it's own shape of data. Check the full version of code to get more detailed info. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { PostData} from './interface'; | ||
import { getProducts } from './utils'; | ||
|
||
const data: PostData = { | ||
title: 'test title', | ||
body: 'title description', | ||
}; | ||
|
||
getProducts(data); | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import axios from "axios"; | ||
|
||
import { Paginated, PostData, ResponseData } from "../interface"; | ||
import { AlibabaHandler } from "./alibaba.handler"; | ||
|
||
describe('AlibabaHandler', () => { | ||
const postData: PostData = { | ||
title: 'test title', | ||
body: 'title description', | ||
}; | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('should handle amazon data', async () => { | ||
jest.spyOn(axios, 'post').mockReturnValue(Promise.resolve({ data: { id: 6, ...postData } })); | ||
const alibabaHandler = new AlibabaHandler(); | ||
const result = await alibabaHandler.fetchData(postData); | ||
expect(result).toEqual({ id: 6, ...postData }); | ||
|
||
const paginated = await alibabaHandler.paginate(result); | ||
expect(paginated).toEqual({ | ||
page: 1, | ||
offset: 10, | ||
total: 10, | ||
data: [result], | ||
}); | ||
|
||
const parsed = alibabaHandler.parseData(paginated as Paginated<ResponseData>); | ||
expect(parsed).toEqual([6, 11]); | ||
}) | ||
|
||
it('should handle amazon data all in handle function', async () => { | ||
const amazonHandler = new AlibabaHandler(); | ||
jest.spyOn(AlibabaHandler.prototype, 'fetchData').mockResolvedValue({ id: 6, ...postData }); | ||
jest.spyOn(AlibabaHandler.prototype, 'paginate').mockResolvedValue({ id: 6, ...postData }); | ||
jest.spyOn(AlibabaHandler.prototype, 'parseData').mockReturnValue([7]); | ||
|
||
const result = await amazonHandler.handle(postData); | ||
expect(result).toEqual([7]); | ||
expect(AlibabaHandler.prototype.fetchData).toHaveBeenCalledTimes(1); | ||
expect(AlibabaHandler.prototype.paginate).toHaveBeenCalledTimes(1); | ||
expect(AlibabaHandler.prototype.parseData).toHaveBeenCalledTimes(1); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import axios from "axios"; | ||
import { Paginated, PostData, ResponseData } from "../interface"; | ||
import { BaseHandler } from "../template-abstract"; | ||
|
||
export class AlibabaHandler extends BaseHandler<ResponseData, PostData> { | ||
protected api = 'posts'; | ||
|
||
parseData(result: Paginated<ResponseData>): number[] { | ||
console.log('parsing alibaba data...'); | ||
const id = result.data[0].id; | ||
return [id, id+5]; | ||
} | ||
|
||
async paginate(data: ResponseData): Promise<ResponseData | Paginated<ResponseData>> { | ||
console.log('paginating alibaba data...'); | ||
|
||
const url = `${this.baseUrl}/${this.api}` | ||
const response = await axios.post<ResponseData>(url, data); | ||
return { | ||
page: 1, | ||
offset: 10, | ||
total: 10, | ||
data: [response.data], | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import axios from "axios"; | ||
|
||
import { PostData, ResponseData } from "../interface"; | ||
import { AmazonHandler } from "./amazon-handler"; | ||
|
||
describe('AmazonHandler', () => { | ||
const postData: PostData = { | ||
title: 'test title', | ||
body: 'title description', | ||
}; | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('should handle amazon data one by one', async () => { | ||
jest.spyOn(axios, 'post').mockReturnValue(Promise.resolve({ data: { id: 6, ...postData } })); | ||
const amazonHandler = new AmazonHandler(); | ||
const result = await amazonHandler.fetchData(postData); | ||
expect(result).toEqual({ id: 6, ...postData }); | ||
|
||
const paginated = await amazonHandler.paginate(result); | ||
expect(paginated).toEqual(result); | ||
|
||
const parsed = amazonHandler.parseData(paginated as ResponseData); | ||
expect(parsed).toEqual([6]); | ||
}) | ||
|
||
it('should handle amazon data all in handle function', async () => { | ||
const amazonHandler = new AmazonHandler(); | ||
jest.spyOn(AmazonHandler.prototype, 'fetchData').mockResolvedValue({ id: 6, ...postData }); | ||
jest.spyOn(AmazonHandler.prototype, 'paginate').mockResolvedValue({ id: 6, ...postData }); | ||
jest.spyOn(AmazonHandler.prototype, 'parseData').mockReturnValue([7]); | ||
|
||
const result = await amazonHandler.handle(postData); | ||
expect(result).toEqual([7]); | ||
expect(AmazonHandler.prototype.fetchData).toHaveBeenCalledTimes(1); | ||
expect(AmazonHandler.prototype.paginate).toHaveBeenCalledTimes(1); | ||
expect(AmazonHandler.prototype.parseData).toHaveBeenCalledTimes(1); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { PostData, ResponseData } from "../interface"; | ||
import { BaseHandler } from "../template-abstract"; | ||
|
||
export class AmazonHandler extends BaseHandler<ResponseData, PostData> { | ||
parseData(data: ResponseData): number[] { | ||
console.log('parsing amazon data...'); | ||
|
||
const id = data.id; | ||
return [id]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './amazon-handler'; | ||
export * from './alibaba.handler'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
export interface IBaseHandler<T, K> { | ||
fetchData(data: K): Promise<T>; | ||
paginate(data: T): Promise<Paginated<T> | T>; // maybe has paginate, maybe not | ||
parseData(data: T): number[]; | ||
} | ||
|
||
export interface Paginated<T> { | ||
page: number; | ||
offset: number; | ||
total: number; | ||
data: T[]; | ||
} | ||
|
||
export interface ResponseData { | ||
title: string; | ||
body: string; | ||
id: number; | ||
} | ||
|
||
export interface PostData { | ||
title: string; | ||
body: string; | ||
} | ||
|
||
export interface RequestPayload<T> { | ||
params: Record<string, string>; | ||
headers: Record<string, string>; | ||
data: T; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"scripts": { | ||
"start": "ts-node template-method", | ||
"start:bad": "ts-node bad-practice" | ||
}, | ||
"devDependencies": { | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.4.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { IBaseHandler, Paginated } from "./interface"; | ||
import axios, { AxiosRequestConfig } from "axios"; | ||
|
||
export abstract class BaseHandler<T, K> implements IBaseHandler<T, K> { | ||
protected baseUrl = 'https://jsonplaceholder.typicode.com'; | ||
protected api = 'posts'; | ||
|
||
abstract parseData(data: T | Paginated<T>): number[]; | ||
|
||
async fetchData(data: K): Promise<T> { | ||
const url = `${this.baseUrl}/${this.api}` | ||
const config: AxiosRequestConfig = { | ||
headers: { | ||
'Authorization': 'Bearer your-access-token', | ||
'Content-Type': 'application/json', | ||
}, | ||
}; | ||
const response = await axios.post<T>(url, data, config); | ||
return response.data; | ||
} | ||
|
||
async paginate(data: T): Promise<T | Paginated<T>> { | ||
return data; | ||
} | ||
|
||
async handle(data: K) { | ||
const result = await this.fetchData(data); | ||
const paginated = await this.paginate(result); | ||
const parsed = this.parseData(paginated); | ||
return parsed; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { AlibabaHandler, AmazonHandler } from "./implemtations"; | ||
import { PostData } from "./interface"; | ||
|
||
const postData: PostData = { | ||
title: 'test title', | ||
body: 'title description', | ||
}; | ||
|
||
async function bootstrap() { | ||
const amazon = new AmazonHandler(); | ||
const amazonResult = await amazon.handle(postData); | ||
console.log('amazonResult: ', amazonResult); | ||
|
||
|
||
const alibaba = new AlibabaHandler(); | ||
const alibabResult = await alibaba.handle(postData); | ||
console.log('alibabResult: ', alibabResult); | ||
} | ||
|
||
bootstrap() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { getProducts } from './utils' | ||
import { PostData } from "./interface"; | ||
import axios from "axios"; | ||
|
||
describe('Template Method bad practice', () => { | ||
|
||
const data: PostData = { | ||
title: 'test title', | ||
body: 'title description', | ||
}; | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should fetch data', async () => { | ||
jest.spyOn(axios, 'post').mockReturnValueOnce(Promise.resolve({ data: { id: 5, ...data } })); | ||
jest.spyOn(axios, 'post').mockReturnValue(Promise.resolve({ data: { id: 6, ...data } })); | ||
|
||
const [parsedAmazon, parsedAlibaba] = await getProducts(data); | ||
expect(parsedAmazon).toEqual({ id: 5, ...data }); | ||
expect(parsedAlibaba).toEqual({ id: 6, ...data, idWithTitle: '6 - test title' }); | ||
}) | ||
}) |
Oops, something went wrong.