mobile menu icon

"Move seam to delegate" refactoring

Published by Manuel Rivero on 09/05/2024

Legacy Code, Refactoring


We’d like to document a refactoring that we usually teach to teams we work with: the Move seam to delegate refactoring.

Move seam to delegate.

Motivation.

Sometimes we want to move some code to a delegate but that code contains a protected method we’re overriding in our tests in order to sense or separate[1]. That method is an object seam.

The tests (the violet eye) are coupled to the seam.
The tests (the violet eye) are coupled to the seam.

Our current tests are anchoring that method because if we move it, all the tests will fail.

Let’s see an example.

Have a look at the BirthdayGreetingsService class:

We’d like to move the logic that sends greetings by email to a different class in order to decouple the infrastructure logic from the domain logic.

The problem is that we can’t move the protected sendMessage method because, if we do it all the tests would fail.

This method was introduced by applying the Extract & Override Call[2] dependency-breaking technique in order to test BirthdayGreetingsService, and it’s being overridden in our tests.

That seam allowed us to spy which emails were going to be sent, and to avoid the undesired side-effect of actually sending emails every time we run the tests.

If we want our tests to allow us moving the logic that sends the email to a different class, we’ll first need to move the seam to that class. Let’s see how we can perform this Move seam to delegate refactoring safely.

Mechanics.

We’ll do a parallel change of the tests in order to move the seam to the delegate where we’d eventually like to move the code. These are the steps to do it:

1. Expand.

a. Create the delegate class if it doesn’t exist yet, and initialise a field with its type in the constructor of the class being tested. Use Parameterize Constructor[3] to inject the delegate in the class.

b. Copy the overridden method in the delegate and make it public[4].

c. Subclass & Override[5] the delegate class in the tests, and do in its overridden method exactly what the class under test overridden method was doing (stubbing or spying).

d. Inject an instance of the delegate’s subclass in the class under test.

Let’s see how the code of our example looks at the end of the expand phase:

a. This is the code of the delegate we created, EmailGreetingsSender, where we copied the sendMessage method:

b. This is the change we did in BirthdayGreetingsService, by introducing an EmailGreetingsSender field, initialising it in the constructor and applying Parameterize Constructor in this case using the Introduce Parameter refactoring which was automated by the IDE we were using[6]:

c. Finally this is the change in the setUp method of the tests of BirthdayGreetingsService. Notice how we are applying Subclass & Override to EmailGreetingsSender’s sendMessage method, and how both seams do exactly the same:

2. Migrate.

Change the production code so that it calls the public method in the delegate instead of the originally overridden protected method. The tests should keep passing.

In our example, the migration phase involves just changing the place in BirthdayGreetingsService where the protected sendMessage method is called, So that it calls the sendMessage method in EmailGreetingsSender:

sendMessage(msg); changes to greetingsSender.sendMessage(msg);

All the tests should keep passing after doing that change.

3. Contract.

In this phase we delete all the resulting dead code.

First, we delete the dead code in the tests: the method that was overriding the seam, and, if possible, also the subclass of the class under test used for testing purposes. After doing these the tests should still pass.

Finally, we also remove the dead code in production: the overridden protected method that originally provided the seam.

In our example, we first delete the anonymous class that was overriding BirthdayGreetingsService for testing purposes, which it’s not needed anymore.

And then, delete the protected sendMessage method in BirthdayGreetingsService.

After applying this parallel change to the tests, the seam is now located in the delegate, and we can freely move the code that uses the seam.

Now the tests (the violet eye) are coupled to the seam in the delegate and we can freely move there the logic that sends greetings by email.
Now the tests (the violet eye) are coupled to the seam in the delegate and we can freely move there the logic that sends greetings by email.

Conclusion.

We have described the Move seam to delegate refactoring mechanics. We hope this refactoring technique might be useful to you when working with seams in legacy code.

Acknowledgements

I’d like to thank Fran Reyes for revising and giving feedback about this blog post.

Finally, I’d also like to thank Engin Akyurt for the photo.

Notes

[1] Generally, when we break dependencies to get tests in place, we do it for either sensing or separating:

The seam in the example we use throughout the post is used to sense the emails sent by the code.

[2] Michael Feathers describes the Extract & Override Call dependency-breaking technique in chapter 25 of his book, Working Effectively with Legacy Code. It’s a variation of the Subclass & Override dependency-breaking technique[5].

[3] Parameterize Constructor is another dependency-breaking technique described in Working Effectively with Legacy Code, in which you safely add a new parameter to a constructor in order to introduce an object seam.

[4] If the protected method has some dependencies on other fields or methods of the class, you’d need to do more work. However, these dependencies on the class are likely a sign that the extraction to create the seam was too coarse-grained. A finer-grained seam with just the code that calls the awkward dependency and that receives any required class data as parameters would be much safer.

[5] Subclass & Override is another dependency-breaking technique described in Working Effectively with Legacy Code, in which we use inheritance to override the behaviour of a method (our seam) in order to sense or separate, without affecting the behaviour we’d like to test. In the example we use throughout the post, we used it to sense the emails sent by the delegate.

[6] You can find an explanation of how to apply this automated refactoring with IntelliJ Idea in this video.

Volver a posts