Refactoring Commonality using the IWith Pattern
By Harry Bellamy
- 4 minutes read - 707 wordsWe’ve all been there. We’ve got two (or more) classes with that have a need for the same functionality that share no base class. The project manager is asking when this will all be done, so we just create a method in one class and copy it over to the other.
Then we find that another class has a need for the same functionality. So we copy it over to there as well. Before we know it, it’s all over every single repo.
public class NameGenerator
{
public string GenerateName(string firstName, string surname)
{
// argument validation omitted for brevity
// ...
return $"{firstName} {surname}";
}
}
The Helper Method
If you’re lucky, the helper method has been defined as static and the code isn’t copied everywhere. If you’re not, the method has been copied all over the codebase and into other repos too.
As this method takes a pair of strings as input, it’s hard to determine what the method expects as input.
Why not use a common base class?
This can be a possibility when using sufficiently similar classes. If classes already share a common base class it may make more sense to move the helper method into the base class instead.
If other, non-similar classes require this though then this approach may restrict reuse.
Inject another class to do the formatting
We could create a new class dedicated to the formatting:
public class NameFormatter : INameFormatter
{
public string GetFullName(string firstName, string surname)
=> $"{firstName} {surname}";
}
Very separation of concern, what’s not to like?
This approach will result in having to change the signature of all the constructors of affected classes to inject it. The discussion of the options here are around existing codebases, so a smaller set of changes may be desirable.
The ‘IWith’ Pattern
Disclaimer: this might have another name, but this is the name I’m using after seeing it in some Azure APIs.
To refactor the above example, introduce an interface with two properties on it:
public interface IWithFirstNameAndSurname
{
string FirstName { get; }
string Surname { get; }
}
Alternatively you could take the interface segregation principle to its logical extreme - a single property per interface (IWithFirstName
and IWithSurname
).
IWithFirstNameAndSurname
would then inherit both these interfaces.
Making your original two classes implement this interface allows them to share functionality in ways they previously couldn’t, with the minimum of changes.
Adding Extension Methods
Now there is commonality between the classes that share the properties (they share an interface) The helper method can now be extracted into an extension method as such:
public static class With<X>Extensions
{
public static string GetFullName(this IWith<X> withX)
=> $"{withX.FirstName} {withX.Surname};
}
Default interface implementation
If you’re using C# 8.0 or later, the code could be refactored into the IWith<X>
interface itself by adding a default interface implementation:
public interface IWithX
{
string FirstName { get; }
string Surname {set }
string FullName => $"{FirstName} {Surname}";
}
IMO this is cleaner than the extension methods approach, as it allows the use of properties where desired and doesn’t require a separate file for implementation.
This also allows for the default interface implementation to be overridden, allowing classes to have different functionality if desired.
But you’re strong coupling here! That’s bad!
Yes. And….maybe.
However, I believe this can be justified in certain situations.
Unit Testing
Yes, the top level class is strongly coupled to the extension method. However that logic has been extracted and can be tested in isolation. This allows for a comprehensive set of unit tests on the extension method, covering the necessary edge cases.
Code that calls the extension method will not necessarily need their own tests to cover the edge cases as they are already written. This allows for their tests to cover the class' core logic.
Remember that code should be constantly refactored to stay healthy. This approach could be used as a first step to introduce testability into previously untestable code. If desired, this could be refactored again to have clearer code seams.
TL;DR
Introduce interfaces where common properties occur and refactor towards these interfaces to allow code to be reused. Utilise extension methods, default interface implementations, additional classes or some combination of these techniques to consolidate duplicated code.