TDD and refactoring to patterns in C#: how to write a cron parser - Part 4

In this post, we will begin the process of refactoring the code to enhance its readability.

Why undertake the process of refactoring ?

Refactoring code is the process of restructuring existing computer code without changing its external behavior. The primary goal of refactoring is to improve the code's internal structure, making it easier to understand, maintain, and extend while preserving its functionality.

Here are some reasons why refactoring is often essential:

  • Refactoring improves code readability by organizing it in a more logical and understandable way. Clear and well-structured code is easier for developers to comprehend and work with.

  • A well-refactored codebase is easier to maintain. It reduces the likelihood of introducing bugs when making changes and allows for faster bug detection and fixing.

  • Refactoring makes code more scalable by removing duplication, improving design, and ensuring that the codebase can easily accommodate future changes and additions.

  • Refactoring helps eliminate code duplication, reducing the chances of errors and making the codebase more consistent.

  • Refactoring addresses "code smells," which are signs of potential issues in the code. Common code smells include duplicated code, long methods, and complex conditional statements.

  • Refactoring allows for improvements in the overall design of the code. This includes better organization of classes, modules, and functions, leading to a more maintainable and extensible system.

  • In some cases, refactoring can lead to performance improvements by identifying and eliminating inefficient code patterns.

  • Refactoring makes it easier to introduce new features or modify existing ones. A well-structured codebase allows developers to build upon existing functionality without introducing unnecessary complexity.

  • Clean and well-refactored code enhances the code review process. It facilitates collaboration among team members and helps catch potential issues early.

  • Developers working with clean, well-organized code can be more productive. Refactoring reduces cognitive load and makes it easier for developers to focus on solving problems rather than deciphering confusing code.

In summary

Refactoring is a crucial aspect of the software development process that contributes to the long-term health and sustainability of a codebase. It allows for continuous improvement, adaptation to changing requirements, and the creation of maintainable and efficient software systems.

What is the problem in our case ?

There isn't a fundamental issue as such, but while the code may achieve its intended functionality, it does so in a way that is not very readable. For instance, we are compelled to provide comments explaining the various items of the fields array to comprehend their functions (such as month, day of the week, and so forth). It would be more beneficial if these distinctions were directly reflected in the code.

 1if (fields[3] != "*") // month
 2{
 3	nextOccurrences = nextOccurrences.Where(t => t.Month == Convert.ToInt32(fields[3])).ToList();
 4}
 5
 6if (fields[2] != "*") // dayOfMonth
 7{
 8	nextOccurrences = nextOccurrences.Where(t => t.Day == Convert.ToInt32(fields[2])).ToList();
 9}
10
11if (fields[4] != "*") // dayOfWeek
12{
13	nextOccurrences = nextOccurrences.Where(t => (int)t.DayOfWeek == Convert.ToInt32(fields[4])).ToList();
14}
15
16if (fields[1] != "*") // hour
17{
18	nextOccurrences = nextOccurrences.Select(t => new DateTime(t.Year, t.Month, t.Day, Convert.ToInt32(fields[1]), 0, 0)).ToList();
19}
20
21if (fields[0] != "*") // minute
22{
23	nextOccurrences = nextOccurrences.Select(t => new DateTime(t.Year, t.Month, t.Day, t.Hour, Convert.ToInt32(fields[0]), 0)).ToList();
24}

In the next phase of our development process, we will address these issues through refactoring while adhering to best practices in software design.

What are design patterns ?

Design patterns are reusable solutions to common problems encountered in software design. They represent best practices for solving specific design issues and provide general templates or blueprints for creating software structures. Design patterns help streamline the development process by offering tested and proven solutions that can be adapted to various scenarios.

Here are some key characteristics of design patterns:

  • Design patterns encapsulate proven solutions to recurring design problems. They have been used and refined by experienced developers over time.

  • Design patterns provide a level of abstraction that allows developers to focus on high-level design concepts rather than dealing with low-level implementation details.

  • Design patterns promote flexibility and adaptability in software design. They can be customized and extended to suit different requirements.

  • Design patterns establish a common vocabulary among developers, facilitating communication and understanding of design decisions.

  • Design patterns promote encapsulation by separating responsibilities and functionalities into distinct components.

Some well-known design patterns include the Singleton pattern, Factory pattern, Observer pattern, and MVC (Model-View-Controller) pattern. Each pattern addresses specific design challenges and can be applied in various contexts to improve the overall structure and maintainability of software systems.

A bit of history

Design patterns underwent extensive scrutiny in the late 1990s and became the focal point of a renowned and best-selling book (Design Patterns).

Yes and concretely ? Enter the Interpreter

A cron expression can be likened to a language; it seeks to convey future dates from a string, much like how English or French conveys meaning from sentences. Interestingly, there exists a design pattern tailored for this purpose! When dealing with a language, it can establish a representation for interpreting sentences — enter the Interpreter pattern.

The Interpreter pattern describes how to define a grammar for simple languages, represent sentences in the language and interpret these sentences.
Design Patterns

Purists might find it surprising to employ the Interpreter pattern for such a straightforward grammar (given that a cron expression is essentially just a string) or to assert its use for just a few lines of code. However, our aim is to demonstrate the process of refactoring.

