DEV Community

Cover image for How to Refactor Duplicate Methods with Subtle Differences
Anthony Fung
Anthony Fung

Posted on • Originally published at webdeveloperdiary.substack.com

How to Refactor Duplicate Methods with Subtle Differences

In last week’s article, we explored a real-world example of how programming to an interface can make life easier when we need to make changes to an existing codebase. Requirements changes can come about unexpectedly. Writing code against abstractions of our components means we’re free to change specific implementations with others where required.

This week, we’ll see an example of how this concept can be used to make our code more reusable.

A Tale of Two Apps

Let’s imagine we inherit a project to work on. Its purpose is to read in data from user-specified sources, transform it, and relay the mapped data to another system. It consists of two complementary subsystems:

  • A Windows desktop app, complete with a UI that allows you to experiment with various data conversion options.

  • A command line ‘player’ that runs preconfigured transformations created using the desktop app.

The codebase consists of two projects in a Visual Studio solution, one corresponding to each of the previously mentioned systems. The mapping logic in the desktop app contains statements to write logging messages to the UI’s log window. Conceptually, it looks something like the following:

public void ReadAndTransformData()
{
    LogWindow.Clear();
    LogWindow.Text += "Reading in data...\r\n";
    var data = ReadData();
    LogWindow.Text += "Finished reading data. " +
        "Starting transformation...\r\n";
    TransformData(data);
    LogWindow.Text += "Data transform complete!";
}
Enter fullscreen mode Exit fullscreen mode

The core logic is duplicated in the command line version, but with a few modifications. For example, as there’s no dedicated logging window in console apps, LogWindow.Text is replaced by Console.WriteLine.

public void ReadAndTransformData()
{
    Console.Clear();
    Console.WriteLine("Reading in data...");
    var data = ReadData();
    Console.WriteLine("Finished reading data. " +
        "Starting transformation...");
    TransformData(data);
    Console.WriteLine("Data transform complete!");
}
Enter fullscreen mode Exit fullscreen mode

There isn’t too much logic in place to begin with, so having two nearly identical copies of the code is manageable. But more features are added over time. Both the size and complexity of the projects grow to a point where they’re difficult to maintain. Refactoring the projects to share code would result in a single codebase, and would have two benefits:

  • Changes in one app would carry through to the other, meaning less development time.

  • Both apps would share the same logic, meaning testing would be easier.

But a desktop app can’t log to the console, and a console app doesn’t have a logging window: how could we bridge the (subtle but important) differences?

Looking at the Bigger Picture

Let’s take a step back for a moment and consider what we’re doing. We have a task of reading in data and transforming it. Because it can take a while, we want to give feedback at key moments. In the context of running this process, it’s unimportant whether we have a logging window or if we need to write to the console: we want to let the user know what’s happening, but we shouldn’t concern ourselves (at this level) with how we do it. To realise this this abstraction, we can write an interface for a logger that we can use instead of the specifics we previously had in place.

public interface ILog
{
    public void Clear();
    public void Log(string message);
}
Enter fullscreen mode Exit fullscreen mode

We can then refactor the core logic. For this to be available in both apps, we can move it into a shared library referenced by both projects.

public void ReadAndTransformData(ILog logger)
{
    logger.Clear();
    logger.Log("Reading in data...");
    var data = ReadData();
    logger.Log("Finished reading data. Starting transformation...");
    TransformData(data);
    logger.Log("Data transform complete!");
}
Enter fullscreen mode Exit fullscreen mode

Now that our refactored process is logging to an interface, we can write classes to connect output messages to their appropriate destinations. For the desktop app, we might write something like the following:

public class LogWindowLogger : ILog
{
    private readonly TextBox _logWindow;

    public LogWindowLogger(TextBox logWindow)
    {
        _logWindow = logWindow;
    }

    public void Clear()
    {
        _logWindow.Clear();
    }

    public void Log(string message)
    {
        _logWindow.Text += $"{message}\r\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

And the following for the console app:

public class ConsoleLogger : ILog
{
    public void Clear()
    {
        Console.Clear();
    }

    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

If you have repeated copies of similar functions but with subtle differences, identifying abstractions can help to refactor them into common shared code modules. When you focus on what you want your code to do, rather than how, you’ll be able to find common themes.

The specifics of how will be important. But they’re usually smaller in scope and can be provided in separate modules specifically built for their targeted environments. And when combined, you’ll have a cleaner codebase that’s faster to develop, easier to test, but still just as powerful.


Thanks for reading!

This article is from my newsletter. If you found it useful, please consider subscribing. You’ll get more articles like this delivered straight to your inbox (once per week), plus bonus developer tips too!

Top comments (0)