Achieving separation of concerns using higher order functions — Part II

In the first part of this series, we discussed what separation of concerns is, how it helps us to keep our code maintainable, and laid out an example of a web service that mixes multiple concerns in its implementation. We started to refactor this service in order to extract individual concerns into their own functions, and now, we’ll continue this effort by focusing on extracting the HTTP specific concerns.

Disclaimer: The utilities I implement on this article are included in the most popular (and not so much) web frameworks out there. The purpose of this article is not to reinvent the wheel but to explain the design principles behind the tools we use on a daily basis.

After implementing our logging higher order function, we simplified the getCourse service’s implementation. Let’s do the same with the test spec by removing the logger dependency here too.

import { COURSE_NOT_FOUND, PREMIUM_RESTRICTED } from 'src/common/messages';
import getCourseFactory from 'src/api/course/getCourse';

describe('getCourse', () => {
  let userService;
  let courseService;
  const courseId = '1';
  // let logger -- we no longer need a logger variable

  beforeEach(() => {
    userService = { currentUser: jest.fn(() => ({ premium: false })) };
    courseService = { findById: jest.fn(() => ({ premium: false })) };

    // We don't need to instantiate a fake logger object
    // logger = { info: jest.fn() }
  });

  describe('given the course does not exist', () => {

    it('should return a 404 response indicating the course does not exist', () => {
        const sut = getCourseFactory(courseService, userService);
        const response = {
          status: jest.fn(() => response),
          json: jest.fn(),
        };
        const request = {
          params: jest.fn(() => courseId),
        };
        courseService.findById.mockReturnValueOnce(null);

        sut(request, response);

        expect(courseService.findById).toHaveBeenCalledWith(courseId);
        expect(response.status).toHaveBeenCalledWith(400);
        expect(response.json).toHaveBeenCalledWith({ message: COURSE_NOT_FOUND });
    });
  });

  describe('given the course is premium and the user is not premium', () => {
    it('should return a 401 response indicating the user is not authorized to access this resource', () => {
        // implement test setup and assertions
    });
  });

  // rest of the test cases to complete the coverage of this function
});

The number of dependencies of a service is usually a clear indication of its complexity. As this number grows, using the service becomes complicated because its clients should deal with all its dependencies. A test spec is a clear proof of this because it has to instantiate the system under test which makes very clear how complex that operation could be.

Now let’s jump into the 2nd concern, HTTP. All our web services should describe what they want to respond based on the client’s request. Sometimes these responses will be quite simple, i.e. A status code and a JSON payload:

response.status(401).json({ message: PREMIUM_RESTRICTED });

In other occasions, they’ll need special headers or a different content type:

// A hypothetical response to the action of creating a new course

response.status(200)
  .append('Cache-Control', 'max-age=3600')
  .append('Content-Type', 'text/html')
  .append('Content-Language', 'es')
  .append('Content-Length', viewContent.length)
  .send(viewContent);

As we call more methods of the response API, our service function becomes more coupled to our web framework of choice. If our code is tightly coupled to a framework, it becomes difficult to replace in an egregious situation (losing official support, encountering design flaws, etc.). Coupling is the opposite of modularity and thus separation of concerns.

To keep these two apart, we should separate the response’s specification from the task of building such object. An approach to achieve this goal is by describing the web service’s response using a plain JavaScript object:

import { COURSE_NOT_FOUND, PREMIUM_RESTRICTED } from 'src/common/messages';

function getCourseServiceFactory(courseService, userService) {
  return function getCourse(request) {
    const course = courseService.findById(request.params('id'));
    const currentUser = userService.getCurrentUser();

    if (!course) {
      return {
        status: 404,
        type 'json',
        body: { message: COURSE_NOT_FOUND },
      };
    }

    if (course.premium && !currentUser.premium) {
      return {
        status: 401,
        type: 'json',
        body: { message: PREMIUM_RESTRICTED }
      };
    }

    return {
      status: 401,
      type: 'json',
      body: { message: PREMIUM_RESTRICTED }
    };
  };
}

On this version of getCourse, the service doesn’t have knowledge of the response API provided by the web framework. It relies on the assumption that something else will take care of building a proper HTTP response based on the specification described by the object returned. What is that something else then? It will be a higher order function that knows how to interpret the specification object and act accordingly. This is a oversimplified version of what this function could be:

// withLogging implementation
function withResponseBuilder(fn) {
  return (request, response) => {
    const result = fn(request, response);

    response.status(result.status)
      .append('Content-Type', getContentTypeFromSpec(result.type))
      .send(encodeResponseBody(result.body));

    return result;
  }
}

const enhancedGetCourse = withResponseBuilder(
  withLogging(
    getCourse,
    'info',
    (request) => `Trying to access course with id ${request.params('id')}`,
  ),
);

From a large scale point of view, our response specification object is an example of a domain-specific language and the withResponseBuilder function works as an interpreter of the former. A DSL interpreter is a complex piece of software. A response specification language can include many constructs such as special headers, custom body renderers, etc. Our newest higher order function is just a façade of a more intricate mechanism. For more examples of defining behavior as data, you can check the node-machine project and more specifically, sails' machine-as-action.

Let’s update our test specification file to remove the response API dependency.

// getCourse.spec.js

// code hidden to save space

it('should return a 404 response indicating the course does not exist', () => {
    const sut = getCourseFactory(courseService, userService);
    const request = {
      params: jest.fn(() => courseId),
    };
    courseService.findById.mockReturnValueOnce(null);

    const response = sut(request);

    expect(courseService.findById).toHaveBeenCalledWith(courseId);
    expect(response).toEqual({
        status: 400,
        body: { message: COURSE_NOT_FOUND }
    });
});

The work needed to test getCourse’s response went from mocking a response object and asserting method calls over that mock to just checking if the object returned satisfies our expectations. As we implement more services in our platform, this simplification will save us a lot of time.

In the 3rd and last part of this series, we’ll extract our third concern: authorization and explore how higher order function can interact with each other.