This refactoring, along with the explicit modeling of the various fields, will enable us to implement more intricate scenarios.

Code in Visual Studio

  • Add a new interface named ICronField.cs with the following code.
1 public interface ICronField
2 {
3     public List<DateTime> GiveNextOccurrencesFrom(List<DateTime> references);
4 }
  • Add a new class named MonthCronField.cs with the following code.
 1public class MonthCronField : ICronField
 2{
 3    private RootCronExpression Expression { get; set; }
 4
 5    private string Pattern { get; set; }
 6
 7    public MonthCronField(RootCronExpression expression)
 8    {
 9        Expression = expression;
10        Pattern = expression.Pattern.Split(' ')[3];
11    }
12
13    public List<DateTime> GiveNextOccurrencesFrom(List<DateTime> candidates)
14    {
15        if (Pattern == "*") return candidates;  
16        
17        var month = Convert.ToInt32(Pattern);
18
19        candidates = candidates.Where(t => t.Month == month).ToList();
20        return candidates;
21    }
22}

This class explicitely encapsulates the logic specific to the month part of a cron expression. This code is decoupled from other logics (dayofweek, dayofmonth) within the RootCronExpression class.

  • Add the other classes (DayOfTheMonthCronField, DayOfTheWeekCronField, HourCronField, MinuteCronField) with their own logic.
 1public class DayOfMonthCronField : ICronField
 2{
 3    private RootCronExpression Expression { get; set; }
 4
 5    private string Pattern { get; set; }
 6
 7    public DayOfMonthCronField(RootCronExpression expression)
 8    {
 9        Expression = expression;
10        Pattern = expression.Pattern.Split(' ')[2];
11    }
12
13    public List<DateTime> GiveNextOccurrencesFrom(List<DateTime> candidates)
14    {
15        if (Pattern == "*") return candidates;
16
17        var dayOfMonth = Convert.ToInt32(Pattern);
18
19        candidates = candidates.Where(t => t.Day == dayOfMonth).ToList();
20        return candidates;
21    }
22}
 1public class DayOfWeekCronField : ICronField
 2{
 3    private RootCronExpression Expression { get; set; }
 4
 5    private string Pattern { get; set; }
 6
 7    public DayOfWeekCronField(RootCronExpression expression)
 8    {
 9        Expression = expression;
10        Pattern = expression.Pattern.Split(' ')[4];
11    }
12
13    public List<DateTime> GiveNextOccurrencesFrom(List<DateTime> candidates)
14    {
15        if (Pattern == "*") return candidates;
16
17        var day = Convert.ToInt32(Pattern);
18
19        candidates = candidates.Where(t => (int)t.DayOfWeek == day).ToList();
20        return candidates;
21    }
22}
 1public class HourCronField : ICronField
 2{
 3    private RootCronExpression Expression { get; set; }
 4
 5    private string Pattern { get; set; }
 6
 7    public HourCronField(RootCronExpression expression)
 8    {
 9        Expression = expression;
10        Pattern = expression.Pattern.Split(' ')[1];
11    }
12
13    public List<DateTime> GiveNextOccurrencesFrom(List<DateTime> candidates)
14    {
15        if (Pattern == "*") return candidates;
16
17        var hour = Convert.ToInt32(Pattern);
18
19        candidates = candidates.Select(t => new DateTime(t.Year, t.Month, t.Day, hour, 0, 0)).ToList();
20        return candidates;
21    }
22}
 1public class MinuteCronField : ICronField
 2{
 3    private RootCronExpression Expression { get; set; }
 4
 5    private string Pattern { get; set; }
 6
 7    public MinuteCronField(RootCronExpression expression)
 8    {
 9        Expression = expression;
10        Pattern = expression.Pattern.Split(' ')[0];
11    }
12
13    public List<DateTime> GiveNextOccurrencesFrom(List<DateTime> candidates)
14    {
15        if (Pattern == "*") return candidates;
16
17        var minute = Convert.ToInt32(Pattern);
18
19        candidates = candidates.Select(t => new DateTime(t.Year, t.Month, t.Day, t.Hour, minute, 0)).ToList();
20        return candidates;
21    }
22}
  • Modify the RootCronExpression class with the following code.
 1public DateTime GiveNextExecutionFrom(DateTime reference)
 2{
 3    var nextOccurrences = GetDates(reference);
 4    nextOccurrences = _monthField.GiveNextOccurrencesFrom(nextOccurrences);
 5    nextOccurrences = _dayOfMonthField.GiveNextOccurrencesFrom(nextOccurrences);
 6    nextOccurrences = _dayOfWeekField.GiveNextOccurrencesFrom(nextOccurrences);
 7    nextOccurrences = _hourField.GiveNextOccurrencesFrom(nextOccurrences);
 8    nextOccurrences = _minuteField.GiveNextOccurrencesFrom(nextOccurrences);
 9
10    return nextOccurrences.Where(x => x >= reference).Min();
11}

Running the tests

It remains to verify that no regressions have been introduced by this code change. Ultimately, this is the true raison d'être of writing tests upfront.

Good news! Our application is still functional and provides accurate results.

So far, we have refactored our code to be more readable. In the next article (here), we will add a new feature and explore how to delve deeper into writing good software.