Chapter 2. Checklists

How do you transition from programmer to software engineer? I don’t want to claim that this book has the definitive answer to that question, but I hope it can set you on the path.

I believe that it’s so early in the history of software development that there’s lots of things we still don’t understand. On the other hand, we can’t wait until we’ve figured it all out. We learn by experimentation. The activities and methodologies presented in this book are inspired by many great people who came before me1. These practices have worked for me and the many people I’ve taught. I hope that they’ll work for you as well, or that they’ll inspire you to identify even better ways to work.

1 There are too many to be listed here, but take a look at the bibliography. I’ve done my best to credit everyone for their contributions, but I’ve certainly forgotten some, for which I apologise.

2.1 An aid to memory

A fundamental problem with software development is that there’s a lot going on. Our brains aren’t good at keeping track of many things at the same time. We also have a tendency to skip doing things that don’t seem important right now.

The problem isn’t that you don’t know how to do a thing; it’s that you forget to do it, even though you know that you ought to.

This problem isn’t isolated to programming. Pilots suffer from it, and they invented a simple solution to the problem: checklists.

I realise that this sounds incredibly dull and constraining, but consider the origins of the checklist. According to Atul Gawande[40] it started in 1935 with the B-17 bomber. Compared to previous airplanes, the B-17 was much more complex. In fact, it was so complex that it crashed on a demonstration flight for potential army buyers, killing two crew members, including the pilot.

An investigation into the crash concluded that it was due to ‘pilot error’. Given that the pilot was one of the army air corps’ most experienced test pilots, this could hardly be written off as lack of training. As one newspaper put it, the plane was just “too much airplane for one man to fly.”[40]

A group of test pilots came up with a solution: a checklist of simple actions to perform during take-off, and another to follow during landing.

Simple checklists empower skilled professionals such as airplane pilots. When a task is complex, it’s almost inevitable that you forget to consider a thing or two. A checklist helps you focus on the hard parts of your task by taking your mind off the trivial things. You don’t have to make an effort to remember to do all the trivial things; you just have to remember to refer to the checklist at various pause points.

It’s important to understand that checklists are supposed to enable, support, and liberate practitioners. They’re not there to monitor or audit. The power of checklists is that you use them in the situation - not that they leave any trail of evidence. Perhaps the most powerful lists are those that specifically don’t leave any audit trail. These could simply be lists on a wall poster, a clipboard, in a ring binder, or similar.

Checklists are not intended to constrain you, but rather to improve results. As one of the Atul Gawande’s informants put it:

“When surgeons make sure to wash their hands or to talk to everyone on the team” - he’d seen the surgery checklist - “they improve their outcomes with no increase in skill. That’s what we are doing when we use the checklist.”[40]

If pilots and surgeons can follow checklists, then so can you. The key is to improve the outcome with no increase in skill.

At various points in the coming chapters, I’m going to present you with checklists. This isn’t the only ‘engineering method’ that you’ll learn, but it’s the simplest. It’s a good place to start.

A checklist is just an aid to memory. It doesn’t exist to restrict you; it exists to help you remember to perform trivial, but important actions, such as washing your hands before surgery.

2.2 Checklist for a new code base

The checklists I’ll present in this book are suggestions. They’re based on how I approach programming, but your circumstances differ from mine, so they may not fit perfectly. Just like the take-off checklist for an Airbus A380 is different from the take-off checklist for the B-17.

Use my checklist suggestions verbatim, or as inspiration.

Here’s a checklist for starting a new code base:

Images Use Git

Images Automate the build

Images Turn on all error messages

That doesn’t look like much, and that’s deliberate. A checklist isn’t a complex flowchart with detailed instructions. It’s a simple list of items that you can cover in a few minutes.

Checklists come in two forms: read-do and do-confirm[40]. With a read-do checklist, you read each item on the list and immediately perform the action before you move on to the next item. With a do-confirm checklist, you do all the things, and then you run through the checklist and confirm that you’ve done all the activities.

I’ve deliberately left the above list vague and conceptual, but since it’s worded in the imperative form, it suggests a read-do checklist. You could easily turn it into a do-confirm checklist, but if you do, you should make sure to go through it with at least one other person. That’s what pilots do. One reads the checklist and the other confirms. It’s too easy to skip a step if you’re by yourself, but a copilot can keep you honest.

Exactly how to use Git, automate the build, and turn on all error messages is up to you, but in order to make the above checklist concrete, I’ll show you a detailed, running example.

2.2.1 Use Git

