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 a software. Any developer can write code that meets the functional requirement, but the differnece comes into play when the developer reads between the lines of the functional requirements,  and codes for the non-fuinctional 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 resucue 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 human can read is of utmost importance.
 
Martin Fowler’s quote – one of my favorite – 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, reccommends  writing a method/function in a single level of abstraction.
 
Let us first quickly understand what is abstraction and what is level of abstraction.
 
Abstraction is a fundamental concept in OOPS. In brief and layman’s term and it talks about hiding the “how” part and only expose “what” to the outer world. Maybe in some other blog I will elaborate on this and explain its true meaning.
 
Level of abstraction comes to the point where our mental grouping comes into effect. It’s best imagined like our cognitive ability to continue being abstracted from details of operations. In general, different “blocks” of code inside a method is a classic indicator of different level of abstractions. 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,
  1. [  
  2.   {  
  3.     "city""Bangalore",  
  4.     "day""Monday",  
  5.     "time""Morning",  
  6.     "temp""15",  
  7.     "humidity""60",  
  8.     "reportedBy""IndiaToday"  
  9.   },  
  10.   {  
  11.     "city""London",  
  12.     "day""Tuesday",  
  13.     "time""Afternoon",  
  14.     "temp""10",  
  15.     "humidity""85",  
  16.     "reportedBy""BBC"  
  17.   },  
  18.   {  
  19.     "city""New York",  
  20.     "day""Sunday",  
  21.     "time""Evening",  
  22.     "temp""12",  
  23.     "humidity""70",  
  24.     "reportedBy""CNN"  
  25.   },  
  26.   {  
  27.     "city""Berlin",  
  28.     "day""Thursday",  
  29.     "time""Night",  
  30.     "temp""5",  
  31.     "humidity""65",  
  32.     "reportedBy""DW"  
  33.   }  
  34. ]  
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,
  1. public class ParseWeatherData  
  2.     {  
  3.         public List<WeatherDataDto> GetWeatherReport()  
  4.         {  
  5.             var weatherDtoList = new List<WeatherDataDto>();  
  6.   
  7.             var weatherReport = JsonConvert.DeserializeObject<WeatherDataModel[]>(File.ReadAllText("weather.json"));  
  8.   
  9.             if (weatherReport != null && weatherReport.Length > 0)  
  10.             {  
  11.                 foreach (var report in weatherReport)  
  12.                 {  
  13.                     var weatherDto = new WeatherDataDto()  
  14.                     {  
  15.                         City = report.city,  
  16.                         Temparature = report.temp,  
  17.                         ReportedBy = report.reportedBy  
  18.                     };  
  19.                     weatherDtoList.Add(weatherDto);  
  20.                 }  
  21.             }  
  22.              
  23.             return weatherDtoList;  
  24.         }  
  25.     }  
  26.   
  27.     public class WeatherDataDto  
  28.     {  
  29.         public string City { getset; }  
  30.         public string Temparature { getset; }  
  31.         public string ReportedBy { getset; }  
  32.     }  
  33.   
  34.     public class WeatherDataModel  
  35.     {  
  36.         public string city { getset; }  
  37.         public string day { getset; }  
  38.         public string time { getset; }  
  39.         public string temp { getset; }  
  40.         public string humidity { getset; }  
  41.         public string reportedBy { getset; }  
  42.     }  
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.
 
 
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 conplies with the SLAB principle.
  1. public List<WeatherDataDto> GetWeatherReport()  
  2.         {  
  3.             var weatherDtoList = new List<WeatherDataDto>();  
  4.   
  5.             var weatherReport = ReadWeatherReport();  
  6.   
  7.             if (IsValidWeatherReport(weatherReport))  
  8.             {  
  9.                 MapToReportDto(weatherDtoList, weatherReport);  
  10.             }  
  11.   
  12.             return weatherDtoList;  
  13.   
  14.             static WeatherDataModel[] ReadWeatherReport()  
  15.             {  
  16.                 return JsonConvert.DeserializeObject<WeatherDataModel[]>(File.ReadAllText("weather.json"));  
  17.             }  
  18.   
  19.             static bool IsValidWeatherReport(WeatherDataModel[] weatherReport)  
  20.             {  
  21.                 return weatherReport != null && weatherReport.Length > 0;  
  22.             }  
  23.   
  24.             static void MapToReportDto(List<WeatherDataDto> weatherDtoList, WeatherDataModel[] weatherReport)  
  25.             {  
  26.                 foreach (var report in weatherReport)  
  27.                 {  
  28.                     var weatherDto = new WeatherDataDto()  
  29.                     {  
  30.                         City = report.city,  
  31.                         Temparature = report.temp,  
  32.                         ReportedBy = report.reportedBy  
  33.                     };  
  34.                     weatherDtoList.Add(weatherDto);  
  35.                 }  
  36.             }  
  37.         }  

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 be violating SLAB.
 
As an extension to SRP and SOC, this principle acts as a guiding force and emphasizes the readability aspect of clean coding practices.