4
\$\begingroup\$

This is a simple implementation of an ng2 filter pipe, which can currently take 2 optional facets: <string>status and <string>description. A "filter pipe" is an angular2 view decorator.

The filter receives a list of objects (values) which I want to filter by some criteria specific to the type of data object passed to it. The types of criteria are known in advance for each type of object.

Currently, I have implemented only one object type, a "maintenance event", which can be filtered by:

  • a status field, which is a fixed enum of strings, and
  • a free-text description

The filter is implemented in the view like so:

<div *ngFor="let event of maintenanceEvents | filter:'MAINTENANCE_EVENT':statusFilter:wordFilter">

where statusFilter and wordFilter are populated by a form.

The rest parameter (...filters) was used to allow any future arbitrary number of filters to be passed to the common filter module. So, in future, I could implement a filter like this:

<div *ngFor="let person of people | filter:'PEOPLE':title:permissions:locations:reports">

This is my complete pipe module:

import { Pipe, PipeTransform } from "@angular/core";

@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {

    transform(values: any, type: string, ...filters: Object[]): any {

        switch (type) {
            case 'MAINTENANCE_EVENT':

                const ALL = "Show all";
                const BLANK = "";
                const DESCRIPTION_FILTER = new RegExp(<string>filters[1], 'gi');

                if (filters.length !== 2) {
                    throw new SyntaxError('MAINTENANCE_EVENT requires exactly 2 filter parameters: <string>status and <string>description')
                }

                if (filters[0] === ALL && filters[1] === BLANK) {
                    return values;
                }
                else {
                    return values.filter(value => {
                        if (value.hasOwnProperty('status') && value.hasOwnProperty('part') && value.part.hasOwnProperty('description')) {
                            if (filters[0] === ALL && filters[1] !== BLANK) {
                                return DESCRIPTION_FILTER.test(value['part']['description']);
                            }
                            else if (filters[0] !== ALL && filters[1] === BLANK) {
                                return value.status === filters[0];
                            }
                            else if (filters[0] !== ALL && filters[1] !== BLANK) {
                                return value.status === filters[0] && DESCRIPTION_FILTER.test(value['part']['description']);
                            }
                        }
                        else {
                            console.error("Attempting to filter by properties not present in available <MaintenanceEvent>s");
                        }
                    });
                }

            default:
                // Return everything
                return values;
        }
    }
}

Particularly, I'm interested in an alternative to the if, else if, else if ... else if N chain, since this will become wholly unmaintainable for filters with more complex facets (we plan for as many as 9 in future).

\$\endgroup\$
2
\$\begingroup\$

Direction

My approach would be based on the Strategy design pattern. Each of the original if-elseif-elseif-... sections is now becoming an IRule object that represents the rule. This object does two things:

A) It defines whether it may or may not be applied to the given filter input;

B) It defines the application function (logic) itself.

In order to select a proper rule dynamically in run time, we need a registry of the rules which is implemented as a simple array.

Benefits

Implementation of a new "rule" becomes almost trivial: we'll need to create a rule object itself, and add it to a proper place in the list of registry entries, so that it's picked up.

Code

import { Pipe, PipeTransform } from "@angular/core";

const ALL = "Show all";
const BLANK = "";

/**
 * Represents a contract of a single rule.
 */
interface IRule {
    /**
     * A predicate that sayw whether the rule may or may not be applied to a value based on the filter.
     */
    isApplicableTo(filters: Object[]): boolean;

    /**
     * Implementation of rule applied to a value based on filter parameters.
     */
    applyTo(filters: Object[], value: any): boolean;
}

/**
 * The complete list of rules that your system supports.
 * NOTICE that the order of the rule is important.
 * If more than one rule is applicable to a filters object,
 * the very first applicable rule is applied.
 */
const RULES_REGISTRY: IRule[] = [
    new AllMatchingRegexRule(),
    new MatchingStatusRule(),
    new SomeMatchingRegexRule()
];

/**
 * Three sample rules based on your original code.
 */

class AllMatchingRegexRule implements IRule {
    isApplicableTo(filters: Object[]): boolean {
        const safeFilters = getSafeFilters(filters);
        return isAllFilter(safeFilters) && isNotBlankFilter(safeFilters);
    }

    applyTo(filters: Object[], value: any): boolean {
        return descriptionRegexMatched(filters[1], value);
    }
}

class MatchingStatusRule implements IRule {
    isApplicableTo(filters: Object[]): boolean {
        const safeFilters = getSafeFilters(filters);
        return isNotAllFilter(safeFilters) && isBlankFilter(safeFilters);
    }

    applyTo(filters: Object[], value: any): boolean {
        return statusMatches(filters, value);
    }
}

class SomeMatchingRegexRule implements IRule {
    isApplicableTo(filters: Object[]): boolean {
        const safeFilters = getSafeFilters(filters);
        return isNotAllFilter(safeFilters) && isNotBlankFilter(safeFilters);
    }

    applyTo(filters: Object[], value: any): boolean {
        return value.status === filters[0] && descriptionRegexMatched(filters[1], value);;
    }
}

// Reusable working horse lambdas (use in Rules)

const getSafeFilters = (filters: Object[]) => filters && filters.length >= 2 ? filters : new Object[2];

const descriptionRegexMatched = (pattern: any, value: any) => new RegExp(pattern, 'gi').test(value['part']['description']);

const statusMatches = (filters: Object[], value: any) => value.status === filters[0];

const isAllFilter = (filters: Object[]) => filters[0] === ALL;
const isNotAllFilter = (filters: Object[]) => !isAllFilter(filters);

const isBlankFilter = (filters: Object[]) => filters[1] === BLANK;
const isNotBlankFilter = (filters: Object[]) => !isBlankFilter(filters);

Consuming code (that uses the Rule registry)

Now your filter can be implemented like this:

@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {

    transform(values: any, type: string, ...filters: Object[]): any {
        switch (type) {
            case 'MAINTENANCE_EVENT':
                if (filters.length !== 2) {
                    throw new SyntaxError('MAINTENANCE_EVENT requires exactly 2 filter parameters: <string>status and <string>description')
                }

                if (isAllFilter(filters) && isNotBlankFilter(filters)) {
                    return values;
                } else {
                    return values.filter(value => {
                        const ruleToApply = RULES_REGISTRY.find(rule => rule.isApplicableTo(filters));

                        if (!ruleToApply) {
                            throw new Error("Attempting to filter by properties not present in available <MaintenanceEvent>s");
                        }

                        return ruleToApply.applyTo(filters, value);
                    });
                }

            default:
                return values;
        }
    }
}

Disclaimer

  • I am very new to TypeScript so I may be using wrong idioms. The key in my answer is the design change, rather than implementation details.

P.S.

Notice that the same approach may be applied to type == 'MAINTENANCE_EVENT' 'OTHER_EVENT' .... And, speaking more generally, Strategy design pattern is almost always a working alternative to an if or switch. Of course, to get the benefits of the pattern, we need to write some boilerplate code (IRule + Rule registry), however it's a great trade of for the scenarios where there are a lot of rules, or the rules are frequently added/modified, or the rule selection strategy may change in run time.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.