In some of my C# project I used the Specification pattern for handling business rules. An example of this can be found at https://en.wikipedia.org/wiki/Specification_pattern. But I was wondering if we could create the same experience in Typescript as well. So lets give it a try. I just used the example of the WIKI site but converted it to Typescript.
Specify the interface
Before creating specifications we need the ISpecification interface
export interface ISpecification<T> {
IsSatisfiedBy(candidate: T): boolean;
}
I don't like the way Wiki creates a ISpecification<T> with the And/Or/Not operators so I created a second interface which contains the operators for composite specifications.
export interface ICompositeSpecification<T> extends ISpecification<T>{
and(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
or(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
not(): ICompositeSpecification<T>;
}
Creating Base classes
First we will create a base class for the composite specification.
export abstract class CompositeSpecification<T> implements ICompositeSpecification<T>
{
abstract IsSatisfiedBy(candidate: T): boolean;
and(other: ICompositeSpecification<T>) : ICompositeSpecification<T> {
return new AndSpecification<T>(this, other);
}
or(other: ICompositeSpecification<T>) : ICompositeSpecification<T> {
return new OrSpecification<T>(this, other);
}
not() : ICompositeSpecification<T>{
return new NotSpecification<T>(this);
}
}
As you can see it is also possible in Typescript to use abstract classes with abstract methods! The class must also be 'exported' so it can be used as base class for other Specifications in your project. Now we only need to create the And/Or and Not specification like described in the Wiki page. First the AndSpecification
class AndSpecification<T> extends CompositeSpecification<T>{
constructor(
public left:ICompositeSpecification<T>,
public right:ICompositeSpecification<T>){
super();
}
IsSatisfiedBy(candidate: T) : boolean{
return this.left.IsSatisfiedBy(candidate)
&& this.right.IsSatisfiedBy(candidate);
}
}
As you can see, there is not much code needed for creating this class. The 'left' and 'right' member are specified by the constructor so I can skip the code for settings properties like
this.left = left;
this.right = right
With this AndSpecification in place the other specification are more or less the same except the IsSatisfiedBy method
class OrSpecification<T> extends CompositeSpecification<T>{
constructor(
public left:ICompositeSpecification<T>,
public right:ICompositeSpecification<T>){
super();
}
IsSatisfiedBy(candidate: T) : boolean{
return this.left.IsSatisfiedBy(candidate)
|| this.right.IsSatisfiedBy(candidate);
}
}
class NotSpecification<T> extends CompositeSpecification<T>{
constructor(
public spec:ICompositeSpecification<T>){
super();
}
IsSatisfiedBy(candidate: T) : boolean{
return !this.spec.IsSatisfiedBy(candidate);
}
}
Example of usage
As an example I created a Person class with name, age and gender property
enum Gender {
Male,
Female
}
class Person {
constructor(
public name: string,
public age: number,
public gender: Gender) {
}
}
The create a list of Persons and use that inside the app class
import { Component, OnInit } from '@angular/core';
import { CompositeSpecification, ISpecification} from './specifications';
@Component({
selector: 'my-app',
templateUrl: 'app/app.component.html'
})
export class AppComponent implements OnInit {
persons: Person[] = [];
ngOnInit() {
this.persons.push(new Person('Mike', 34, Gender.Male));
this.persons.push(new Person('Perry', 8, Gender.Male));
this.persons.push(new Person('Gregory', 6, Gender.Male));
this.persons.push(new Person('Rachel', 3, Gender.Female));
this.persons.push(new Person('Betty', 35, Gender.Female));
}
}
I created this example with Angular2 but this will work in any Typescript application
Let say you want to filter the list of persons with the following specification:
- All mature persons
- All mature female persons
- All mature persons and females
- All immature persons or female
For this we need the following specification classes
export class IsMatureSpecification extends CompositeSpecification<Person>{
IsSatisfiedBy(candidate: Person): boolean {
return candidate.age > 18;
}
}
export class GenderSpecification extends CompositeSpecification<Person>{
constructor(private gender: Gender){ super(); }
IsSatisfiedBy(candidate: Person): boolean {
return candidate.gender == this.gender;
}
}
We can add these specifications to the AppComponent class
export class AppComponent implements OnInit {
persons: Person[] = [];
private femaleSpec = new GenderSpecification(Gender.Female);
private matureSpec = new IsMatureSpecification();
....
}
In the AppComponent I created a private method for filtering the list of persons using a specification
private executeSpecification(spec: ISpecification<Person>) {
let filteredList: Person[] = [];
this.persons.forEach(person => {
if (spec.IsSatisfiedBy(person)) {
filteredList.push(person);
}
});
return filteredList;
}
With the private method from above I can easily show you that the specifications work with the following code
get females() {
return this.executeSpecification(this.femaleSpec);
}
get matureFemales() {
let matureFemales = this.femaleSpec.and(this.matureSpec);
return this.executeSpecification(matureFemales);
}
get matureOrFemales() {
let matureFemales = this.femaleSpec.or(this.matureSpec);
return this.executeSpecification(matureFemales);
}
get immatureOrFemales() {
let matureFemales = this.femaleSpec.or(this.matureSpec.not());
return this.executeSpecification(matureFemales);
}
Combine this with the following app.component.html and you can see filtered lists of persons
<h1>Specification Patterns</h1>
<div *ngFor="let person of persons">
{{person.name}} - {{person.age}}
</div>
<hr>
<div *ngFor="let female of females">
{{female.name}} - {{female.age}}
</div>
<hr>
<div *ngFor="let female of matureFemales">
{{female.name}} - {{female.age}}
</div>
<hr>
<div *ngFor="let person of matureOrFemales">
{{person.name}} - {{person.age}}
</div>
<hr>
<div *ngFor="let person of immatureOrFemales">
{{person.name}} - {{person.age}}
</div>
This will give the following output in the browser
Conclusion
YES! It is possible and very ease to use the specification pattern in Typescript.
Note: After I finished this blog post I found an npm package that has more or less the same implementation of the specification pattern at: https://www.npmjs.com/package/ts-specification