Git has become the de-facto standard source control system. Use it2. Compared to centralised source control systems such as CVS or Subversion, a distributed source control system offers a tremendous advantage. If you know how to use it, that is.

2 Despite being superior to most alternatives, Git has plenty of issues. The biggest problem is its complicated and inconsistent command-line interface. If a better distributed source control system comes along in the future, feel free to migrate. At the time when I’m writing this, however, there’s no better alternative.

Git isn’t the most user-friendly piece of technology on the planet, but you’re a programmer. You’ve managed to learn at least one programming language. Compared to that, learning the basics of Git is easy. Do yourself a favour and invest one or two days learning the basics of it. Not a graphical user interface on top of it, but how it actually works.

Git gives you the ability to boldly experiment with your code. Try something out, and if it doesn’t work, just undo your changes. It’s the ability to work as a source control system on your hard drive that makes Git stand above centralised version control systems.

There are several graphical user interfaces (GUIs) on top of Git, but in this book, I’ll stick to the command line. Not only is it the foundation of Git, it’s also the way I normally prefer to work with it. Although I’m on Windows, I work in Git Bash.

The first thing you should do in a new code base is to initialise a local Git repository3. Open your command-line window in the directory where you’d like to put the code. At this time you don’t have to worry about online Git services like GitHub; you can always connect the repository later. Then write4:

3 I’d stick to this rule for any code base that I expect will live for more than a week. I sometimes don’t bother initialising a Git repository for truly ephemeral code, but my threshold for creating a Git repository is quite low. You can always undo it again by deleting the .git directory.

4 Don’t write the $ - it’s just there to indicate the command-line prompt. I’ll include it throughout the book when showing what happens on the command line.

$ git init

That’s it. You may also consider following the advice from my friend Enrico Campidoglio[17] and add an empty commit:

$ git commit --allow-empty -m "Initial commit"

I usually do this because it enables me to rewrite the history of my repository before I publish it to an online Git service. You don’t have to do this, though.

2.2.2 Automate the build

When you have hardly any code, it’s easy to automate compilation, testing, and deployment. Trying to retrofit Continuous Delivery[49] onto an existing code base can seem a formidable undertaking. That’s the reason I think you should do this right away.

Currently there’s no code, only a Git repository. You’ll need a minimal application in order to have something to compile. Create the minimal amount of code that you’re able to deploy, and then deploy it. This is an idea similar to a Walking Skeleton[36], but one step earlier in the development process, as suggested by figure 2.1.

Images

Figure 2.1: Use a wizard or scaffolding program to create a shell of the application, commit it and deploy it. Then use an automated test to create a Walking Skeleton[36] that you commit and deploy.

A Walking Skeleton is an implementation of the thinnest possible slice of real functionality that you can automatically build, deploy, and test end-to-end[36]. You can do that next, but I think that there’s value in first establishing a deployment pipeline[49].

Common issues related to establishing a deployment pipeline

What if you can’t set up a deployment pipeline yet? Perhaps you don’t have a Continuous Integration server. If so, make it a priority to get one. You don’t have to get an actual server. These days, there’s plenty of cloud-based Continuous Delivery services.

Perhaps you don’t have a production environment yet. Try to work around this issue by configuring your deployment pipeline so that you can release to some pre-production environment. Preferably one that looks as much like the production environment as possible. Even if you can’t get your hands on hardware that resembles the production environment, at least try to simulate the production system’s network topology. You can use smaller machines, virtual machines, or containers.

Most of the policies that I suggest in this book are free. This one tends to cost money, for servers, software, or cloud-based services. The amounts are typically only fractions of a programmer’s salary, so compared to the the total cost of developing software, it’s money well spent.

Before you set up a deployment pipeline, however, you should make sure that you can easily compile the code and run developer tests. You’ll need some code.

This book is composed around an example that acts as its backbone. You’ll see how to develop a simple online restaurant reservation system in C#. Right now, we need a web service that’ll handle HTTP requests.

In order to move in that direction, the simplest way to get started is to create an ASP.NET Core web project. I’ll be using Visual Studio to do this5. While I like to use command-line interfaces for interactions I frequently perform, I like the guidance an IDE can give me for actions I only do now and then. You can, if you’d like, use a command-line tool instead, but the result should be the same: a few files and a working web site. Listings 2.1 and 2.2 show6 the files that Visual Studio created.

5 I’ll not show any screen shots or otherwise get into details about this process. Before the book is published, these would be out of date. It is, however, a simple process involving only one or two steps.

6 C# is a relatively verbose language, so I generally only show the highlights of a file. I’ve left out using directives and namespace declarations.

