when to pass args to the constructor of a service in ts?

26 Views Asked by At

I am struggling to understand when to pass args to the constructor of a service, and when to pass them in the execution of the service.

As an example, I have a service that filters some excel rows:

@Injectable()
export class FilterRowsService {
  private rows: AvailablesLogWithDate[];

  constructor(rows: AvailablesLogWithDate[]) {
    this.rows = rows;
  }

  execute({
    descriptions,
    products,
    warehouse,
    dates,
  }: {
    dates?: Dates;
    descriptions?: string[];
    products?: string[];
    warehouse?: string;
  } = {}) {
    this.filterByDates({ dates });
    this.filterByProducts({ products });
    this.filterByDescriptions({ descriptions });
    this.filterByWarehouse({ warehouse });

    return this.rows;
  }

  private filterByWarehouse({ warehouse }: { warehouse: string }) {
    if (!warehouse) {
      return this.rows;
    }

    this.rows = this.rows.filter((row) => {
      const warehouseInfo = warehousesInfos[warehouse];
      const warehouseKeywords = warehouseInfo.availableKeyWords;

      return warehouseKeywords.includes(row.Location);
    });
  }

I have multiple filtering methods, that changes the state of rows. I decided to pass the rows to the constructor and then create a class var, so that I don't need to pass down the rows every single time in the filtering methods. But it doesn't feel right to be changing the state of the rows. But maybe it's all right, since it only happens within the class.

or the second approach would be to not have this class var, and pass the rows to the execute method and do something like this:

@Injectable()
export class FilterRowsService {
  execute({
    rows,
    descriptions,
    products,
    warehouse,
    dates,
  }: {
    rows: AvailablesLogWithDate[];
    dates?: Dates;
    descriptions?: string[];
    products?: string[];
    warehouse?: string;
  }) {
    let filteredRows = rows;
    filteredRows = this.filterByWarehouse({ warehouse, rows });
    filteredRows = this.filterByDates({ dates, rows });
    flteredRows = this.filterByProducts({ products, rows });
    filteredRows = this.filterByDescriptions({ descriptions, rows });

    return filteredRows;
  }

  private filterByWarehouse({
    warehouse,
    rows,
  }: {
    warehouse: string;
    rows: AvailablesLogWithDate[];
  }) {
    if (!warehouse) {
      return rows;
    }

    return rows.filter((row) => {
      const warehouseInfo = warehousesInfos[warehouse];
      const warehouseKeywords = warehouseInfo.availableKeyWords;

      return warehouseKeywords.includes(row.Location);
    });
  }

so basically every filtering method returns a filteredRows new var, and in the execute body, there's a filteredRows that changes after every filtering method, and initial rows are never mutated.

Second approach seems more scalable and correct, but I'm wondering if first approach makes sense and if not, why not.

1

There are 1 best solutions below

0
plalx On BEST ANSWER

Not sure what's the relation with DDD, but anyway...

In general you would pass arguments in the constructor when the dependencies have the same lifecycle of the object being created, and method arguments when the dependencies are only needed for that specific use case. Further guidance also states services should be stateless. This makes services easier to reason about, reusable and thread-safe.

Thread safety is not really an argument in JavaScript, but it certainly feels odd for the client code to have to instantiate a new service every time you want to filter rows. Furthermore, services in Angular are singleton in their injection context which makes the constructor(rows) version even less practical.

I understand you want to avoid having to pass around rows and there's many options for that, while preserving a stateless service.

  1. Create a new class to represent the filter process. This is basically your initial idea, but hidden as an implementation details which allows the service class to remain stateless.
class FilterRowsService {
    filter(rows, ...) {
        return new FilterProcess(rows, ...).execute();
    }
}
  1. Use closures to wrap around rows. May not be the best given functions have to be re-created, but the performance impact would probably be unnoticeable in your case.
class FilterRowService {
    filter(rows, ...) {
        filterByWarehouse();
        filterByDates();
        
        return rows;

        function filterByWarehouse() {
            rows = rows.filter(...)
        }

        function filterByDates() {
            rows = rows.filter(...)
        }
    }
}
  1. Compose filters in a single one or chain them. By focusing on the predicates only we don't have to pass the rows around at all. Furthermore, we also avoid looping multiple times over rows. We build a single predicate composed of all the conditions. The warehouseFilter and datesFilter functions would be factories for (row) => boolean predicates.
class FilterRowsService {
    filter(rows, ...) {
        const compositePredicate = [warehouseFilter(warehouse), datesFilter(dates), ...]
            .reduce((a, b) => row => a(row) && b(row));
 
        return rows.filter(compositePredicate);
    }
}

Here's a working sample of this idea:

class SomeFilterService {
    filter(items, text, color) {
        return items.filter(this._composePredicates(
            this._textIncludes(text),
            this._colorIs(color)
        ));
    }
    
    _composePredicates(...predicates) {
        return predicates.reduce((a, b) => item => a(item) && b(item));
    }

    _textIncludes(text) {
        return item => item.text.includes(text);
    }
    
    _colorIs(color) {
        return item => item.color === color;
    }
}

const items = new SomeFilterService().filter([
  { text: 'hello', color: 'blue' }, 
  { text: 'test', color: 'blue' }, { text: 'test', color: 'red' 
 }], 'test', 'blue');

console.log(items);

Note that if the only thing the service is doing is composing predicates then perhaps you should use the Builder pattern instead to compose the filter.

Hopefully this answer provided you with enough guidance & options to make the correct design decisions to solve your problem.