Sign in
Log inSign up

JavaScript-fying declarative algorithms

Niklas Zantner's photo
Niklas Zantner
·Aug 6, 2019·

3 min read

When it comes to how to write good JS code a lot of developers coming from classical object-oriented languages find it having a hard time writing code as readable as they might be able within classic OOP environments. The fundamental problem seems to be, that JS as it is not statically typed, therefore does not provide any semantic information via its class/object definitions a developer can quickly peek via proper tooling.

Let's have a look at a short example

The following example transforms a given object to an API conform object. Both of the objects carry the semantic information necessary to understand how to modify data based on filtering, sorting and aggregations. How the exact data structure looks like is not important here.

convertViewTransformationsToAPIConformTransformations(transformations) {
  const transformations = [];

  if (dataManipulation.filterConfigurations)) {
    transformations.push(...filterConfiguration) 
  }

  if (dataManipulation.sortingConfigurations)) {
    transformations.push(...sortingConfiguration) 
  }

  if (dataManipulation.aggregationConfigurations) {
    transformations.push(...sortingConfiguration) 
  }

  return transformations;
}

Nice and easy isn't it? But wait, we forgot to check if the filterConfigurations, sortingConfigurations and aggregationConfigurations are arrays and not empty, let's fix this:

import { isArray, isEmpty } from 'lodash';

transformDataManipulationToAPIConformDataManipulation(dataManipulation) {
  const transformations = [];

  if (isArray(dataManipulation.filterConfigurations)
    && !isEmpty(dataManipulation.filterConfigurations)) {
    transformations.push(...dataManipulation.filterConfigurations) 
  }

  if (isArray(dataManipulation.sortingConfigurations)) 
    && !isEmpty(dataManipulation.sortingConfigurations)) {
    transformations.push(...dataManipulation.sortingConfigurations) 
  }

  if (isArray(dataManipulation.aggregationConfigurations)) 
    && !isEmpty(dataManipulation.aggregationConfigurations)) {
    transformations.push(...dataManipulation.aggregationConfigurations) 
  }

  return transformations;
}

Ok, still fine-ish, but it's getting less readable.

A server developer comes up and starts talking (Note to the reader):

Server-Dev: "Heyo JS-Hero! We forgot to specify how the order of transformations elements should look like. Please make sure that if we have aggregations, we start with the sorting, then the aggregation then the filters. Else at first filters, then sorting".
Client-Dev: "Okilidokili"

Note to the reader, why this order needs to exist is not important, we just need some more control flow to make our example more interesting.

So let's go ahead and implement the scope change (or maybe scope extension):

import { isArray, isEmpty } from 'lodash';

transformDataManipulationToAPIConformDataManipulation(dataManipulation) {
  const transformations = [];

  if (!isArray(dataManipulation.aggregationConfigurations) 
    || isEmpty(dataManipulation.aggregationConfigurations)) {
    // There is no aggregation configuration, therefore start with filters and the sorting

    if (isArray(dataManipulation.filterConfigurations)
      && !isEmpty(dataManipulation.filterConfigurations)) {
      transformations.push(...dataManipulation.filterConfigurations) 
    }

    if (isArray(dataManipulation.sortingConfigurations) 
      && !isEmpty(dataManipulation.sortingConfigurations)) {
      transformations.push(...dataManipulation.sortingConfigurations) 
    }
  } else {
    // There is a aggregation configuration, therefore start with sorting, then aggregtion and then filters

    if (isArray(dataManipulation.sortingConfigurations) 
      && !isEmpty(dataManipulation.sortingConfigurations)) {
      transformations.push(...dataManipulation.sortingConfigurations) 
    }

    if (isArray(dataManipulation.aggregationConfigurations) 
      && !isEmpty(dataManipulation.aggregationConfigurations)) {
      transformations.push(...dataManipulation.aggregationConfigurations) 
    }

    if (isArray(dataManipulation.filterConfigurations)
      && !isEmpty(dataManipulation.filterConfigurations)) {
      transformations.push(...dataManipulation.filterConfigurations) 
    }
  }

  return transformations;
}

Well. This sucks. A lot.

Let's have a look at another approach:

import { isArray, isEmpty } from 'lodash';

transformDataManipulationToAPIConformDataManipulation(dataManipulation) {
  const transformations = [];

  const isFilledArray = array => isArray(array) && !isEmpty(array);

  const hasAggregations = isFilledArray(dataManipulation.aggregationConfigurations);
  const transformAggregations = aggregations => { if (hasAggregations) transformations.push(aggregations) };

  const hasFilters = isFilledArray(dataManipulation.filterConfigurations);
  const transformFilters = filters => { if (hasFilters) transformations.push(filters) };

  const hasSortings = isFilledArray(dataManipulation.sortingConfigurations);
  const transformSortings = sortings => { if (hasSortings) transformations.push(sortings) };

  if (hasAggregations) {
    transformFilters(dataManipulation.filterConfigurations);
    transformSortings(dataManipulation.sortingConfigurations);
  } else {
    transformSortings(dataManipulation.sortingConfigurations);
    transformAggregations(dataManipulation.aggregationConfigurations);
    transformFilters(dataManipulation.filterConfigurations);
  }

  return transformations;
}

As you can see the readability improved by structuring the control flow in a less declarative manner, but a more functional-ish one. In conclusion, next to

  • an easier readability in general
  • it is also easier to review (your co-workers will thank you)
  • and easier to add new attributes to the configuration array (apart from filters, sortings, etc.)

I hope the following screenshot of the two solutions next to each other can further showcase the difference:

Screenshot from 2019-08-11 15-17-58.png

To avoid some questions like:

"Why did you inline the if, that's ugly!" - I generally agree with you, in most cases in-lining if statements hurts readability but in this case, I think it actually does the opposite. Maybe a personal preference.

"You could have abstracted the transformations.push statements further?!" Yep, I could, but that was not what this example was intended to show. Good catch, though!