Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid repeatable code with Observables(DRY)

Here is my code (with renamed methods) from Angular:

I have a function interface/contract

type IAutocomlete = (
  term: string,
  selectedIds?: string[]
) => Observable<TableFilterQueryResponse[]>;

This is my draft service that I want to inject in my Components in the future. I will have more methods with similar implementation.

    export class SuperCleanService {
      constructor(
        private dataController: DataController,
        private serviceController: ServiceController
      ) {}
    .....
    private searchAs$: IAutocomlete = (term, selectedIds) => {
        return this.dataController
          .service1GET(term, selectedIds)
          .pipe(
            map((res) =>
              res.map((a) => ({
                viewValue: a.name,
                value: a.id
              }))
            )
          );
      };
      private searchB$: IAutocomlete = (term, selectedIds) => {
        return this.serviceController
          .service1(term, selectedIds)
          .pipe(
            map((res) =>
              res.map((a) => ({
                viewValue: a.id,
                value: a.id
              }))
            )
          );
      };
      private searchC$: IAutocomlete = (term, selectedIds) => {
        return this.dataController
          .service2GET(term, selectedIds)
          .pipe(
            map((res) =>
              res.map((a) => ({
                viewValue: a.name,
                value: a.id
              }))
            )
          );
      };
    ...
    }

How can I refactor (following DRY rule) and make it as clean as possible? My main goal is to avoid using map in each function.

like image 245
Elmatsidis Paul Avatar asked Oct 15 '22 08:10

Elmatsidis Paul


2 Answers

Custom Operator

Consider creating a custom operator that houses the reusable logic.

custom-pipe.ts

import { pipe, of } from "rxjs";
import { startWith, switchMap } from "rxjs/operators";

export function myCustomOperator() {
  return pipe(
    map((res) =>
      res.map((a) => ({
        viewValue: a.name,
        value: a.id
      }))
    )
  );
}

Your component will become:

export class SuperCleanService {
  constructor(
    private dataController: DataController,
    private serviceController: ServiceController
  ) {}
.....
  private searchAs$: IAutocomlete = (term, selectedIds) => {
    return this.dataController
      .service1GET(term, selectedIds)
      .pipe(
        myCustomOperator()
      );
  };
  private searchB$: IAutocomlete = (term, selectedIds) => {
    return this.serviceController
      .service1(term, selectedIds)
      .pipe(
        myCustomOperator()
      );
  };
  private searchC$: IAutocomlete = (term, selectedIds) => {
    return this.dataController
      .service2GET(term, selectedIds)
      .pipe(
        myCustomOperator()
      );
  };
...
}
like image 184
C_Ogoo Avatar answered Oct 20 '22 14:10

C_Ogoo


In this case I would use generics with wrapper method which accepts autocomplete function and maps to desired result.

interface Autocomplete {
  id?: string;
  name?: string;
}

type IAutocomplete = (
  term: string,
  selectedIds?: string[]
) => Observable<TableFilterQueryResponse[]>;

type AutocompleteCallback<T> = (
  term: string,
  selectedIds?: string[]
) => Observable<T[]>;

...

  // use instance arrow function to preserve 'this' in callback method
  private autocompleteMapper = <T extends Autocomplete>(
    callback: AutocompleteCallback<T>,
    term: string,
    selectedIds: string[]
  ) => {
    return callback(term, selectedIds).pipe(
      map((res) =>
        res.map((a) => ({
          viewValue: a.name,
          value: a.id
        }))
      )
    );
  };

  private search1$: IAutocomplete = (term, selectedIds) => {
    return this.autocompleteMapper<ResponseInterface>(
      this.dataControllerService.service1GET,
      term,
      selectedIds
    );
  };

  ...

``
like image 26
Blazh Avatar answered Oct 20 '22 15:10

Blazh