Parallel Change Refactoring

clean-code, pseudo-code, refactoring, software-craftsmanship

Parallel Change is the refactoring technique, that allows implementing backward-incompatible changes to an API in a safe manner. It consists of 3 steps:

  • Expand - add new functionality as an extension of the interface (e.g.: add new methods), instead of editing signatures of existing interfaces
  • Migrate - mark (or log a warning) existing interface as deprecated and give time to the clients of the interface to migrate to the new interface. This might be as simple as changing your own code-base one client of this interface at a time in case when you do not have 3rd party clients of these interfaces
  • Contract - once all clients have migrated remove old interfaces

This technique is tremendously useful when you have 3rd party clients for your API (open-source library, SaaS with REST API, etc). Also, it is as useful for normal application development because it allows such breaking changes to never break your test suite. It allows deploying your code in the middle of the refactoring, in fact, every few minutes (Continuous Integration + Continuous Deployment).

This article contains code examples in a pseudo-code.

UserSearch

Let’s imagine, that we have some class, that is used to search User by id:

1
2
3
4
5
class UserSearch {
  func search(id : UserId): [User] {
    // .. we somehow use the database gateway here to search ..
  }
}

Now, new requirement comes in and it seems, that we need to be able to search by e-mail too, so we have to add more functionality here:

1
2
3
4
5
6
7
class UserSearch {
  // ...

  func search_by_email(email : Email): [User] {
    // .. we somehow use the database gateway here to search ..
  }
}

Later, new requirement comes in and now we need to be able to search by the nickname too. So we follow the pattern:

1
2
3
4
5
6
7
class UserSearch {
  // ...

  func search_by_nickname(nickname : Nickname): [User] {
    // .. we somehow use the database gateway here to search ..
  }
}

This is great and all, but we clearly can see, how this class violates Open-Closed Principle: every time there is a new thing to search the user by, we will have to alter this class. This is not good. One of the possible solutions might be closing this class against this kind of change by introducing polymorphic Query:

1
2
3
4
5
6
7
class UserSearch {
  // all other search_* methods were removed

  func search(query : Query): [User] {
    // .. we somehow use the database gateway here to search
  }
}

After doing that, if we run our test suite, it will be failing, probably, even with compile errors. This is not good because now we have to go through every failure and fix it, this will prevent us from continuously integrating for quite some time (half an hour, or a couple of days, depending on the impact of this change). And this has high chances of resulting in merge conflicts, that will impede work of others on our team.

Instead, let’s apply the parallel change.

Applying Parallel Change

Expand

First, we need to introduce brand new method of our class (of course with unit-tests), without touching anything else:

1
2
3
4
5
6
7
class UserSearch {
  // ...

  func search_by_query(query : Query): [User] {
    // .. we somehow use the database gateway here to search
  }
}

At this point, we are going to deploy this new code.

Migrate

Second, we need to add a deprecation warning to old interface, and, in fact, old functions can be rewritten via new one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserSearch {
  func search(id : UserId): [User] {
    warn("`UserSearch.search` is deprecated. Use `UserSearch.search_by_query` instead")
    search_by_query(UserIdQuery(id))
  }

  func search_by_email(email : Email): [User] {
    warn("`UserSearch.search_by_email` is deprecated. Use `UserSearch.search_by_query` instead")
    search_by_query(UserEmailQuery(email))
  }

  func search_by_nickname(nickname : Nickname): [User] {
    warn("`UserSearch.search_by_nickname` is deprecated. Use `UserSearch.search_by_query` instead")
    search_by_query(UserNicknameQuery(nickname))
  }

  // ...
}

At this point, we are going to deploy this new code.

Next, we need to:

  • inform all clients of our system about this deprecation and give them time-frame to migrate, or
  • if all clients of UserSearch are under our control, we need to change all calls to use new interface:
1
2
// was: `user_search.search(user_id)`
user_search.search_by_query(UserIdQuery(user_id))

After every line of change like that (big system probably has a multitude of these) we are going to deploy.

1
2
// was: `user_search.search_by_email(user_email)`
user_search.search_by_query(UserEmailQuery(user_email))

After every line of change like that we are going to deploy.

1
2
// was: `user_search.search_by_nickname(user_nickname)`
user_search.search_by_query(UserNicknameQuery(user_nickname))

Of course, after each line we are going to deploy.

Contract

Once all our tests pass and there is no single deprecation warning from UserSession, or the time-frame we have given to our 3rd party clients is finished, we can remove old functionality by simply removing deprecated methods (and their unit-tests), and what we will have left is:

1
2
3
4
5
class UserSearch {
  func search_by_query(query : Query): [User] {
    // .. we somehow use the database gateway here to search
  }
}

Of course, we can deploy our system now.

Bottom Line

Notice, how following this technique avoids even a single compile error or test suite failure. And if failure happens, the last small code change (probably one line) was wrong, you just CTRL+Z it to get back to GREEN state.

Was that refactoring necessary? Oh yeah it was, because in next few weeks, there were new requirements that would have forced us to add 2 new search_by_* methods to UserSearch class - instead, we just created new derivatives of Query interface/protocol and used them in the places where they are needed. This way we were able to change how UserSearch class works without modifying its source code, by only adding new code. This is a great win.

You would not want to miss next articles on this tech blog, we still have a lot to talk about:

  • Continuous Integration and Continuous Delivery - importance of not impeding others,
  • Open-Closed Principle - changing behavior by adding new code,
  • Triangulation technique in Test-Driven Development - overlooking this technique might cause one fail at doing TDD,
  • Mutational Testing, “Build Your Own Testing Framework” series and so much more!

Stay tuned!

Thanks!

Thank you for reading, my dear reader. If you liked it, please share this article on social networks and follow me on twitter: @tdd_fellow.

If you have any questions or feedback for me, don’t hesitate to reach me out on Twitter: @tdd_fellow.