DEV Community

Cover image for Design pattern | Criteria
Jesús Mejías Leiva for Product Hackers

Posted on • Originally published at blog.susomejias.dev

Design pattern | Criteria

We start this new series with the pattern called Criteria,
This pattern allows you to build search queries using a common interface,
which makes the code much more modular and flexible.


Uses cases without Criteria pattern

I have assembled the examples with Typescript but the implementation of said pattern is equally valid for other languages.

Let's see an example where the use of the Criteria pattern can help us.

First of all we are going to create a very simple type to define what is a Client for our domain:

interface Client {
  name: string;
  age: number;
  gender: 'M' | 'F',
  city: string
}
Enter fullscreen mode Exit fullscreen mode

then suppose we have to perform several filters to the following list of clients:

const clients: Client[] = [
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' },
  { name: 'John', age: 15, gender: 'M', city: 'London' },
  { name: 'Marta', age: 14, gender: 'F', city: 'Madrid' },
  { name: 'Luis', age: 30, gender: 'M', city: 'Barcelona' }
]
Enter fullscreen mode Exit fullscreen mode

Clients who are over 15 years old:

const clients: Client[] = [
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' },
  { name: 'John', age: 15, gender: 'M', city: 'London' },
  { name: 'Marta', age: 14, gender: 'F', city: 'Madrid' },
  { name: 'Luis', age: 30, gender: 'M', city: 'Barcelona' }
]

clients.filter(client => client.age > 15)

/*
[
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' },
  { name: 'Luis', age: 30, gender: 'M', city: 'Barcelona' }
];
*/
Enter fullscreen mode Exit fullscreen mode

Now we are going to complicate the query a bit,
imagine that we are asked to obtain the clients whose age is older than 20 and younger than 30 years old,
who are women and your city is Madrid or Barcelona

const clients: Client[] = [
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' },
  { name: 'John', age: 15, gender: 'M', city: 'London' },
  { name: 'Marta', age: 14, gender: 'F', city: 'Madrid' },
  { name: 'Luis', age: 30, gender: 'M', city: 'Barcelona' }
]

clients.filter(client =>
  (client.age > 20 && client.age < 30) &&
  client.gender === "F" &&
  (client.city === 'Madrid' || client.city === 'Barcelona')
)

/*
[
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' }
];
*/
Enter fullscreen mode Exit fullscreen mode

As we can see, things start to get complicated and it starts to be difficult to maintain and read,
In addition, at the semantic level there is a large margin for improvement,
for this, we will see how we can improve these aspects by applying the Criteria pattern.


Same complex use case with Criteria pattern

We start by creating the interface that will define our Criteria, we will use typescript generics for it
to gain flexibility:

interface Criteria<T> {
  meetCriteria(items: T[]): T[]
}
Enter fullscreen mode Exit fullscreen mode

We continue creating a Composite class that implements our Criteria interface, it will also allow us to
maintaining an array of criterias to apply and of course the implementation of the required meetCriteria method
upon implementation, the addCriteria method will allow us to add criteria that will function as AND:

class CompositeCriteria<T> implements Criteria<T> {
  private criteriaList: Criteria<T>[] = []

  addCriteria(criteria: Criteria<T>): void {
    this.criteriaList.push(criteria)
  }

  meetCriteria(items: T[]): T[] {
    let result = items

    for (const criteria of this.criteriaList) {
      result = criteria.meetCriteria(result)
    }

    return result
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, as I mentioned above, the addCriteria when adding criteria to a list would work implicitly
as an AND therefore we will need to make an implementation to be able to do OR operations:

class OrCriteria<T> implements Criteria<T> {
  constructor(private firstCriteria: Criteria<T>, private secondCriteria: Criteria<T>) {}

  meetCriteria(items: T[]): T[] {
    const firstResult = this.firstCriteria.meetCriteria(items)
    const secondResult = this.secondCriteria.meetCriteria(items)

    return Array.from(new Set([...firstResult, ...secondResult]))
  }
}
Enter fullscreen mode Exit fullscreen mode

How to use this new Criteria API

First of all we will create some queries which will help us in the construction of our filter:

const ageOlderThanTwentyYearsCriteria = {
  meetCriteria(items: Client[]): Client[] {
    return items.filter((client) => client.age > 20)
  }
};

const ageYoungerThanThirtyYearsCriteria = {
  meetCriteria(items: Client[]): Client[] {
    return items.filter((client) => client.age < 30)
  }
}

const madridCityCriteria = {
  meetCriteria(items: Client[]): Client[] {
    return items.filter((client) => client.city === 'Madrid')
  }
}

const barcelonaCityCriteria = {
  meetCriteria(items: Client[]): Client[] {
    return items.filter((client) => client.city === 'Barcelona')
  }
}

const femaleCriteria = {
  meetCriteria(items: Client[]): Client[] {
    return items.filter((client) => client.gender === 'F')
  }
}
Enter fullscreen mode Exit fullscreen mode

This is where the magic happens since we will be able to mount the filtering to our liking and with much greater semantics,
I am sure that if I give you this code you will know very quickly which filter is being built:

const clients: Client[] = [
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' },
  { name: 'John', age: 15, gender: 'M', city: 'London' },
  { name: 'Marta', age: 14, gender: 'F', city: 'Madrid' },
  { name: 'Luis', age: 30, gender: 'M', city: 'Barcelona' }
]

const compositeCriteria = new CompositeCriteria<Client>()

compositeCriteria.addCriteria(ageOlderThanTwentyYearsCriteria)

compositeCriteria.addCriteria(ageYoungerThanThirtyYearsCriteria)

compositeCriteria.addCriteria(
  new OrCriteria(
    madridCityCriteria,
    barcelonaCityCriteria
  )
)

compositeCriteria.addCriteria(femaleCriteria)

compositeCriteria.meetCriteria(clients)

/* Same result
[
  { name: 'Ana', age: 25, gender: 'F', city: 'Madrid' }
];
*/
Enter fullscreen mode Exit fullscreen mode

Benefits

  • Better maintainability of code.
  • Better readability.
  • Allows you to make complex filters more easily.
  • Move code to semantics of our domain.

Thanks for reading me 😊

Top comments (0)