Decoupled Design with Rx
Following Jay Bazuzi’s post, Decoupled Design with Events, I wanted to show a possible variation using Rx in Typescript.
In particular for this discussion I like how Subject
s can act as channels (or ‘topics’) for eventing.
Here’s a simplified Typescript version of Jay’s event-based solution (I tried staying loyal to the OP):
export class Program {
public static configure() {
const foo = new Foo();
const aClass = new AClass();
const bar = foo.bar.bind(foo);
aClass.onBaz = bar;
return { aClass, foo, bar };
}
public static main() {
const { aClass } = this.configure();
aClass.do();
}
}
type Action<T> = (t: T) => void;
export class AClass {
public onBaz!: Action<string>;
public do() {
this.onBaz?.("Hello, World!");
}
}
class Foo {
public bar(message: string) {
console.log(message);
}
}
And the tests (I’m using ava
here):
test("AClass raises event at right time", (t) => {
const aClass = new AClass();
const onBaz = sinon.spy();
aClass.onBaz = onBaz;
aClass.do();
t.deepEqual(onBaz.firstCall.firstArg, "Hello, World!");
});
test("configure", (t) => {
const { aClass, bar } = Program.configure();
t.is(aClass.onBaz, bar);
});
with Rx
For our Rx example, we’ll be replacing onBaz: Action<string>
with a rxjs.Subject<string>
.
class AClass {
private readonly baz = new Subject<string>();
public get onBaz() {
return this.baz.asObservable();
}
public do() {
this.baz.next("Hello, Rx!");
}
}
AClass
will be emitting events via its onBaz: Observable
.
This can be tested in isolation by subscribing to onBaz
:
const aClass = new AClass();
const bazSpy = sinon.spy();
aClass.onBaz.subscribe(bazSpy);
aClass.do();
t.deepEqual(bazSpy.firstCall.firstArg, "Hello, Rx!");
Program.configure()
will tie things up - connecting the source of the event (AClass
) the consumer (Foo
).
public static configure() {
// the channel through which the events are flowing can be extracted
// as a separate entity and later used in tests
const bazChannel = new Subject<string>;
const foo = new Foo();
const bar = foo.bar.bind(foo);
// foo starts reacting to bazChannel events
bazChannel.subscribe(bar)
const aClass = new AClass();
// bazChannel starts receiving baz events from aClass
aClass.onBaz.subscribe(bazChannel);
return { aClass, foo, bar, bazChannel };
}
Here I’ve introduced a second Subject
to act as a connecting channel between emitter and consumer, this is not necessary but it creates an extra separation which I sometimes like.
There might be a few benefits for doing that, but the one that I care about in our context is - this makes the connecting tissue separate, explicit, testable, with a proper name, and basically makes things clearer.
Here’s an example of how we can use this extra entity in our tests - we can intercept bazChannel
events as they’re emitted from the source:
const { aClass, bazChannel } = Program.configure();
const bazSpy = sinon.spy();
bazChannel.subscribe(bazSpy);
aClass.do();
t.deepEqual(bazSpy.firstCall.firstArg, "Hello, Rx!");
Full source code, including all of Jay’s examples translated to Typescript can be found here.