This is a typescript implementation of the C# package with the same name InteractR
InteractR is a indirection, pipeline and mediation framework inspired by the ideas of "Clean architecture" and MediatR. It is designed to separate the business / application specific logic from the presentation logic.
The idea is that you could create re-usable application componenets aka use cases that are independenten of infrastructure and presentation specifics.
Angular: https://github.com/madebykrol/interactr-ts.ng.resolver/
There are 3 main components related to building applications using InteractR
- Input (UseCase)
- Interactor
- Output (OutputPort)
Then there are the
- Interactor Hub
- Pipeline middleware
- Resolver
When a use case is executed through the interactor hub. You pass in input (extension of UseCase) and a presenter (implementation of OutputPort) The interactor then uses the inputdata to orchestrate some services, and eventually outputs data through the outputport.
Idealy usecases should be immutable and validate their own data on construction.
class MyUseCase extends UseCase<MyOutputPort> {
readonly firstname: string;
readonly lastname: string;
constructor(firstname: string, lastname: string) {
if(StringUtil.IsNullOrEmpty(firstname)) {
throw new FirstnameEmptyOrNullException('Firstname must be set to a value');
}
if(StringUtil.IsNullOrEmpty(lastname)) {
throw new LastnameEmptyOrNullException('Lastname must be set to a value');
}
this.firstname = firstname;
this.lastname = lastname;
}
}
The outputport is a abstraction between the actual presentation and the application logic and it's public API should be designed to best suit the application logic (The interactor) not the presentation.
As interfaces don't really exist as a concept in runtime it is recommended to create these as pure abstract classes. Which simply means that they do not implement any methods or properties at all.
abstract class MyOutputPort {
abstract displayFullName(name: string): void;
}
class MyUseCaseInteractor implements Interactor<MyUseCase, MyOutputPort> {
execute(usecase: MyUseCase, outputPort: MyOutputPort): UseCaseResult{
outputPort.displayFullName(usecase.firstname + ' ' + usecase.lastname);
}
}
The interactor hub is the thing that makes InteractR work. It introduces a level of indirection to your application where interactors won't be called directly. They can. But they should not be. It exposes a public method "execute" that takes a usecase and a outputport as parameters and outputs a UseCaseResult.
Typically you inject an instance of "Hub" into your components or controllers and then execute a usecase.
class MyComponent extends Component {
constructor(private interactor: Hub, /* other params */) {}
}
InteractR supports middleware pipelines. This means that you can register one or more middlewares to the execution pipeline, that will execute in the order of registration before and after the interactor executes. When a Middleware executes it can choose to keep the execution flow by invoking the next delegate function or terminate the pipeline by just returning without invoking next.
class MyUseCaseMiddleware implements Middleware<MyUseCase, MyOutputPort> {
run(usecase: MyUseCase, outputPort: MyOutputPort, next) {
// Do something before next middleware / interactor
next(usecase, outputPort);
// Do something after
}
}
You can also create a global pipeline that executes for all usecases by implementing the GlobalMiddleware abstract class.
When a use case is executed the interactor is resolved through a resolver. Either you can use the SelfContainedResolver where you register both Middleware and Interactors as instances or a resolver that uses a dependency injection container to resolve the instances. These are then called based on what Usecase you execute.
let resolver = new SelfContainedResolver();
let interactorHub = new InteractorHub(resolver);
interactorHub.execute(new MyUseCase(), new MyUseCaseOutputPortImpl());
Depending on what resolving strategy you might choose, the registration will be different. For example if your resolver uses a dependency injection container the instances will be resoler through that. But the resolver that is part of the package exposes public methods to register Middlewere and Interactors and will contain instances of interactors and middleware within it self.
let resolver = new SelfContainedResolver();
resolver.registerInteractor(new MyUseCaseInteractor(), MyUseCase);
You register middleware in the same way as interactors.
class MyComponent extends Component {
constructor(private interactor: Hub, /* other params */) {}
onLoginClick(): void {
this.presenter.setComponent(this);
this.interactor.execute(new LoginUseCase(username, password), presenter);
this.presenter.present();
}
}
import { SelfContainedResolver } from './selfcontained.resolver';
import { UseCase } from './usecase';
import { UseCaseResult } from './usecase.result';
import { Middleware, GlobalMiddleware } from './middleware';
import { Interactor } from './interactor';
import { InteractorHub } from './interactor.hub';
abstract class AbstractFooOutputPort {
abstract displayMessage(message: string): void;
}
class FooOutputPort implements AbstractFooOutputPort {
displayMessage(message: string): void { console.log(message); }
}
class FooUseCase extends UseCase<AbstractFooOutputPort> {}
class FooInteractor implements Interactor<FooUseCase, AbstractFooOutputPort> {
execute(usecase: FooUseCase, outputPort: AbstractFooOutputPort): UseCaseResult {
outputPort.displayMessage('Foo? Bar!');
return new UseCaseResult(true);
}
}
class FooMiddleware implements Middleware<FooUseCase, AbstractFooOutputPort> {
run(usecase: FooUseCase, outputPort: AbstractFooOutputPort, next: any): UseCaseResult {
console.log('Before interactor 1');
var result = next(usecase);
console.log('After interactor 1');
return result;
}
}
class FooMiddleware2 implements Middleware<FooUseCase, AbstractFooOutputPort> {
run(usecase: FooUseCase, outputPort: AbstractFooOutputPort, next: any): UseCaseResult {
console.log('Before interactor 2');
var result = next(usecase);
console.log('After interactor 2');
return result;
}
}
class FooMiddleware3 implements Middleware<FooUseCase, AbstractFooOutputPort> {
run(usecase: FooUseCase, outputPort: AbstractFooOutputPort, next: any): UseCaseResult {
outputPort.displayMessage('Terminate pipeline');
return new UseCaseResult(false);
}
}
class TerminatingGlobalMiddleware implements GlobalMiddleware {
run<T>(usecase: T, next: any): UseCaseResult {
return new UseCaseResult(false);
}
}
var resolver = new SelfContainedResolver();
resolver.registerInteractor(new FooInteractor(), FooUseCase);
resolver.registerMiddleware(new FooMiddleware(), FooUseCase);
resolver.registerMiddleware(new FooMiddleware2(), FooUseCase);
// resolver.registerGlobalMiddleware(new TerminatingGlobalMiddleware()); // This Global middleware will terminate the pipeline
// resolver.registerMiddleware(new FooMiddleware3(), FooUseCase); // With this middleware registrered the interactor won't execute
var hub = new InteractorHub(resolver);
var result = hub.execute(new FooUseCase(), new FooOutputPort());
console.log(result.success);