Skip to content

Commit

Permalink
Merge pull request #10 from vahidvdn/feat/template-method
Browse files Browse the repository at this point in the history
feat: add template method bad practice
  • Loading branch information
vahidvdn authored Sep 11, 2024
2 parents 1e6db40 + 4797c2b commit dba9a17
Show file tree
Hide file tree
Showing 16 changed files with 391 additions and 1 deletion.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ Contributing to a community project is always welcome.
- [x] Chain of Responsibility Pattern
- [x] Builder Pattern
- [x] Decorator Pattern
- [ ] Template Method Pattern
- [x] Template Method Pattern
- [ ] Observer Pattern
- [ ] Proxy Pattern
- [ ] Command Pattern
- [ ] Single Responsibility

Expand Down
68 changes: 68 additions & 0 deletions app/template-method/README.md
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.
12 changes: 12 additions & 0 deletions app/template-method/bad-practice.ts
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);



46 changes: 46 additions & 0 deletions app/template-method/implemtations/alibaba-handler.spec.ts
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);
})
})
26 changes: 26 additions & 0 deletions app/template-method/implemtations/alibaba.handler.ts
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],
};
}
}
41 changes: 41 additions & 0 deletions app/template-method/implemtations/amazon-handler.spec.ts
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);
})
})
11 changes: 11 additions & 0 deletions app/template-method/implemtations/amazon-handler.ts
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];
}
}
2 changes: 2 additions & 0 deletions app/template-method/implemtations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './amazon-handler';
export * from './alibaba.handler';
31 changes: 31 additions & 0 deletions app/template-method/interface.ts
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;
}


10 changes: 10 additions & 0 deletions app/template-method/package.json
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"
}
}
33 changes: 33 additions & 0 deletions app/template-method/template-abstract.ts
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;
}

}
20 changes: 20 additions & 0 deletions app/template-method/template-method.ts
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()
24 changes: 24 additions & 0 deletions app/template-method/utils.spec.ts
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' });
})
})
Loading

0 comments on commit dba9a17

Please sign in to comment.