Let's assume I have an interface
interface I <T extends InternalResult> {
doStuff(): T;
}
and two concrete classes implementing this interface:
class A implements I <InternalAResult> {
doStuff(): InternalAResult {
// implementation
}
}
class B implements I <InternalBResult> {
doStuff(): InternalBResult {
// implementation
}
}
My client cannot execute doStuff()
directly, but needs an instance of a class implementing an Executer
interface, like one of those:
interface Executer <T, R> {
execute(it: T): R;
}
class AExecuter implements Executer <A, AResult> {
execute(it: A): AResult {
let internalResult = it.doStuff();
// ... do something specific with internalResult create result
return result;
}
}
class BExecuter implements Executer <B, BResult> {
execute(it: B): BResult {
let internalResult = it.doStuff();
// ... do something other specific with internalResult create result
return result;
}
}
For such cases I always used the Visitor pattern in Java. I could create a visitor, pass two Executer
instances to it and implement the visit
method with an A
and a B
overloading, creating a conditional-free solution, like this:
class Visitor {
private aExecuter: AExecuter;
private bExecuter: BExecuter;
visit(it: A): Result {
return this.aExecuter.execute(it);
}
visit(it: B): Result {
return this.bExecuter.execute(it);
}
}
Now, there is no such thing as method overloading in TypeScript / JavaScript. But what is the alternative to creating a conditional like this?:
if (it instanceof A)
aExecuter.execute(it);
else if (it instanceof B)
bExecuter.execute(it);
Note: I want to have the A
class know nothing about AExecuter
. This would increase the coupling between those two classes and I could not switch the implementation of AExecutor
easily.
You can use the approach the typescript compiler team used and have a field that discriminates against the type of field, replacing instanceof
with a simple string/number comparison, which is probably less expensive (although you should test for your use case):
enum Types {
A, B
}
interface I <T extends InternalResult> {
readonly type : Types;
doStuff(): T;
}
class A implements I <AResult> {
readonly type = Types.A
doStuff(): AResult {
return new AResult();
}
}
class B implements I <BResult> {
readonly type = Types.B
doStuff(): BResult {
return new BResult();
}
}
class Visitor {
private aExecuter: AExecuter = new AExecuter();
private bExecuter: BExecuter = new BExecuter();
visit(it: A | B): AResult | BResult {
// Since we don't have a default return, we will get a compiler error if we forget a case
switch(it.type){
case Types.A : return this.aExecuter.execute(it); break;
case Types.B : return this.bExecuter.execute(it); break;
}
}
}
Another approach which requires less writing (but is less type safe) is to use the constructor name as a key for access the correct method:
class Visitor {
private aExecuter: AExecuter;
private bExecuter: BExecuter;
visit(it: A | B): AResult | BResult {
let visitor: (it: A | B) => AResult | BResult = (this as any)['visit' + it.constructor.name];
if(visitor == null) {
throw "Visitor not found"
}
return visitor.call(this, it)
}
visitA(it: A): AResult {
return this.aExecuter.execute(it);
}
visitB(it: B): BResult {
return this.bExecuter.execute(it);
}
}
Maybe link the executor directly to the class, like:
A.prototype.Executor = ExecutorA;
B.prototype.Executor = ExecutorB;
So then you can just do
(new it.Executor).execute(it)
Alternatively one could use a Map to link the class and the Executor:
const executors: Map<I, Executor> = new Map();
executors.set(A, ExecutorA).set(B, ExecutorB);
which can be used as:
(new executors.get( it.constructor )).execute(it)
You can always skip the visitor pattern completely and use a multimethod library instead, then you can simplify your code to something like this:
import { multi, method, Multi } from '@arrows/multimethod'
interface IExecute extends Multi {
(it: A): AResult
(it: B): BResult
}
const execute: IExecute = multi(
method(A, (it: A) => it.doStuff()),
method(B, (it: B) => it.doStuff()),
)
// Usage:
execute(new A())
execute(new B())
It less type-safe (compiler won't warn you if you put a multimethod together incorrectly - but it's trivial code), but it is a much simpler solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With