Extract method is about finding small pieces of logic and breaking that into its own method. In object oriented design we strive to follow the single responsibility principle, and this also applies to methods—a method should have only one reason to change, or a method should only do one thing and do it well.
By extracting logic into small methods you will be in a better position to reuse those methods and achieve code reuse. Also, you will have a single point to implement change should the logic or functionality need to be updated.
Simple Example
Here is a simple method that prints out some details about an invoice:
private void PrintOwing(Invoice anInvoice)
{
PrintBanner();
double outstandingBalance = CalculateOutstandingBalance(anInvoice);
// print details
Console.WriteLine("Name: {0}", anInvoice.Customer);
Console.WriteLine("Amount: {1}", outstandingBalance);
}
This method is fairly small as it is, but how many reasons does it have to change? If the format of the output changes this would change the method. If the order of the output changes it would change the method. This method has multiple reasons to change.
Let’s start by taking the portion of the code that prints output (lines 6-7) and move them to their own method. This may require moving variables or passing them to the new method. Make sure the new method has a good name, and make sure you call the new method:
private void PrintOwing(Invoice anInvoice)
{
PrintBanner();
PrintDetails();
}
private void PrintDetails(Invoice anInvoice)
{
double outstandingBalance = CalculateOutstandingBalance(anInvoice);
Console.WriteLine("Name: {0}", anInvoice.Customer);
Console.WriteLine("Amount: {1}", outstandingBalance);
}
Real Example
You will undoubtedly find methods that do too many things. Let’s look at one small portion of a very long method:
private IEnumerable<UCNewsFeedModel> GetNewsFeed()
{
// ommited
if (publicationDate != "")
{
var timeZones = new Dictionary<string, string>
{
{"EDT", "-04:00"},
{"EST", "-05:00"},
{"CST", "-06:00"},
{"MST", "-07:00"},
{"PST", "-08:00"}
};
try
{
var modifiedInputDate = publicationDate.Substring(0,
publicationDate.LastIndexOf(" ", StringComparison.InvariantCultureIgnoreCase));
var timeZoneIdentifier =
publicationDate.Substring(
publicationDate.LastIndexOf(" ", StringComparison.InvariantCultureIgnoreCase) + 1);
string timeZoneOffset;
try
{
timeZoneOffset = timeZones[timeZoneIdentifier];
}
catch
{
timeZoneOffset = "-05:00";
}
var dateForParsing = modifiedInputDate + " " + timeZoneOffset;
var dt = DateTime.ParseExact(dateForParsing, "ddd, dd MMM yyyy HH:mm:ss zzz",
new CultureInfo("en-US"));
publicationDate = dt.ToShortDateString();
}
catch
{
publicationDate = "";
}
}
// omitted
}
How many things is this portion of the method doing? It’s a lot. We can see that there is a portion that takes a time zone like “EST” and translates it to a time zone offset like “-0:500”. Let’s ignore the fact that there are logic issues (we assume that only certain time zones are used).
The first thing I look at is the logic that converts a time zone offset to a time zone. Let’s move just that logic to a new method:
private string GetTimeZoneOffsetFromIdentifier(string name)
{
if (string.IsNullOrEmpty(name)) return "-0:500";
if (name.Equals("EST", StringComparison.OrdinalIgnoreCase)) return "-0:500";
if (name.Equals("EDT", StringComparison.OrdinalIgnoreCase)) return "-0:400";
if (name.Equals("CST", StringComparison.OrdinalIgnoreCase)) return "-0:600";
if (name.Equals("MST", StringComparison.OrdinalIgnoreCase)) return "-0:700";
if (name.Equals("PST", StringComparison.OrdinalIgnoreCase)) return "-0:800";
return "-0:500";
}
Let’s call this new method and then remove the dictionary that is no longer needed. This new method also allows us to get rid of the try/catch
block as well:
private IEnumerable<UCNewsFeedModel> GetNewsFeed()
{
// ommited
if (publicationDate != "")
{
try
{
var modifiedInputDate = publicationDate.Substring(0, publicationDate.LastIndexOf(" ", StringComparison.InvariantCultureIgnoreCase));
var timeZoneIdentifier = publicationDate.Substring(publicationDate.LastIndexOf(" ", StringComparison.InvariantCultureIgnoreCase) + 1);
string timeZoneOffset = GetTimeZoneOffsetFromIdentifier(timeZoneIdentifier);
var dateForParsing = modifiedInputDate + " " + timeZoneOffset;
var dt = DateTime.ParseExact(dateForParsing, "ddd, dd MMM yyyy HH:mm:ss zzz",
new CultureInfo("en-US"));
publicationDate = dt.ToShortDateString();
}
catch
{
publicationDate = "";
}
}
// omitted
}
private string GetTimeZoneOffsetFromIdentifier(string name)
{
if (string.IsNullOrEmpty(name)) return "-0:500";
if (name.Equals("EST", StringComparison.OrdinalIgnoreCase)) return "-0:500";
if (name.Equals("EDT", StringComparison.OrdinalIgnoreCase)) return "-0:400";
if (name.Equals("CST", StringComparison.OrdinalIgnoreCase)) return "-0:600";
if (name.Equals("MST", StringComparison.OrdinalIgnoreCase)) return "-0:700";
if (name.Equals("PST", StringComparison.OrdinalIgnoreCase)) return "-0:800";
return "-0:500";
}
There are still other methods that can be extracted. This method seems to take a date written as “Tue, 10 Oct 2023 17:12:47 EST
” and then converts it to a format like “10/10/2023
“. Let’s take line 9 above which gets the time zone identifier and then extract a method to do just that. Note that the original code is in an indiscriminate try/catch
block and the new method has some error checking in it:
private IEnumerable<UCNewsFeedModel> GetNewsFeed()
{
// ommited
if (publicationDate != "")
{
try
{
var modifiedInputDate = publicationDate.Substring(0,
publicationDate.LastIndexOf(" ", StringComparison.InvariantCultureIgnoreCase));
var timeZoneIdentifier = GetTimeZoneIdentifier(publicationDate);
string timeZoneOffset = GetTimeZoneOffsetFromIdentifier(timeZoneIdentifier);
var dateForParsing = modifiedInputDate + " " + timeZoneOffset;
var dt = DateTime.ParseExact(dateForParsing, "ddd, dd MMM yyyy HH:mm:ss zzz",
new CultureInfo("en-US"));
publicationDate = dt.ToShortDateString();
}
catch
{
publicationDate = "";
}
}
// omitted
}
private string GetTimeZoneIdentifier(string date)
{
if (string.IsNullOrEmpty(date)) return "";
date = date.TrimEnd(' ');
if (date.LastIndexOf(" ") < 0) return "";
return date.Substring(date.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase) + 1);
}
private string GetTimeZoneOffsetFromIdentifier(string name)
{
if (string.IsNullOrEmpty(name)) return "-0:500";
if (name.Equals("EST", StringComparison.OrdinalIgnoreCase)) return "-0:500";
if (name.Equals("EDT", StringComparison.OrdinalIgnoreCase)) return "-0:400";
if (name.Equals("CST", StringComparison.OrdinalIgnoreCase)) return "-0:600";
if (name.Equals("MST", StringComparison.OrdinalIgnoreCase)) return "-0:700";
if (name.Equals("PST", StringComparison.OrdinalIgnoreCase)) return "-0:800";
return "-0:500";
}
Now we can do a similar thing for line 8 above which gets the date and time portion from the string:
private IEnumerable<UCNewsFeedModel> GetNewsFeed()
{
// ommited
if (publicationDate != "")
{
try
{
var modifiedInputDate = GetDateAndTime(publicationDate);
var timeZoneIdentifier = GetTimeZoneIdentifier(publicationDate);
string timeZoneOffset = GetTimeZoneOffsetFromIdentifier(timeZoneIdentifier);
var dateForParsing = modifiedInputDate + " " + timeZoneOffset;
var dt = DateTime.ParseExact(dateForParsing, "ddd, dd MMM yyyy HH:mm:ss zzz", new CultureInfo("en-US"));
publicationDate = dt.ToShortDateString();
}
catch
{
publicationDate = "";
}
}
// omitted
}
private string GetDateAndTime(string date)
{
if (string.IsNullOrEmpty(date)) return "";
date = date.TrimEnd(' ');
if (date.LastIndexOf(" ") < 0) return "";
return date.Substring(0, date.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase));
}
private string GetTimeZoneIdentifier(string date)
{
if (string.IsNullOrEmpty(date)) return "";
date = date.TrimEnd(' ');
if (date.LastIndexOf(" ") < 0) return "";
return date.Substring(date.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase) + 1);
}
private string GetTimeZoneOffsetFromIdentifier(string name)
{
if (string.IsNullOrEmpty(name)) return "-0:500";
if (name.Equals("EST", StringComparison.OrdinalIgnoreCase)) return "-0:500";
if (name.Equals("EDT", StringComparison.OrdinalIgnoreCase)) return "-0:400";
if (name.Equals("CST", StringComparison.OrdinalIgnoreCase)) return "-0:600";
if (name.Equals("MST", StringComparison.OrdinalIgnoreCase)) return "-0:700";
if (name.Equals("PST", StringComparison.OrdinalIgnoreCase)) return "-0:800";
return "-0:500";
}
Lines 11 – 15 take the pieces to make a new DateTime
and then returns the short format. That can be extracted:
private IEnumerable<UCNewsFeedModel> GetNewsFeed()
{
// ommited
if (publicationDate != "")
{
var modifiedInputDate = GetDateAndTime(publicationDate);
var timeZoneIdentifier = GetTimeZoneIdentifier(publicationDate);
string timeZoneOffset = GetTimeZoneOffsetFromIdentifier(timeZoneIdentifier);
publicationDate = GetShortDateFormatForDateAndTimezoneOffset(modifiedInputDate , timeZoneOffset )
}
// omitted
}
private string GetShortDateFormatForDateAndTimezoneOffset(string date, string tzOffset)
{
try
{
string dateToParse = date + " " + offset;
var dt = DateTime.ParseExact(dateForParsing, "ddd, dd MMM yyyy HH:mm:ss zzz", new CultureInfo("en-US"));
return dt.ToShortDateString();
}
catch (Exception)
{
return "";
}
}
private string GetDateAndTime(string date)
{
if (string.IsNullOrEmpty(date)) return "";
date = date.TrimEnd(' ');
if (date.LastIndexOf(" ") < 0) return "";
return date.Substring(0, date.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase));
}
private string GetTimeZoneIdentifier(string date)
{
if (string.IsNullOrEmpty(date)) return "";
date = date.TrimEnd(' ');
if (date.LastIndexOf(" ") < 0) return "";
return date.Substring(date.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase) + 1);
}
private string GetTimeZoneOffsetFromIdentifier(string name)
{
if (string.IsNullOrEmpty(name)) return "-0:500";
if (name.Equals("EST", StringComparison.OrdinalIgnoreCase)) return "-0:500";
if (name.Equals("EDT", StringComparison.OrdinalIgnoreCase)) return "-0:400";
if (name.Equals("CST", StringComparison.OrdinalIgnoreCase)) return "-0:600";
if (name.Equals("MST", StringComparison.OrdinalIgnoreCase)) return "-0:700";
if (name.Equals("PST", StringComparison.OrdinalIgnoreCase)) return "-0:800";
return "-0:500";
}
We now have several small methods that are reusable and have a single reason to change. The original method can still be broken down but we have done enough here to demonstrate the extract method refactoring.
References
- Fowler M, Beck K, Brant J, Opdyke W, Roberts D. Refactoring: Improving the Design of Existing Code. Addison-Wesley; 1999.