Decorator

Enhancing functionality while ensuring single responsibility

Decorator is a way to enhance a behavior of an object by adding a wrapper around its functionality. In the example below, class SimulatedApi provides a fake call to an API that can take anywhere between one and five seconds.
class SimulatedApi {
  makeRequest = async (): Promise =>
    new Promise((resolve) =>
      setTimeout(() => resolve(this.getData()), this.getTimeout() * 1000)
    );

  getData = () => [
    { name: "John Johnson", age: 32 },
    { name: "Jane Johnson", age: 32 },
    { name: "Jim Johnson", age: 10 },
  ];

  getTimeout = () => {
    const delay = Math.random() * (5 - 1) + 1;
    //console.log("delay (ms): ", Math.round(delay * 1000));
    return delay;
  };
}
Let's imagine that we want to enhance the method by tracking how long the calls take and also add caching in case the data has been previously received. Something that may come to mind first would be to extend a class and add desired behavior. But inheritance may put us on wheels such that future updates and enhancements may be limited. Favoring Composition over Inheritance, the solution is to create a new class and inject the API service as a dependency. First, we'll extract the interface:
interface IMakingSimulatedApiCalls {
  makeRequest(): Promise;
}
The next class implements this interface and calculates the total time it took for the data to come back.
class SimulatedApiWithLogging implements IMakingSimulatedApiCalls {
  simulatedApi: IMakingSimulatedApiCalls;
  startDate: number;
  endDate: number;
  constructor(simulatedApi: IMakingSimulatedApiCalls) {
    this.simulatedApi = simulatedApi;
  }

  async makeRequest(): Promise {
    this.startDate = Date.now();
    const responseData = await this.simulatedApi.makeRequest();
    this.endDate = Date.now();
    console.log("time taken (ms): ", this.endDate - this.startDate);
    return new Promise((resolve) => resolve(responseData));
  }
}
When the class is instantiated, it takes an instance of the SimulatedApi class in a constructor. This is the main idea behind a Decorator pattern -- get something, wrap it in another class and decorate the existing functionality with the extra functionality that's needed further. Single Responsibility Principle in action, so to speak.
In order to cache data and keep the code clean, we need to create a SimpleCacheAccessor class that implements an ICacheAccessor interface.
interface ICacheAccessor {
  setCache(incomingData: Array<{}>): void;
  getCache: () => Array<{}>;
  hasData(): boolean;
  showData(): any[];
}

class SimpleCacheAccessor implements ICacheAccessor {
  setCache(incomingData: {}[]): void {
    this.data = incomingData;
  }
  getCache = () => this.data;
  data: any[] = [];
  showData = () => this.data;
  hasData(): boolean {
    return this.data.length > 0;
  }
}
Finally, the code for the SimulatedApiWithCaching class that can make calls, log time taken, and use cache.
class SimulatedApiWithCaching implements IMakingSimulatedApiCalls {
  api: IMakingSimulatedApiCalls;
  cacheAccessor: ICacheAccessor;

  constructor(api: IMakingSimulatedApiCalls, cacheAccessor: ICacheAccessor) {
    this.api = api;
    this.cacheAccessor = cacheAccessor;
  }

  async makeRequest(): Promise {
    if (!this.cacheAccessor.hasData()) {
      console.log("reaching out to API");
      const result = await this.api.makeRequest();
      this.cacheAccessor.setCache(result);
      console.log("data:", this.cacheAccessor.showData());
    } else {
      console.log("data (from cache):", this.cacheAccessor.showData());
      return this.cacheAccessor.showData();
    }
  }
}
Here's the code to call an instance of the SimulatedApiWithCaching class. Notice the pattern like nesting dolls, this is also one of the peculiarities of the Decorator pattern -- nested wrapping of the instantiation calls.
let simpleApiAccessor = new SimulatedApi(); //instantiate api accessor
let apiAccessorWithLogging = new SimulatedApiWithLogging(simpleApiAccessor); //use it for class with logging
let cacheAccessor = new SimpleCacheAccessor();
let apiAccessorWithLoggingWithCaching = new SimulatedApiWithCaching( //use that for class with caching
  apiAccessorWithLogging,
  cacheAccessor
);
console.log("---  request 1 ---");
apiAccessorWithLoggingWithCaching.makeRequest();
setTimeout(() => {
  console.log("---  request 2 ---");
  apiAccessorWithLoggingWithCaching.makeRequest();
}, 5000);
Here are the results:
---  request 1 ---
reaching out to API
time taken (ms):  1815
data: [
  { name: 'John Johnson', age: 32 },
  { name: 'Jane Johnson', age: 32 },
  { name: 'Jim Johnson', age: 10 }
]
---  request 2 ---
data (from cache): [
  { name: 'John Johnson', age: 32 },
  { name: 'Jane Johnson', age: 32 },
  { name: 'Jim Johnson', age: 10 }
]