When you run the web site, it serves a single text file with the contents:

Hello World!

That’s good enough for now, so commit the code to Git.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Listing 2.1: Default ASP.NET Core web service entry point, generated by Visual Studio. (Restaurant/f729ed9/Restaurant.RestApi/Program.cs)

The goal in this step is to automate the build. While you can open your IDE and use it to compile the code, that’s not automatable. Create a script file that performs the build, and commit that to Git as well. Initially, it’s as simple as listing 2.3 shows.

I spend all of my command-line time in Bash, despite working in Windows, so I’ve defined a shell script. You can create a .bat file, or a PowerShell script instead, if that’s more to your liking7. The important part is that right now, it calls dotnet build. Notice that I’m configuring a Release build. The automated build should reflect what will eventually go into production.

7 If you need to do something more complex, such as assemble documentation, compile reusable packages for package managers, and so on, you may consider a full-blown build tool. But start simple, and only add complexity if you need it. Often, you don’t.

As you add more build steps, you should add them to the build script as well. The point of the script is that it should serve as a low-friction tool that developers can run on their own machine. If the build script passes on a developer’s machine, it’s OK to push the changes to the Continuous Integration server.

The next step should be to establish your deployment pipeline. When you’ve added new commits to master this should trigger a process that (if successful) deploys the changes to your production environment, or at least makes everything ready so that deployment is but a manual sign-off away.

The details involved in doing this are beyond the scope of this book. They depend on which Continuous Integration server or service you use, as well as its version. This changes all the time. I could show you how to enable this on Azure DevOps Services, Jenkins, TeamCity, and so on, but then this would become a book about that particular technology.

public class Startup
{
    // This method gets called by the runtime. Use this method to add
    // services to the container.
    // For more information on how to configure your application,
    // visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
    }

    // This method gets called by the runtime. Use this method to configure
    // the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}

Listing 2.2: Default Startup file, generated by Visual Studio. I’ve edited the comment line breaks to make them fit on the page. (Restaurant/f729ed9/Restaurant.RestApi/Startup.cs)

#!/usr/bin/env bash
dotnet build --configuration Release

Listing 2.3: Build script. (Restaurant/f729ed9/build.sh)

2.2.3 Turn on all error messages

I once sat with another programmer to teach him how to add unit tests to an existing code base. Soon we ran into trouble. The code compiled, but didn’t do what it was supposed to do. He started to frantically navigate around the code base, chaotically changing a thing here, a line of code there. I asked him:

“Could we see if there are any compiler warnings?”

I had a fairly good idea about what the problem was, but I try to help people by letting them discover things for themselves. You learn better that way.

“That’s no use,” he replied. “There are hundreds of compiler warnings in this code base.”

That turned out to be true, but I insisted that we looked through the list, and I quickly found the warning I knew would be there. It correctly identified the problem.

Compiler warnings and other automated tools can detect problems with code. Use them.

In addition to using Git, this is one of the lowest-hanging fruits you can pick. I’m bemused that so few people use the tools that are readily available to them.

Most programming languages and environments come with various tools that’ll check your code, such as compilers, linters, code analysis tools, and style and formatting guards. Use as many as you can; they’re rarely wrong.

In this book I’ll be using C# for examples. It’s a compiled language, and compilers typically emit warnings whenever they detect code that compiles, but is most likely wrong. These warning are usually correct, so it pays to take heed of them.

As the anecdote illustrates, it can be difficult to discover a new compiler warning if you already have 124 other warnings. For that reason, you should have zero tolerance for warnings. You should have zero warnings.

In fact, you should treat warnings as errors.

All compiled languages I’ve worked with come with an option to turn compiler warnings into compiler errors. That’s an effective way to prevent warnings from accumulating.

It can seem like a formidable task to address hundreds of existing warnings. It’s much easier to address a single warning the moment it appears. For that reason, turn the warnings-as-errors option on as one of the first things you do in a new code base. That effectively prevents any compiler warnings from accumulating.

When I do that in the code base introduced in section 2.2.2, the code still compiles. What little code Visual Studio had generated for me fortunately doesn’t emit any warnings8.

8 In Visual Studio, the warnings-as-errors settings is associated with a build configu-ration. You should definitely treat warnings as errors in Release mode, but I also do it in Debug mode. If you want to change this setting for both configurations, you have to remember to do it twice. Perhaps you should make that part of your checklist.

Many languages and programming environments come with additional automated tools you can use. A linter, for example, is a tool that warns you if a piece of code seems to be exhibiting a code smell. Some even check spelling errors. There are linters for such diverse languages as JavaScript and Haskell.

