Clean Code - Single Level Of Abstraction

Introduction

The biggest differentiator between a Senior Developer and a Junior Developer is understanding and implementing the “non-functional” requirements of software. Any developer can write code that meets the functional requirements, but the difference comes into play when the developer reads between the lines of the functional requirements and codes for the non-functional requirements.

Ok, hang on, contrary to popular belief, the non-functional requirement isn’t only about “performance” numbers, neither is it about optimal memory usage alone. It also boils down to three major non-functional aspects,

  • Maintainability
  • Readability
  • Testability

And clean code principles come to the rescue for these three core non-functional requirements of code.

The topic for this blog is related to code readability. It is often observed that in enterprise software development, a piece of code might be written only once, but it is read and maintained maybe 100 times in its lifetime. Hence understanding a code that humans can read is of utmost importance.

Martin Fowler’s quote – one of my favorites – reads: “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

The "Single Level of Abstraction" Principle

Single Level of Abstraction (SLAB) – as the name suggests, recommends writing a method/function in a single level of abstraction.

Let us first quickly understand what is abstraction and what is level of abstraction is.

Abstraction is a fundamental concept in OOPS. In brief and layman’s terms, it talks about hiding the “how” part and only exposing “what” to the outer world. Maybe in some other blog, I will elaborate on this and explain its true meaning.

The level of abstraction comes to the point where our mental grouping comes into effect. It’s best imagined as our cognitive ability to continue being abstracted from details of operations. In general, different “blocks” of code inside a method a classic indicators of different levels of abstraction. This means, the reader of the code now has to create a “branch” in their mental grouping to read that condition or loop block and merge back to the same level where the block ended.

In my opinion, this also could be correlated with the cognitive and cyclomatic complexities of code. A higher number of these in a code is a direct indication that the code is violating the principle of “SLAB – Single Level of Abstraction”.

OK, enough talk -- let’s look at a simple example and see how in C#, we can achieve this principle.

Let us assume we need to read a JSON array from a file that contains weather report data of different cities across the world. The JSON file may look like this.

[
    {
        "city": "Bangalore",
        "day": "Monday",
        "time": "Morning",
        "temp": "15",
        "humidity": "60",
        "reportedBy": "IndiaToday"
    },
    {
        "city": "London",
        "day": "Tuesday",
        "time": "Afternoon",
        "temp": "10",
        "humidity": "85",
        "reportedBy": "BBC"
    },
    {
        "city": "New York",
        "day": "Sunday",
        "time": "Evening",
        "temp": "12",
        "humidity": "70",
        "reportedBy": "CNN"
    },
    {
        "city": "Berlin",
        "day": "Thursday",
        "time": "Night",
        "temp": "5",
        "humidity": "65",
        "reportedBy": "DW"
    }
]

To parse this data and send only the data our application requires, we often end up writing the following classes and the method in it in this fashion,

public class ParseWeatherData   
{   
    public List<WeatherDataDto> GetWeatherReport()   
    {   
        var weatherDtoList = new List<WeatherDataDto>();   

        var weatherReport = JsonConvert.DeserializeObject<WeatherDataModel[]>(File.ReadAllText("weather.json"));   

        if (weatherReport != null && weatherReport.Length > 0)   
        {   
            foreach (var report in weatherReport)   
            {   
                var weatherDto = new WeatherDataDto()   
                {   
                    City = report.city,   
                    Temparature = report.temp,   
                    ReportedBy = report.reportedBy   
                };   
                weatherDtoList.Add(weatherDto);   
            }   
        }   
         
        return weatherDtoList;   
    }   
}   

public class WeatherDataDto   
{   
    public string City { get; set; }   
    public string Temparature { get; set; }   
    public string ReportedBy { get; set; }   
}   

public class WeatherDataModel   
{   
    public string city { get; set; }   
    public string day { get; set; }   
    public string time { get; set; }   
    public string temp { get; set; }   
    public string humidity { get; set; }   
    public string reportedBy { get; set; }   
}   

Let’s take a look at the GetWeatherReport method and understand how this simple code violates the “Single Level of Abstraction” and can be further refactored to make it compliant with this principle.

GetWeatherReport

In the above illustration, I can make at least 3 levels of abstraction that the method is working with.

  • Abstr 1: In the level, the weather report JSON is parsed with the help of a JSON parser (in this case NewtonSoft library)
  • Abstr 2: Here the validation happens for the serialized weather report
  • Abstr 3: In this level, the DTO is mapped from the received model.

As per the “Single Level of Abstraction,” this is a violation and should further be refactored. In C# we could refactor this by either extracting dedicated private methods or local functions (C# 7.0 onwards).

In the below example, I would show how this could be further refactored using local functions so that it complies with the SLAB principle.

public List<WeatherDataDto> GetWeatherReport()
{
    var weatherDtoList = new List<WeatherDataDto>();

    var weatherReport = ReadWeatherReport();

    if (IsValidWeatherReport(weatherReport))
    {
        MapToReportDto(weatherDtoList, weatherReport);
    }

    return weatherDtoList;

    static WeatherDataModel[] ReadWeatherReport()
    {
        return JsonConvert.DeserializeObject<WeatherDataModel[]>(File.ReadAllText("weather.json"));
    }

    static bool IsValidWeatherReport(WeatherDataModel[] weatherReport)
    {
        return weatherReport != null && weatherReport.Length > 0;
    }

    static void MapToReportDto(List<WeatherDataDto> weatherDtoList, WeatherDataModel[] weatherReport)
    {
        foreach (var report in weatherReport)
        {
            var weatherDto = new WeatherDataDto()
            {
                City = report.city,
                Temparature = report.temp,
                ReportedBy = report.reportedBy
            };
            weatherDtoList.Add(weatherDto);
        }
    }
}

Summary

As we can learn from the above discussion, the Single Level of Abstraction is a clean coding principle that mainly enhances the readability of code. This also helps in maintaining a complex method by further refactoring it into smaller chunks of code.

In general, any complex condition, loop, or logical block of code could be classified as a different level of abstraction and could potentially violate SLAB.

As an extension to SRP and SOC, this principle acts as a guiding force and emphasizes the readability aspect of clean coding practices.


Similar Articles