I gradually became aware of this cluster of anti-patterns when building Web Services. I found solutions and mostly kept them to myself because I was able build software faster, and with fewer errors and lag. That was my secret sauce.
I think it’s time to share, because I really want to challenge conventional thinking and provide you with more conceptual design tools. Note, that this is more of an academic article, because you shouldn’t implement my approaches without the right tooling. Instead, I want your help to demand the correct principles in coding, so that you will get your tooling one day.
(These days I have moved beyond to an even better approach, one that I hope to share in the coming year.)
In my last article https://medium.com/codex/when-dry-becomes-an-anti-pattern-code-in-code-87b1caab791f, I highlighted the eminent importance of readability.
In one of the replies to that article, someone flagged a potential flaw with my limited definition:
the "more readable" code does not scale because one will need to read too much.
I still need to take the time to write a full dedicated article, but for now, I’ll only add some colour.
Colour Theory is well understood in the visual arts, yet software only has pseudo-science so far. We’re exploring the shortcomings of DRY science, but my hastily invented philosophy, Readable Is Most Important (RIMI), is also lacking.
Writing “more lines” of code in a single function is ideal. The alternative is flinging those lines of code across multiple code fragments. Each fragment requires its own ceremony of function-declaration and maybe also class-declaration. When you are trying to read abstracted code, you can’t just read in sequence like you are reading this sentence, you are forced to jump around the code all random-access like. Computers are less effective at random access, humans are terrible at it. The human has a working-memory of around 7 symbols, and jumping around an abstracted graph of code quickly reaches that limit for all except the 10x coders, who love writing such abstractions.
Of course, simply having more lines of code doesn’t accomplish the goal of readability. You need to design and understand a wide range of definitive examples as well as edge cases. I tend to try and build a “high-level process” function. Such a function has around 1-line per “step” and makes heavy use of sub-functions to hide the details.
For example:
void ImportMechanics(sourceQuery, tableRoot, transformQuery, loadQuery)
{
var sourceData = ReadFromSource("SELECT *...");
WriteToStaging(sourceData, Mechanics_Staging);
TransformData("UPDATE Mechanics_Staging...");
TransformData("UPDATE Mechanics_Staging..."); //Second transformation
LoadData("UPSERT Mechanics...");
}
This means that function can be utilised as the “index”. When reading the code, this is the home base. This benefit is amplified even further in this chapter.
The following 3 chapters will cover 3 areas of abstraction commonly found in Asp.Net based Web Services. They are: the Pipeline; Custom Adapter Code; and, the ORM.
The diagram above shows how web requests are handled. A request is made on port 443, and on Windows the HTTP.SYS module marshals the request to the correct process where the Host matches. Asp.Net is the library that handles the request in the process. A software developer needs to ultimately glue Asp.Net to the ORM, which then communicates with the Database. Logic is ideally separated from the glue to make it easier to Unit Test, and reuse Logic.
In this chapter, I will focus on the Pipeline.
Asp.Net is an opinionated framework, and those opinions have changed and improved over time, generally for the better. In the latest version, Asp.Net Core 5, the word Middleware is commonly used.
The Asp.Net pipeline deals with basic Web Server activities, such as HSTS, HTTPS redirect, and Static files. I’m not talking about those. Instead, focus on Cookie Policy, Auth, Session, and of course MVC.
The ASP.NET MVC patterns are well-established and powerful, but they are not optimal to read. Code examples appear “minimal”, perhaps even “artisanal”, but that doesn’t make it easier to read, that’s a false-comfort. Instead code is dispersed, and a high-level process function is not defined altogether.
[ApiController]
[Authorise(Role="Sales")]
[Route("api/[controller]")]
public class CarsController: ControllerBase
{
private readonly CarsContext db;public TodoItemsController(TodoContext context)
{
_context = context;
}
[HttpPost()]
public IActionResult Create(CarDetails newCar)
{
db.Cars.Add(newCar);
db.SaveChanges();
return Ok();
}
}
Those who are familiar with the Asp.Net framework will feel quite at home here. But it’s not as readable as actual code, you can’t read what you can’t see. Shoving the code in the closet makes it look “tidy” but it’s not really tidy is it?
- Attributes are used to decorate members, to dynamically register Web Services at runtime. Attributes cannot be debugged.
- Authorisation uses attributes to annotate the requirements of a Web Service method, but they are enforced in a blackbox before the method is called. Misconfigured authorisation is very difficult to debug.
- Deserialisation is implied from the types of parameters of the method, and happens in a black box in the middleware. It will fail if you misconfigure it.
- URI is derived from reflection.
- Error handling can be configured centrally
- Caching can be configured centrally, and with attributes per method. This can get quite complex. Don’t do caching.
- Dependency Injection will provide services during construction. This is the recommended pattern for DbContexts because the framework takes care of disposing the context after results have finished writing to the Response stream.
To debug a problem, you need to know how Asp.Net works AND you need to look in several places to see how it’s configured.
Consider the imperative-code approach:
public class CarsController: HttpController
{
public void Create(HttpContext ctx)
{
try
{
ctx.Request.DemandAuthorisedUser("Staff"); //Throws an exception if not
var newCar = CarDetails.Deserialize(ctx.Request); //Real code
using (var db = LocalDatabase.Create())
{
db.Cars.Add(newCar);
db.SaveChanges();
}
ctx.Response.Ok();
}
catch (Exception ex)
{
ctx.Response.Error(ex); //Break-point here if you like
}
}
}public void RegisterWebApis(IApplicationBuilder app)
{
var carsController = new CarsController();
app.Use("api/Cars/Create", context => carsController.Create);
}
There’s a bit of pseudo-code in there because this is mostly academic at this point.
All of the code is one place to be read by a fresh graduate.
- There’s no need for Dependency Injection, because there is a Factory method. That can be changed out if you need — but that will practically never happen anyway.
- Authorisation is now one-line of code. It’s a line of code you can debug. It’s a line of code you can change. Do you want to disable authorisation for that function? Comment out the line. Do you want a single-use token only for this function? Easy.
- The URI is defined specifically for this function. You be inside the Create function, press CTRL+F12, and find where it’s bound to a specific relative URI.
- Error handling is obvious. You can set a break-point while debugging to investigate the depths of an exception. (You can even capture Response serialization errors if you have such a function returning data).
So although it’s still best-practice to use Asp.Net how it was intended, I have demonstrated how there is a way to make Web Service code more readable. Hiding away code logic does make a Controller “minimal”, but that’s like shoving a mess into a cupboard. Complexity is inescapable, and it should be on display all in one place.
I’m sure the Microsoft engineers adhered to DRY principles, and many are very happy with the API offered. I would like to see tooling and support for RIMI principled developers.