C# comes with a similar set of tools called analysers. While turning warnings into errors is only a checkbox somewhere, adding analysers is a little more involved. Still, with the latest version of Visual Studio, it’s straight-forward9.

9 Again, I’m not describing the actual steps required to do this, because a detailed description is likely to be outdated before the book is published.

These analysers represent decades of knowledge about how to write .NET code. They began as an internal tool called UrtCop. It was used during early development of the .NET framework itself, so it predates .NET 1.0. It was later renamed to FxCop[23]. It has lived an uneasy existence in the .NET ecosystem, but has recently been re-implemented on top of the Roslyn compiler tool chain.

It’s an extensible framework that contains an abundance of guidelines and rules. It looks for violations of naming conventions, potential security problems, incorrect use of known library APIs, performance issues, and much more.

When activated in the sample code shown in listings 2.1 and 2.2, the default rule set emits no fewer than seven warnings! Since the compiler now treats warnings as errors, the code no longer compiles. At first blush, this may seem to get in the way of getting work done, but the only thing this should upset is the illusion that code is maintainable without careful contemplation.

Seven warnings today are easier to address than hundreds of warnings in the future. Once you get over the shock, you realise that most of the fixes involve deleting code. You only have to make one change to the Program class. You can see the result in listing 2.4. Can you spot the change?

public static class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Listing 2.4: ASP.NET Core web service entry point, after analyser warnings have been addressed. (Restaurant/caafdf1/Restaurant.RestApi/Program.cs)

The change to the Program class is that it’s now marked with the static keyword. There’s no reason for a class to support instantiation when it has only shared members. That’s an example of a code analysis rule. It’s hardly of much import here, but on the other hand, the fix is as simple as adding a single keyword to the class declaration, so why not follow the advice? In other cases, that rule can help you make the code base easier to understand.

Most of the changes that I had to make affect the Startup class. Since they involve deletion of code, I think the result is an improvement. Listing 2.5 shows the result.

What changed? Most noticeably, I deleted the ConfigureServices method, since it didn’t do anything. I also sealed the class and added a call to ConfigureAwait.

Each code analysis rule comes with online documentation. You can read about the motivation for the rule, and how to address warnings.

public sealed class Startup
{
    // This method gets called by the runtime. Use this method to configure
    // the HTTP request pipeline.
    public static void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!")
                    .ConfigureAwait(false);
            });
        });
    }
}

Listing 2.5: Startup file after analyser warnings have been addressed. Compare with listing 2.2. (Restaurant/caafdf1/Restaurant.RestApi/Startup.cs)

Nullable reference types

C# 8 introduces an optional feature known as Nullable reference typesa. It enables you to use the static type system to declare whether or not an object can be null. When the feature is enabled, objects are assumed to be non-nullable, i.e. that they can’t be null.

a The way Microsoft names concepts and features can be confusing. Like all other mainstream C-based languages, reference types have always been nullable in the sense that objects can be null. The feature should really be called nonnullable reference types.

If you want to declare that an object can be null, you adorn the type declaration with a ? (question mark), as in IApplicationBuilder? app.

Being able to distinguish between objects that aren’t supposed to be null from objects that may be null helps reduce the amount of defensive coding you need to add. The feature has the potential to reduce the number of run-time defects in your system, so turn it on. Turn it on when the code base is new, so that you don’t have to deal with too many compiler errors.

When I turn on this feature for the sample code base shown in this chapter, the code still compiles.

Static code analysis is like an automated code review. In fact, when a development organisation contacts me because they’d like me to do a C# code review, I first tell them to run the analysers. That’ll save them hours off my fee.

I typically don’t hear from that potential customer again10. When you run such analysers over an existing code base, you can easily get thousands of warnings and feel overwhelmed. To prevent that, start using those tools right away.

10 I’m a terrible businessman... or am I?

Contrary to compiler warnings, static code analysis tools like linters or the .NET Roslyn analysers tend to produce some false positives11. The automated tools typically give you various options to suppress false positives, so that’s hardly a reason to spurn them.

11 I realise that this is confusing, but here it is: a positive means a warning, i.e. that the code looks wrong. That doesn’t sound positive, but in the terminology of binary classification, positive indicates the presence of a signal, whereas negative indicates absence. It’s also used in software testing and medicine. Just consider what it means to be HIV-positive!

Treat compiler warnings as errors. Treat linter and static code analysis warnings as errors. At first it’ll be frustrating, but it’ll improve the code. It might make you a better programmer as well.

