Question
What is the current (~2024) solution to extend/wrap/intercept/decorate a typescript class with additional behaviour to separate concerns?
There are various questions regarding wrapper classes, various concepts like bind
and Proxy
, iteration of prototype functions, (deprecated) npm helpers , etc.
All of these solutions come from various 'ages' of typescript and javascript.
What I mean
I have a business class, lets say a service:
class MyService{
public async getSomething(): Promise<string> {
return "Hello World!";
}
public async getSomethingElse(): Promise<string> {
return "Hello Someting else";
}
public async longRunningTask(): Promise<string> {
let r1 = await this.getSomething();
console.log(r1);
let r2 = await this.getSomethingElse();
console.log(r2);
return "All done"
}
}
It contains business logic and does its job.
Now I would like to extend the class by other aspects like logging or tracing. The open telemetry library provides sample code on how to achieve telemetry, but makes even simple services very hard to read or to understand.
Here would be the same service business logic with telemetry interweaved, now much more complicated, because it mixes two concerns in one location:
class MyServiceWithTelemetry{
public async getSomething(): Promise<string> {
return trace.startActiveSpan('getSomething', async (span) => {
span.setAttribute("specific to getSomething", "1234");
try
{
return "Hello World!";
}
finally{
span.end();
}
});
}
public async getSomethingElse(): Promise<string> {
return trace.startActiveSpan('getSomethingElse', async (span) => {
span.setAttribute("specific to getSomethingElse", "ELSE");
try
{
return "Hello Someting else";
}
finally{
span.end();
}
});
}
public async longRunningTask(): Promise<string> {
return trace.startActiveSpan('longRunningTask', async (span) => {
span.setAttribute("GUID", "1234");
try
{
let r1 = await this.getSomething();
console.log(r1);
let r2 = await this.getSomethingElse();
console.log(r2);
return "All done"
}
finally{
span.end();
}
});
}
}
There is a lot more going on here, then the business logic itself.
Everything is more nested, everything needs an additional try ... finally
to end a span.
My intention here is to be able to add more tracing/telemetry behaviour than "just add a console.log to every method".
In other languages I probably would extend
MyService
, override all the methods I need with telemetry code and have them then call the base class to perform the business logic.
AFAIK ts/js is very different from compiled languages (and even from interpreted one) with no specific virtual members
due to dynamic dispatch, _proto_
prototype existence, etc.
Is there a recommended ts/js native approach to the separation of concerns above?
The OP'S main problem seems to be, finding a way of how to augment already implemented functionality with the least possible code repetition and the least possible effort. Especially the OP's tracing example shows that once in a while developers are in need of reliable method-modifier abstractions which do wrap additional code around other functions/methods in ways that could be described as
around
,before
,after
,afterThrowing
andafterFinally
. Such modifiers in addition need to reliably deal with method-context and asynchronism.Note ... method-modification has nothing to do with Aspect Oriented Programming (AOP) because the former lacks any of the latter's necessary abstraction layers which are
Joinpoint
,Pointcut
andAdvice
.A very quick implementation tries to demonstrate possible implementations of
around
andafterThrowing
(atFunction.prototype
) together with just one possible way of theirs usage ...... here as around-handler functions, which each target a special
MyService
-method and implement the method's additional trace-specific functionality around its related, but already modified,MyService
-method. This other modification did wrap the targeted originalMyService
-method into try-catch functionality.Edit ... regarding the OP's comment ...
In case one does not want to rely on the above posted modifier and glue-code implementing factory based solution, one still can choose the path of decorated subclassing.
Then
TracingService
would extendMyService
.For convenience reasons one would choose for the former constructor's arguments signature an additional
trace
parameter as this constructor's first argument.The provided
trace
instance would be stored as private property.The prototypal methods of the latter (the extended) class need to be reimplemented by the former (the derived) class, but the implementation would apply delegation via
super
based method invocations.Thus, one just needs to write the tracing part, the code-reuse comes with delegation.