Aspect-oriented Programming
By Tomás Cordara, EDRANS Backend developer
At EDRANS we did a hackathon with the purpose of designing a service layer that would act as a wrapper of an HTTP client and would add extra behavior to it. Cool, uh?
In the end, the organizer of the hackathon showed the development team a solution implemented in Ruby that gives the service layer extra behavior but without changing its implementation, using ideas from Aspect-Oriented Programming, specifically the Decorator Pattern:
Hello there, Decorator Pattern
A Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects.
With this idea in mind… I started thinking about how would I implement that idea with the languages that I commonly use: TypeScript and C#
In TypeScript, by turning on a compiler flag called “experimentalDecorators”
json{
“compilerOptions”: {
“experimentalDecorators”: true
}
}
We can write code that looks like this
typescriptimport { measureElapsedTime } from “../decorators/measureElapsedTime”;
import { memoization } from “../decorators/memoization”;
import { maxLimitCalls } from “../decorators/maxLimitCalls”;
import axios from ‘axios’;export class SearchService { url = “https://www.thesportsdb.com/api/v1/json/1/searchteams.php"; @measureElapsedTime
@maxLimitCalls(3)
@memoization
get(q) {
var path = url + “?t=” + q;
return axios.get(path).then(r => r.data);
}
}
Where we can write a particular search functionality, and add extra behavior to it via decorators.
These decorators are going to be applied every time the function is called and would live in the same context of that function.
Measure Elapsed Time
typescriptimport { performance } from “perf_hooks”;export const measureElapsedTime = (
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) =>
{
const originalMethod = descriptor.value;
descriptor.value = function (…args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const finish = performance.now();
console.log(`Execution time: ${finish — start} milliseconds`)
return result;
};
return descriptor;
};
Memoization
typescriptexport function memoization(target: any, propertyKey: string, descriptor: PropertyDescriptor)
{
const originalValue = descriptor.value;
const cache = new Map<any, any>();
let d = Date.now(); descriptor.value = function (arg: any) { let now = Date.now();
let diff = (now - d)/1000; if (cache.has(arg) && diff < 10) {
console.log("from memo")
return cache.get(arg);
} var result = originalValue.apply(this, [arg]); cache.set(arg, result);
d = Date.now();
return result;
}
}
MaxLimitCalls
typescriptexport const maxLimitCalls = (n: number) =>
{ let times = 0
return (target: any, propertyKey: string, descriptor:
PropertyDescriptor) =>
{
const originalMethod = descriptor.value;
descriptor.value = function (…args) {
if(times >= n){
throw new Error("Over " + times + " Times!");
} const result = originalMethod.apply(this, args);
times++; console.log("Called "+ times + " time" + (times > 1 ? "s" : "")); return result;
};
};
};
C#
The same idea could be implemented over an ASP.NET API endpoint. In C#, attributes can be placed on almost any declaration, and they are used by the internal teams at Microsoft to build features for platforms:
CSharppublic class ValuesController : ControllerBase {
static string url = "https://www.thesportsdb.com/api/v1/json/1/searchteams.php"; [MeasureElapsedTime]
[MaxLimitCalls(3)]
[Memoization]
public async Task<string> Get(string q = default)
{
using (var httpClient = new HttpClient())
{
var path = $"{url}?t={q}";
var response = await httpClient.GetAsync(path);
return await response.Content.ReadAsStringAsync();
}
}
}
MeasureElapsedTime
CSharppublic class MeasureElapsedTime : Attribute, IAsyncActionFilter {
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var watch = new Stopwatch();
watch.Start();
var resultContext = await next();
Console.WriteLine($"Execution time: ${ watch.ElapsedMilliseconds } milliseconds");
}
}
MaxLimitCalls
CSharppublic class MaxLimitCalls : Attribute, IAsyncActionFilter {
public int times;
public static string Cache_Name = "MaxLimitCalls";
public MaxLimitCalls(int times)
{
this.times = times;
}
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
//Needs to be injected as a dependency in "ConfigureServices"
var cache = context.HttpContext.RequestServices.GetService<IMemoryCache>(); if(!cache.TryGetValue(Cache_Name, out int cont))
{
cont = 1;
cache.Set(Cache_Name, cont);
}
else
{
if (cont >= times)
throw new Exception($"Over {times} Times!");
cont = cont + 1;
cache.Set(Cache_Name, cont);
}
var resultContext = await next();
Console.WriteLine("Called " + cont + " time" + (cont > 1 ? "s" : ""));
}
}
Memoization
CSharppublic class Memoization: Attribute, IAsyncActionFilter
{
public static string Cache_Name = "Memoization";
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
//Needs to be injected as a dependency in "ConfigureServices"
var cache = context.HttpContext.RequestServices.GetService<IMemoryCache>(); //a key for the arguments
var serial = JsonSerializer.Serialize(context.ActionArguments); if (!cache.TryGetValue($"{Cache_Name}{serial}", out IActionResult result))
{
var resultContext = await next();
cache.Set($"{Cache_Name}{serial}", resultContext.Result);
}
else
{
Console.WriteLine("From memo!");
context.Result = result;
}
}
}
Want to join our Software Development Team?
Drop us an email at opportunities@edrans.com or check our Career page by clicking here.