2

I am working on an Angular library.

Currently, when I want to retrieve data from my API I use my service:

@Injectable()
export class ProductService {

  public getProduct(id: number): Observable<Product> {
     // return response of http request
  }

}

It is a basic method which only return a Product which is an interface Now, I improve my method to return a Product class which can contains methods, other parameters, etc.

@Injectable()
export class ProductService {

  public getProduct(id: number): Observable<Product> {
     return this.http.get(`http://myapi/products/${id}`).pipe(
        map(response => response.data),
        map(productData => {
          const product = new Product()
          product.unserialize(productData)
          return product;
        })
     )
  }

}

Now Product is an instance of the Product class and I can implement methods in it like this:

export class Product extends Unserializable {
  ...
  get variantsCount(): number {
    return this.variants.length
  }
  ...
}

At this point, everything is pretty clean and work well. But let's say I want to retrieve Product information that must be gathered from the API or add static functions which retrieve one or more Products:

export class Product extends Unserializable {
  ...
  public get $variants (): Observable<ProductVariants> {
    return this.productService.getVariants(this);
  }

  public static get(id: number): Observable<this> {
    return this.productService.getProduct(id).pipe(
        map(productData => {
          const product = new Product()
          product.unserialize(productData)
          return product;
        })
     )
  }

  public static list(limit: number, skip = 0): Observable<this[]> {
     return this.productService.getProducts(limit, skip).pipe(
        map(productsData => {
          // Unserialize every products in the array
          ...
        })
     )
  }

  ...
}

It is a pattern I use a lot when working with VueJS. It would be possible to work with Products in a component like this:

ngOnInit() {
  this.$product = Product.get(this.id);
  this.$variants = this.$product.pipe(switchMap(product => product.variants))
  this.$products = Product.list(5, 0)
}

After all theses lines of code, here is my question:

The class Product is outside of the Angular scope, it is neither a service nor a module. So I am not able to use dependency injection to get the ProductService (or any service like HttpClient). How can I achieve that ? Do I have to provide the service every time I instantiate a new Product ? Can I use a singleton service and retrieve it within my Product instance ?

I've found this question: Getting instance of service without constructor injection with a question which explain how to import a service everywhere in the application. Is there a better solution ? Or maybe my pattern is an anti-pattern with angular.

Martin Paucot
  • 1,191
  • 14
  • 30
  • 1
    imho the idea is to use the service, so yes I would call it an antipattern. it is possible to inject a service into a plain class, I don't think it is a very clean way though. what is the difference between `this.$product = Product.get(this.id);` and `this.$product = this.productService.get(this.id);` afterall?! – tommueller Aug 03 '20 at 08:13
  • The job of this pattern is to use only one class as the Entity and the EntityManager and the service would be only a `client` for the api which simply returns the response of the http request. – Martin Paucot Aug 03 '20 at 08:20

2 Answers2

0

Regarding this part :

ngOnInit() {
  this.$product = Product.get(this.id);
  this.$variants = this.$product.pipe(switchMap(product => product.variants))
  this.$products = Product.list(5, 0)
}

I would say the Product class doesn't make sense anymore as a way to instanciate a product if you are just using static methods.

Basically your Product class behaves like a service and you should consider to change it to a service (which will allow you to use DI).

Gérôme Grignon
  • 3,859
  • 1
  • 6
  • 18
  • So it is an anti-pattern for Angular ? What about retrieving the `variants` of the Product ? Should I have to use `this.productService.getVariants(product)`. It is not really clean as I have to get the Product outside of the Observable – Martin Paucot Aug 03 '20 at 08:21
  • Yes as you are stuck with dependency injection. It would work in isolation (it's used with the Presenter pattern https://indepth.dev/model-view-presenter-with-angular/) but not in a such case. – Gérôme Grignon Aug 03 '20 at 08:25
  • change `this.$product.pipe(switchMap(product => product.variants))` to `this.productService.get(id).pipe(switchMap(product => product.variants))` – Gérôme Grignon Aug 03 '20 at 08:31
0

After looking to the different answers and comments. It looks like that it is an anti-pattern for Angular.

Where the business logic must be managed by services and/or components.

As I still want to have an answer to my question, I did some research and came to a solution fitting my needs.

First of all, I searched how to Inject a service without the automatic dependency Injector as I'm not in a Service nor a component and found this thread: Getting instance of service without constructor injection

I wanted to create my own dependency Injection to have a clean way of Injecting my services instead of doing Injector.get(service) everywhere I need a service.

As I still need to use the constructor to instantiate my class and eventually with parameters I had to find an other way to indicate which services I need.

So I made a decorator called InjectServices which will override the constructor

export function InjectServices(...services: any): any {
  return (constructor: any) => {
    return class extends constructor {
      constructor(...args: any[]) {
        super(...args);

        // The AppInjector cannot be imported before or the module is not instantiated
        const {AppInjector} = require('../mymodule.module');

        for (const service of services) {
          this[Utils.camelize(service.name)] = AppInjector.get(service);
        }
      }
    };
  };
}

It takes services in input, override the constructor to iterates over the arguments and Inject the services into the instance to the camelized name of the service.

I must define AppInjector when the module is instantiated:

export let AppInjector: Injector;

@NgComponent({...})
export class MyModule {
  constructor(private injector: Injector) {
    AppInjector = injector;
  }
}

I can now use the decorator anywhere in my application

import { InjectServices } from '../decorators/inject-service';
import { ProductService } from '../services/product.service';

@InjectServices(ProductService)
export class ProductEntity {

  // This line is just for typing
  private productService: ProductService;

  test(): void {
    this.productService.test();
  }

}

And here we have a (almost) nice dependency injection in a class which is neither a component nor a service !

Martin Paucot
  • 1,191
  • 14
  • 30