Typescript: Strategy Pattern and Reflection

in typescript •  7 years ago  (edited)

Hello,
in this tutorial, I am going to explain you how you can integrate some patterns, which are very common in the Java World, into your Typescript workflow to leverage some old patterns using the new tools that we have at our disposal.

With the help of Wikipedia let's define exactly what problem the Strategy Pattern solves for us:

  • A class should be configured with an algorithm instead of implementing an algorithm directly.
  • An algorithm should be selected and exchanged at run-time.

In Java Spring Boot we can do this kind of easily as these solutions are provided by the framework of choice.

The code hereby can be found in this repo:

https://github.com/Deviad/springfood/tree/master/src/main/java/com/davidepugliese/springfood

We create a Generic Entity Interface

package com.davidepugliese.springfood.models;

public interface GenericEntity {

}

We tell our User model to implement Generic Entity

@Entity
@Table(name = "users") // necessary if you want the table to be named users instead of user
public class User implements GenericEntity {
//    @JsonManagedReference

    @JsonIdentityInfo(
            generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "id")
            
    // cut for brevity
    

We create a GenricDAO that extends Generic Entity.

The most important part in this class is this line:
this.genericType = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(),GenericDAO.class);

That allows us to use T as a variable and use the value that it stores.

public class  GenericDAO<T extends GenericEntity> {
    private final SessionFactory sessionFactory;

    private final EntityManager em;
    private final Class<T> genericType;

    @Autowired
    @SuppressWarnings("unchecked")
    public GenericDAO (SessionFactory sessionFactory, EntityManager em) {
        this.sessionFactory = sessionFactory;
        this.em = em;
        this.genericType = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(),GenericDAO.class);
    }
    
... // cut for brevity

The session factory is provided in our WebConfig.java file like this

... // cut for brevity
    @Autowired
    private EntityManagerFactory entityManagerFactory;

    @Bean
    public SessionFactory getSessionFactory() {
        if (entityManagerFactory.unwrap(SessionFactory.class) == null) {
            throw new NullPointerException("factory is not a hibernate factory");
        }
        return entityManagerFactory.unwrap(SessionFactory.class);
    }
...    // cut for brevity

And this is our basic UserDAO class


package com.davidepugliese.springfood.domain;

import com.davidepugliese.springfood.models.Role;
import com.davidepugliese.springfood.models.User;
import com.davidepugliese.springfood.models.UserInfo;


public interface UserDAO {

//    public User getUser();

    void saveUser(User theUser);
    void updateUser(User theUser);
    void updateUserInfo(UserInfo userInfo);
    User getUser(Integer id);
    User getUserByUsername(String username);
//    Set<Role> getRolesByUsername(String username);
}

What we gained is that we don't have to write the implementation of our crate, read, update and delete methods for every DAO (Data Abstraction Object) so our code becomes way more compact.

Now, let's do the same with Typescript.

To achieve the same functionalities we need to use an IOC Container named Inversify JS.

The steps involved are pretty much the same.

For the sake of explanation I have created a repo that is available here with the main files involved.

The first step, again is to create our Generic Entity Interface.

export interface IGenericEntity<T> {
  create?: (entity: IGenericEntity<T>) => IGenericEntity<T>;
}

In our Bootstrap.ts we need to link our GenericDAO, now called TypeORMClient, to our UserDAO, now called UserService.

container.bind<TypeORMCLient<User>>(TYPES.UserService).to(UserService).whenInjectedInto(UserController);

This happens only when UserService is injected into UserController.

We then create our User Model like this:

import 'reflect-metadata';
import {Entity, PrimaryGeneratedColumn, Column, BaseEntity, Index, OneToMany} from 'typeorm';
import { injectable } from 'inversify';
import {IGenericEntity} from './IGenericEntity';
import {Token} from './';

export interface IUser extends IGenericEntity<IUser> {
  email: string;
  name: string;
  lastName: string;
  age: number;
  password: string;
  id?: number;
  role: number;
  token?: Token;
  tokens?: Token[];
  create?: (user: User) => IUser;
}

@injectable()
@Entity('users')
export class User extends BaseEntity implements IUser {

  @PrimaryGeneratedColumn()
  public id: number;

  // ... cut for brevity

  constructor() {
    super();
  }

