DeepakBhalla

Refactoring Patterns - The Integration Operation Segregation Principal

By Deepak Bhalla Sun 22 April 2018 6 min read

A rarely mentioned refactoring pattern is called the Integration Operation Segregation Principal (IOSP), it allows us to systematically reduce the complexity of large methods into much simpler ones. It becomes particularly important in Functional Programming and is often a staple of refactoring a code base for testing. It's a pattern we sometimes don't realise we are using but once we see it formalised we realise it's everywhere.

The rules of the IOSP are simple:

  1. Classify your methods as either Integration Units or Operational Units

  2. Integration Units only compose other units (including other Integration Units) but do not contain any logic.

  3. Operational Units just contain logic and never integrate any other functional units.

  4. Operational Units should be small, composable and idempotent.

Think of an entry point to your application like a Tree Diagram where each node represents a method. In the IOSP world leaf nodes are operational units while non-leaf nodes are integration units. Take a look at the following diagram which shows a tree diagram representing an entry point to an application's execution over time (assuming a syncronous workflow).

The Integration Operation Segration Principal mapped as a tree diagram

This represents one entry point to an application composed of multiple integrations and operations. Each unit represents a single method

Notice that all operational units are leaf nodes. To allow for complex execution patterns we allow integration units to nest other integration units. To put it simply, Integration Units can compose together multiple Integration Units and multiple Operational Units while Operational Units must not compose anything. Without properly sitting down and designing functions in the IOSP fashion we will most likely find that we write a lot of hybrids.

I would encourage you to look at every method as a Tree Structure like the one above, if not mentally then jot it down somewhere. Try to avoid having leaf nodes which are integration units. Think of the tree structure as something you can re-organise to reduce complexity of testing and reading a code base.

IOSP by example

Let's refactor a relatively complex method into a simpler one based on what we know so far about IOSP (Warning: Java). This example attempts to get a remote data object, serialize it into a Plain old Java Object (POJO) and then save it to its own database and trigger a notification.

Before

/**
 * PersonController.java
 **/

public void getRemoteData() {
    // Get the Person object remotely
    WebClient webClient = WebClient.create("http://example.org")
    JsonObject json = webClient.get()
            .uri("/persons/{id}", id)
            .retrieve();

    // Save the person to our database
    Person person = null;
    if (json != null) {
        person = objectMapper(json, Person.class)
        PersonRespository.save(person);
    }

    // Notify an auditing/notification service we have saved the data
    if (person != null) {
        NotificationService.notifyPersonSaved(person);
    }    
}

This may seem like a simplistic example but it is an example of code I run into multiple times a day.

After

Let's see how we can refactor it using the IOSP. In the following code i've labelled each Integration and Operational Unit so we can understand the effect of IOSP.

/**
 * PersonController.java
 **/

// Integration Unit
public void getRemoteData(String URL, String personId) {
    WebClient webClient = getWebClient(URL);
    Optional<Person> person = getRemotePerson(webClient, personId);
    person.ifPresent(PersonController::saveAndNotify);
}

// Integration Unit
private Optional<Person> getRemotePerson(WebClient webClient, String personId) {
    JsonObject json = retrievePersonData(personId);
    return Person.fromJson(json);
}

// Integration Unit
private void saveAndNotify(Person person) {
    Person person = PersonRespository.save(person);
    PersonNotificationService.notifyPersonSaved(person);
}

// Operational Unit
public JsonObject retrievePersonData(String id) {
    return webClient.get()
        .uri("/persons/{id}", id)
        .retrieve();
}

// Operational Unit
private WebClient getWebClient(String URL) {
    return WebClient.create(URL).accept(MediaType.APPLICATION_JSON) // We only ever serialize JSON
}
/**
 * PersonNotificationService.java 
 **/

// Integration Unit
private void notifyPersonSaved(Person person) {
    // Send notification 1
    // Send notification 2
}
/**
 * Person.java
 **/

// Operational Unit
public Optional<Person> fromJSON(JsonObject json) {
    if (json != null) {
        try {
            return Optional.ofNullable(objectMapper(json, Person.class));
        } catch (JsonMappingException e) {
            // Allow to fail
        }
    }
    return Optional.empty();
}

Things to note:

  1. There is much more code in the refactor but the overall complexity of individual methods are much lower.

  2. There are far more Integration Units than Operational Units and all Integration Units are nested under the single entry point of the getRemoteData method.

  3. Operational Units rely on injected data and are idempotent (but not pure).

  4. Smaller functional units allow for declarative method naming which makes understanding the code much easier without additional comments.

Thinking Test Driven Development

Without properly sitting down and designing your functions in the IOSP fashion you will probably find that you write a lot of hybrids. As a result you will find the complexity of your tests increasing. When we start to think about how the IOSP affects testing we can immediately see that our refactored code is much easier to test.

  1. By encouraging small functional units we make testing simpler and pre-tested code likely to be used elsewhere. Moreover, when the code is reused it may have many callers, those callers may successfully exercise every conceivable context and set of inputs the code would ever be under.

  2. We don't need to use many test doubles 1 in our Operational Units which makes them easier to test. In fact, our Operational Units should need very little mocking at all (apart from mocking side effects like API calls).

  3. We increase mockability of our Operational Units by using argument dependencies from the previously composed functions.

Often times TDD will help us make sure our functions are nice and small and testable and go hand in hand with IOSP style workflows.

Thinking Functionally

We also gain a lot of functional best practices by forcing our Operational Units to be our leaf nodes:

  1. Encourage the use of small idempotent functions to avoid state manipulation

  2. Integration Units have little to no logic and are easy to extend, compose and wrap.

  3. We increase the maintainability of the code base

Closing

If you were to refactor your code base using the IOSP principal my suggesstion would be to pick and entry point to your application and start to refactor those methods by pushing down any logic into lower level functions. Then repeat the process for all functions integrated. Organizing your code in this way will help you increase readability and understandability while following the Single Responsibility principal.

More importantly, start to think of all your methods as as tree diagrams of multiple calls and try and simplify that diagram as much as possible. Make sure it's clean. That´s what IOSP does by introducing two fundamental domain independent responsibilities: Integration and Operation.

  1. Objects that can stand in for a real object in a test (typically spys or mocks)
Deepak Bhalla