Is that engineering? Is that it? It’s not all you can do, but it’s a good first step. Engineering, broadly speaking, is to use all the heuristics and deterministic machinery you can, to improve the chance of ultimate success. Those tools are like automated checklists. Every time you run them, they control for thousands of potential issues.

Some of them have been around for a long time, but in my experience few people use them. The future is unevenly distributed. Turn the controls on. Improve the outcome with no increase in skill.

Treating warnings as errors is easiest to do at the beginning. When a code base is brand-new, there’s no code to warn about. This lets you deal with errors one at a time.

2.3 Adding checks to existing code bases

In the real world, you rarely get the opportunity to begin a new code base. Most professional software development involves working with existing code. While it’s less demanding to treat warnings as errors in a new code base, it’s not impossible in an existing code base.

2.3.1 Gradual improvement

The key is to gradually turn on the extra guards. Most existing code bases contain several libraries12, as exemplified in figure 2.2. Turn on the extra checks one library at a time.

12 Libraries are also known as packages. Visual Studio developers will often refer to libraries as projects within a solution.

Images

Figure 2.2: A code base made from packages. In this example, the packages are HTTP API, Domain Model, and Data Access.

You can often turn on one type of warning at a time. In an existing code base, you may already have hundreds of compiler warnings. Extract the list and group it by type. Then pick a particular type of warning that has perhaps a dozen instances, and fix all of those warnings. Fix them while they’re still compiler warnings, so that you can keep working with the code. Check your changes into Git every time you’ve made an improvement. Merge those gradual improvements into master.

Once you’ve eliminated the last warning of a given type (in that part of the code base), turn those warnings into errors. Then move on to another type of warning, or address the same type in another part of the code base.

You can do the same with linters and analysers. With .NET analysers, for example, you can configure which rules to enable. Address one rule at a time, and once you’ve eliminated all warnings produced by a given rule, turn that rule on so that it prevents all future instances.

Likewise, C#’s nullable reference types feature can be gradually enabled.

The key, in all cases, is to follow the Boy Scout Rule[61]: leave the code in a better state than you found it.

2.3.2 Hack your organisation

When I talk at conferences and in user groups, people often approach me. Usually they are inspired, but feel that their manager will not let them focus on internal quality.

The benefit of treating warnings as errors is that you add an institutional quality gate. If you treat warnings as errors and turn on static code analysis, you relinquish some control. Loss of control doesn’t sound good, but it can sometimes be an advantage.

When you’re facing pressure to ‘just deliver’ because ‘we don’t have time to do it by the book’, imagine replying,

“Sorry, but if I do that, the code doesn’t compile.”

Such a reply has the potential to curb stakeholders’ insistence on ignoring engineering discipline. It’s not strictly the case that you can’t possibly circumvent any of those automatic checks, but you don’t have to tell everyone that. The stratagem is that you turn what used to be a human decision into a machine-enforced rule.

Is this morally appropriate? Use your judgment. As a professional software developer, you’re the technical expert. It’s literally your job to make technical decisions. You can report all details to your superiors, but a lot of the information will be meaningless to non-technical managers. Providing technical expertise includes not confusing stakeholders with details they can’t make sense of or use.

In a healthy organisation, that the best strategy is to be open and honest about what you do. In an unhealthy organisation, for example an organisation with a substantial ‘hustle culture’, adopting a counter-strategy might be more appropriate. You can use automated quality gates to hack the culture of your organisation. Even if this involves mild subterfuge, you could still argue that the ultimate goal is to support good software engineering. This should also be advantageous to the entire organisation.

Use your moral judgment. Do this for the good of the organisation, not just to further your own personal agenda.

2.4 Conclusion

Checklists will improve outcomes with no increase in skill[40]. Use them. Checklists help you remember to make the right decisions. They support you; they don’t control you.

In this chapter, you’ve seen an example of a simple checklist to use when you start a new code base. You then read about the consequences of instituting that checklist. A checklist can be simple, and yet have a big effect.

You saw how to enable Git right away. That’s the simplest of the three items on the checklist. When you consider how easy it is to take that step, though, that small effort pays manifold.

You also saw how to automate the build. This, too, is easy to do when you do it right away. Have a build script. Use it.

Finally, you saw how to turn compiler warnings into errors. You can also use additional automated checks such as linters or static code analysis. Given how easy it is to turn these features on, there’s little reason to pass them by.

In the rest of the book, you’ll see the impact these early decisions have on the code base as I add features.

Engineering is more than following a checklist, or automating what can be automated, but those measures represent a step in the right direction. They’re small improvements you can make today.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.93.221