  public create(user: IUser) {
    const _user = new User();
    _user.email = user.email;
    _user.name = user.name;
    _user.lastName = user.lastName;
    _user.age = user.age;
    _user.password = user.password;
    _user.role = user.role;
    return _user;
  }
}

We need a create function in order to create a new User.
In fact, when the app is booted, an instance of a User is created and, should we use the constructor, it would not find any parameter and an error would be thrown.

This is how our GenericDAO, here called TypeORMClient looks like:

import {Connection, EntityManager} from 'typeorm';
import {injectable, unmanaged} from 'inversify';
import { TypeORMConnection } from './connection';
import {IGenericEntity} from '../../model';

@injectable()
export class TypeORMCLient<T> {
  public db: Connection;
  public em: EntityManager;
  constructor(@unmanaged() private collection: { new (...args: any[]): IGenericEntity<T>; }) {
      TypeORMConnection.getConnection((connection) => {
          this.db = connection;
          this.em = this.db.manager;
        });
  }
  public find(collection?: IGenericEntity<any>, options?: object): Promise<void|IGenericEntity<T>[]> {
      return new Promise<void|IGenericEntity<T>[]>((resolve, reject) => {
        try {
          collection = collection || this.collection as IGenericEntity<any>;
          resolve(this.em.find(this.collection, options));
        } catch (err) {
        }
      });
  }

  public findOneOrFail(objectId: number): Promise<void|IGenericEntity<T>> {
    return new Promise<void|IGenericEntity<T>>((resolve, reject) => {
      try {
        resolve(this.em.findOneOrFail(this.collection, objectId));
      } catch (err) {
      }
    });
  }
  public findOneByOrFail(object: object): Promise<void|IGenericEntity<T>> {
    return new Promise<void|IGenericEntity<T>>((resolve, reject) => {
      try {
        resolve(this.em.findOneOrFail(this.collection, object));
      } catch (err) {
      }
    });
  }

  public save(model: IGenericEntity<T>, collection?: IGenericEntity<any>): Promise<void|IGenericEntity<T>> {
      const entity = new this.collection();
      return new Promise<IGenericEntity<T>>((resolve, reject) => {
        try {
          let data: any;
          data = collection || entity.create(model);
          resolve(this.em.save(data));
        } catch (err) {
        }
      });
  }

  public update(objectID: number, partial: Object, collection?: IGenericEntity<any>): Promise<void|IGenericEntity<T>> {
    return new Promise<any>((resolve, reject) => {
      try {
        collection = collection || this.collection as IGenericEntity<any>;
        resolve(this.em.update(collection as any, objectID, partial));
      } catch (err) {
      }
    });
  }

  public remove(model: IGenericEntity<T>): Promise<any> {
    return new Promise<any>((resolve, reject) => {
      try {
        resolve(this.em.remove(this.collection, model));
      } catch (err) {
      }
    });
  }

Here is our UserService

import {injectable} from 'inversify';
import { TypeORMCLient } from '../utils/sqldb/client';
import { User } from '../model';

@injectable()
class UserService extends TypeORMCLient<User> {
     constructor() {
         super(User);
     }
}

export default UserService;

That is where a class User, our generic type T, is sent to TypeORMCLient<User> so that,

constructor(@unmanaged() private collection: { new (...args: any[]): IGenericEntity<T>; }) { ... }

in TypeORMClient, can create the new object (the reason why we need the create method in the model instead of using constructor) and use its class name as a variable.

You will be asking why the difference in names, the answer is quite straightforward: different communities (Java's and Typescript's) used different names for the same ideas.

The next question is why you would use Typescript instead of Spring. Spring makes a step forward into making easy Web Programming with Java as programming with Servlets was more complicated. However it takes time to learn all the quirks involved into using Jackson as a serializer when combined with Hibernate. In my experience you need to find some fancy functions that make it possible to load collections the lazy way (on demand), otherwise the serializer will have problems with circular references.
Other solutions like Gson have performance issues and you might need to learn other tools like Gradle in order to produce your build and import dependencies automatically in your project.
All of these things make the learning curve on Java technologies steeper compared to Typescript.
Knowing the concepts however is very useful as you can apply them to other languages.

I hope that this tutorial will open your mind to a new world of possibilities.

Thank you.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!