interactr v3.0.4
Interactr v3.0.3
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.
Usage - Setting up your first usecase
There are 3 main components related to building applications using InteractR 1. Input (UseCase) 2. Interactor 3. Output (OutputPort)
Then there are the 1. Interactor Hub 2. Pipeline middleware 3. 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.
Usecase
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;
}
}
OutputPort
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;
}
Interactor
class MyUseCaseInteractor implements Interactor<MyUseCase, MyOutputPort> {
execute(usecase: MyUseCase, outputPort: MyOutputPort): Promise<UseCaseResult>{
outputPort.displayFullName(usecase.firstname + ' ' + usecase.lastname);
}
}
Hub
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 */) {}
}
Middleware
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): Promise<UseCaseResult> {
// 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.
Resolver
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);
await interactorHub.execute(new MyUseCase(), new MyUseCaseOutputPortImpl());
Registrating a interactor
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.
SelfContainedResolver
let resolver = new SelfContainedResolver();
resolver.registerInteractor(new MyUseCaseInteractor(), MyUseCase);
Registrating middleware
You register middleware in the same way as interactors.
Executing a usecase
class MyComponent extends Component {
constructor(private interactor: Hub, /* other params */) {}
onLoginClick(): void {
this.presenter.setComponent(this);
await this.interactor.execute(new LoginUseCase(username, password), presenter);
this.presenter.present();
}
}
Update history
3.0.x
Breaking changes in 3.0.0
1) Filename notations and casings changed.
For example import {InteractorHub} from 'interactr/InteractorHub'
should now be import {InteractorHub} from 'interactr/interactor.hub'
Bug fixes in 3.0.0
1) The self contained resolver now supports use-cases with constructor parameters.
Minor Updates in 3.0.1
1) Patches to readme to reflect updates 2) You can now import packages directly from 'interactr'
Minor Updates in 3.0.2
1) Patches to readme to reflect updates 2) Patch to default exports in index.ts
Minor updates in 3.0.3
1) Patches to readme
Minor updates in 3.0.4
1) introduced Constructor type to make the registrator interface and the self contained resolver more readable. 2) Updated example code. 3) Patches to readme
2.0.x
Breaking changes in 2.0.0
1) Interactr library now utilizes "Promises" which makes the calls to hub.execute awaitable. 2) Changes to the file names has been made to make them consistent to comply with 'forceConsistentCasingInFileNames: true'
Example code (example.ts)
import {
Interactor,
InteractorHub,
Middleware,
GlobalMiddleware,
UseCaseResult,
UseCase,
SelfContainedResolver
} from './';
abstract class AbstractFooOutputPort {
abstract displayMessage(message: string): void;
}
abstract class AbstractBarOutputPort {
abstract displayMessage(message: string): void;
}
class FooOutputPort implements AbstractFooOutputPort {
displayMessage(message: string): void { console.log(message); }
}
class FooUseCase extends UseCase<AbstractFooOutputPort> {
constructor(public fooParam: string) {
super();
}
}
class FooInteractor implements Interactor<FooUseCase, AbstractFooOutputPort> {
async execute(usecase: FooUseCase, outputPort: AbstractFooOutputPort): Promise<UseCaseResult> {
outputPort.displayMessage('Foo?');
return new UseCaseResult(true);
}
}
class BarOutputPort implements AbstractBarOutputPort {
displayMessage(message: string): void { console.log(message); }
}
class BarUseCase extends UseCase<AbstractBarOutputPort> {}
class BarInteractor implements Interactor<BarUseCase, AbstractBarOutputPort> {
async execute(usecase: BarUseCase, outputPort: AbstractBarOutputPort): Promise<UseCaseResult> {
outputPort.displayMessage('Bar!');
return new UseCaseResult(true);
}
}
class FooMiddleware implements Middleware<FooUseCase, AbstractFooOutputPort> {
run(usecase: FooUseCase, outputPort: AbstractFooOutputPort, next: any): Promise<UseCaseResult> {
console.log('Before foo interactor 1');
var result = next(usecase);
console.log('After foo interactor 1');
return result;
}
}
class FooMiddleware2 implements Middleware<FooUseCase, AbstractFooOutputPort> {
run(usecase: FooUseCase, outputPort: AbstractFooOutputPort, next: any): Promise<UseCaseResult> {
console.log('Before foo interactor 2');
var result = next(usecase);
console.log('After foo interactor 2');
return result;
}
}
class FooMiddleware3 implements Middleware<FooUseCase, AbstractFooOutputPort> {
async run(usecase: FooUseCase, outputPort: AbstractFooOutputPort, next: any): Promise<UseCaseResult> {
outputPort.displayMessage('Terminate pipeline');
return new UseCaseResult(false);
}
}
class TerminatingGlobalMiddleware implements GlobalMiddleware {
async run<T>(usecase: T, next: any): Promise<UseCaseResult> {
return new UseCaseResult(false);
}
}
const run = async () => {
var resolver = new SelfContainedResolver();
resolver.registerInteractor(new FooInteractor(), FooUseCase);
resolver.registerInteractor(new BarInteractor(), BarUseCase);
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 fooResult = await hub.execute(new FooUseCase("Foo input"), new FooOutputPort());
var barResult = await hub.execute(new BarUseCase(), new BarOutputPort());
console.log(fooResult.success);
console.log(barResult.success);
}
run();