Code That Fits in Your Head
Code That Fits in Your Head
Code That Fits in Your Head
“I’ve been reading Mark’s blogs for years and he always manages to entertain
while at the same time offering deep technical insights. Code That Fits in Your
Head follows in that vein, offering a wealth of information to any software
developer looking to take their skills to the next level.”
—Adam Tornhill, founder of CodeScene, author of Software Design
X-Rays and Your Code as a Crime Scene
“My favorite thing about this book is how it uses a single code base as a
working example. Rather than having to download separate code samples, you
get a single Git repository with the entire application. Its history is hand-
crafted to show the evolution of the code alongside the concepts being
explained in the book. As you read about a particular principle or technique,
you’ll find a direct reference to the commit that demonstrates it in practice. Of
course, you’re also free to navigate the history at your own leisure, stopping at
any stage to inspect, debug, or even experiment with the code. I’ve never seen
this level of interactivity in a book before, and it brings me special joy because
it takes advantage of Git’s unique design in a new constructive way.”
—Enrico Campidoglio, independent consultant, speaker and
Pluralsight author
“Mark Seemann not only has decades of experience architecting and building
large software systems, but is also one of the foremost thinkers on how to scale
and manage the complex relationship between such systems and the teams
that build them.”
—Mike Hadlow, freelance software consultant and blogger
“Mark Seemann is well known for explaining complex concepts clearly and
thoroughly. In this book he condenses his wide-ranging software development
experience into a set of practical, pragmatic techniques for writing sustainable
and human-friendly code. This book will be a must read for every
programmer.”
—Scott Wlaschin, author of Domain Modeling Made Functional
“Mark writes, ‘Successful software endures’—this book will help you to write
that kind of software.”
—Bryan Hogan, software architect, podcaster, blogger
“Mark has an extraordinary ability to help others think deeply about the
industry and profession of software development. With every interview on
.NET Rocks! I have come away knowing I would have to go back and listen to
my own show to really take in everything we discussed.”
—Richard Campbell, co-host, .NET Rocks!
Code That Fits
in Your Head
Code That Fits
in Your Head
Heuristics for Software Engineering
Mark Seemann
PART I Acceleration 1
xi
Contents
Chapter 2 Checklists 15
2.1 An Aid to Memory 15
2.2 Checklist for a New Code Base 17
2.2.1 Use Git 18
2.2.2 Automate the Build 19
2.2.3 Turn On all Error Messages 24
2.3 Adding Checks to Existing Code Bases 29
2.3.1 Gradual Improvement 30
2.3.2 Hack Your Organisation 31
2.4 Conclusion 32
xii
Contents
Chapter 5 Encapsulation 87
5.1 Save the Data 87
5.1.1 The Transformation Priority Premise 88
5.1.2 Parametrised Test 89
5.1.3 Copy DTO to Domain Model 91
5.2 Validation 92
5.2.1 Bad Dates 93
5.2.2 Red Green Refactor 96
5.2.3 Natural Numbers 99
5.2.4 Postel’s Law 102
5.3 Protection of Invariants 105
5.3.1 Always Valid 106
5.4 Conclusion 108
xiii
Contents
xiv
Contents
xv
Contents
xvi
Contents
xvii
Contents
Bibliography 339
Index 349
xviii
Series Ed itor
Fo r ewor d
Yeah, software runs in the family. And, yeah, I’ve been programming for a
long, long time.
By the end of the lecture I was coding up the algorithm for multiplying two
binary integers, in PDP-8 assembly language. For those of you who aren’t
aware, the PDP-8 had no multiply instruction; you had to write an algorithm
xix
Series Editor Foreword
Anyway, it made me think of just how hard programming actually is. And it
is hard. It’s really hard. It may be the hardest thing that humans have ever
attempted.
Oh, I don’t mean it’s hard to write the code to calculate a bunch of prime
numbers, or a Fibonacci sequence, or a simple bubble sort. That’s not too
hard. But an Air Traffic Control system? A luggage management system? A bill
of materials system? Angry Birds? Now that’s hard. That’s really, really hard.
I’ve known Mark Seemann for quite a few years now. I don’t remember ever
actually meeting him. It may be that we have never actually been together in
the same room. But he and I have interacted quite a bit in professional
newsgroups and social networks. He’s one of my favourite people to disagree
with.
So when I saw this book, I thought about how much fun it was going to be to
read through and disagree with. And that’s exactly what happened. I read
through it. I disagreed with some things. And I had fun trying to find a way to
make my logic supersede his. I think I may have even succeeded in one or two
cases—in my head—maybe.
xx
Series Editor Foreword
But that’s not the point. The point is that software is hard; and much of the
last seven decades have been spent trying to find ways to make it a little bit
easier. What Mark has done in this book is to gather all the best ideas from
those seven decades and compile them in one place.
More than that, he has organized them into a set of heuristics and techniques,
and placed them in the order that you would execute them. Those heuristics
and techniques build on each other, helping you move from stage to stage
while developing a software project.
In fact, Mark develops a software project throughout the pages of this book,
while explaining each stage and the heuristics and techniques that benefit
that stage.
Mark uses C# (one of the things I disagree with ;-), but that’s not relevant.
The code is simple, and the heuristics and techniques are applicable to any
other language you might be using.
Some of these gems are profound, some are just idle mentions, others are
speculations, but all of them are examples of the deep insight that Mark has
acquired over the years.
So read this book. Read it carefully. Think through Mark’s impeccable logic.
Internalise these heuristics and techniques. Stop and consider the insightful
gems as they pop out at you. And just maybe, when it comes time for you to
lecture your grandchildren, you won’t scare the devil out of them.
—Robert C. Martin
xxi
This page intentionally left blank
P r eface
In the second half of the 2000s, I began doing technical reviews for a publisher.
After reviewing a handful of books, the editor contacted me about a book on
Dependency Injection.
The overture was a little odd. Usually, when they contacted me about a book,
it would already have an author and a table of contents. This time, however,
there was none of that. The editor just requested a phone call to discuss
whether the book’s subject matter was viable.
I thought about it for a few days and found the topic inspiring. At the same
time, I couldn’t see the need for an entire book. After all, the knowledge was
out there: blog posts, library documentation, magazine articles, even a few
books all touched on related topics.
On reflection, I realised that, while the information was all out there, it was
scattered, and used inconsistent and sometimes conflicting terminology.
There’d be value in collecting that knowledge and presenting it in a consistent
pattern language.
xxiii
Preface
After some years had gone by, I began to think about writing another book.
Not this one, but a book about some other topic. Then I had a third idea, and
a fourth, but not this one.
A decade went by, and I began to realise that when I consulted teams on
writing better code, I’d suggest practices that I’d learned from better minds
than mine. And again, I realised that most of that knowledge is already
available, but it’s scattered, and few people have explicitly connected the dots
into a coherent description of how to develop software.
Based on my experience with the first book, I know that there’s value in
collecting disparate information and presenting it in a consistent way. This
book is my attempt at creating such a package.
Wh o S h o u l d R e a d T h i s B o o k
This book is aimed at programmers with at least a few years of professional
experience. I expect readers to have suffered through a few bad software
development projects; to have experience with unmaintainable code. I also
expect readers seeking to improve.
1. If you’re curious about which books I mean, take a look at the bibliography.
xxiv
Preface
Prerequ isites
This isn’t a beginner’s book. While it deals with how to organise and structure
source code, it doesn’t cover the most basic details. I expect that you already
understand why indentation is helpful, why long methods are problematic,
that global variables are bad, and so on. I don’t expect you to have read Code
Complete [65], but I assume that you know of some of the basics covered there.
To the degree that I’m a software architect, I’m the latter kind. My expertise is
in how to organise source code so that it addresses long-term business goals. I
write about what I know, so to the degree this book is useful to architects, it
will be that type of architect.
O rg a n i s at i o n
While this is a book about methodologies, I’ve structured it around a code
example that runs throughout the book. I decided to do it that way in order to
make the reading experience more compelling than a typical ‘pattern
catalogue’. One consequence of this decision is that I introduce practices and
heuristics when they fit the ‘narrative’. This is also the order in which I
typically introduce the techniques when I coach teams.
xxv
Preface
If you want to use the book as a handbook, I’ve included an appendix with
a list of all the practices and information about where in the book you can
read more.
Once upon a time, Java code looked a lot like C# code. Modern C# code, on
the other hand, doesn’t look much like Java.
This doesn’t change the concepts presented in the book. Yes, in some
instances, a more succinct C#-specific alternative is possible, but that would
just imply that extra improvements are available.
To Var or Not to Va r
The var keyword was introduced to C# in 2007. It enables you to declare a
variable without explicitly stating its type. Instead, the compiler infers the type
from the context. To be clear, variables declared with var are exactly as
statically typed as variables declared with explicit types.
For a long time the use of this keyword was controversial, but most people
now use it; I do, too, but I occasionally encounter pockets of resistance.
While I use var professionally, writing code for a book is a slightly different
context. Under normal circumstances, an IDE isn’t far away. A modern
development environment can quickly tell you the type of an implicitly typed
variable, but a book can’t.
xxvi
Preface
I have, for that reason, occasionally chosen to explicitly type variables. Most
of the example code still uses the var keyword because it makes the code
shorter, and line width is limited in a printed book. In a few cases, though, I’ve
deliberately chosen to explicitly declare a variable’s type, in the hope that it
makes the code easier to understand when read in a book.
Code L istin gs
The majority of the code listings are taken from the same sample code base.
It’s a Git repository, and the code examples are taken from various stages of
development. Each such code listing includes a relative path to the file in
question. Part of that file path is a Git commit ID.
When you’ve done that, you can now explore the Restaurant.RestApi/
Program.cs file in its full, executable context.
A Not e o n t h e Bi b l io g r a p h y
The bibliography contains a mix of resources, including books, blog posts,
and video recordings. Many of my sources are online, so I have of course
supplied URLs. I’ve made an effort to mostly include resources that I have
reason to believe have a stable presence on the Internet.
Still, things change. If you’re reading this book in the future, and a URL has
become invalid, try an internet archive service. As I’m writing this, https://
archive.org is the best candidate, but that site could also be gone in the future.
Q u otin g Myself
Apart from other resources, the bibliography also includes a list of my own
work. I’m aware that, as far as making a case, quoting myself doesn’t
constitute a valid argument in itself.
xxvii
Preface
I’m not including my own work as a sleight of hand. Rather, I’m including
these resources for the reader who might be interested in more details. When
I cite myself, I do it because you may find an expanded argument, or a more
detailed code example, in the resource I point to.
Ac k now l e d g e me n t s
I’d like to thank my wife Cecilie for love and support during all the years we’ve
been together, and my children Linea and Jarl for staying out of trouble.
I’d also like to thank Adam Tornhill for his feedback on the section about his
work.
I’m indebted to Dan North for planting the phrase Code That Fits in Your
Head in my subconscious, which might have happened as early as 2011 [72].
Register your copy of Code That Fits in Your Head on the InformIT site
for convenient access to updates and/or corrections as they become
available. To start the registration process, go to informit.com/register
and log in or create an account. Enter the product ISBN
(9780137464401) and click Submit. Look on the Registered Products tab
for an Access Bonus Content link next to this product, and follow that
link to access any available bonus materials. If you would like to be
notified of exclusive offers on new editions and updates, please check the
box to receive email from us.
xxviii
Ab o ut t he
Aut ho r
xxix
This page intentionally left blank
T rou b l es h oot in g
12
Professional software development consists of more than feature development.
There are also meetings, time reports, compliance activities, and ... defects.
You run into errors and problems all the time. Your code doesn’t compile, the
software doesn’t do what it’s supposed to, it runs too slowly, et cetera.
The better you get at solving problems, the more productive you are. Most of
your troubleshooting skills may be based on “the shifting sands of individual
experience” [4], but there are techniques that you can apply.
12.1 U n d e r s ta n d i n g
The best advice I can think of is this:
235
Chapter 12 Troubleshooting
If you understand the code from the beginning, chances are that it’ll be easier
to troubleshoot.
236
12.1 Understanding
Don’t be intimidated by the term ‘scientific method’. You don’t have to don a
lab coat or design a randomised controlled double-blind trial. But do try to
come up with a falsifiable hypothesis. This might simply be a prediction, such
as “if I reboot the machine, the problem goes away,” or “if I call this function,
the return value will be 42.”
A typical experiment could be a unit test, with a hypothesis that if you run it,
it’ll fail. See subsection 12.2.1 for more details.
12.1.2 S im p lify
Consider if removing some code can make a problem go away.
The most common reaction to a problem is to add more code to address it.
The unspoken line of reasoning seems to be that the system ‘works’, and the
problem is just an aberration. Thus, the reasoning goes, if the problem is a
special case, it should be solved with more code to handle that special case.
This may occasionally be the case, but it’s more likely that the problem is a
manifestation of an underlying implementation error. You’d be surprised how
often you can solve problems by simplifying the code.
I’ve seen plenty of examples of such an ‘action bias’ in our industry. People
who solve problems I never have because I work hard to keep my code simple:
237
Chapter 12 Troubleshooting
I could go on.
The point is that a catchphrase like KISS2 is useless in itself, because how does
one keep things simple?
You often have to be smart to keep it simple3 , but look for simplicity anyway.
Consider if there’s a way you can solve the problem by deleting code.
12.1.3 R u bber D u ck i ng
Before we discuss some specific problem-solving practices, I want to share
some general techniques. It’s not unusual to be stuck on a problem. How do
you get unstuck?
If you don’t manage your time, you can be stuck with a problem for a long
time, so do manage your time. Time-box the process. For example, set aside 25
minutes to look at the problem. If, after the time is up, you’ve made no
progress, take a break.
238
12.1 Understanding
When you take a break, physically remove yourself from the computer.
Go get a cup of coffee. Something happens in your brain when you get
out of your chair and away from the screen. After a couple of minutes away
from the problem, you’ll likely begin to think about something else. Perhaps
you meet a colleague as you’re moving about. Perhaps you discover that the
coffee machine needs a refill. Whatever it is, it temporarily takes your mind off
the problem. That’s often enough to give you a fresh perspective.
I’ve lost count of the number of times I return to a problem after a stroll, only
to realise that I’ve been thinking about it the wrong way.
If walking about for a few minutes isn’t enough, try asking for help. If you have
a colleague to bother, do that.
I’ve experienced this often enough: I start explaining the problem, but halfway
in, I break off in mid-sentence: “Never mind, I’ve just gotten an idea!”
If you don’t have a colleague, you may try explaining the problem to a rubber
duck, such as the one shown in figure 12.1.
Figure 12.1 A rubber duck. Talk to it. It’ll solve your problems.
239
Chapter 12 Troubleshooting
And if realisation doesn’t come, I have a written question that I can publish.
12.2 D e f e c t s
I once started in a new job in a small software startup. I soon asked my
co-workers if they’d like to use test-driven development. They hadn’t used it
before, but they were keen on learning new things. After I’d shown them the
ropes, they decided that they liked it.
A few months after we’d adopted test-driven development, the CEO came by
to talk to me. He mentioned in passing that he’d noticed that since we’d
started using tests, defects in the wild had significantly dropped.
That still makes me proud to this day. The shift in quality was so dramatic that
the CEO had noticed. Not by running numbers or doing a complex analysis,
but simply because it was so significant that it called attention to itself.
You can reduce the number of defects, but you can’t eliminate them. But do
yourself a favour: don’t let them accumulate.
4. When that happens, I don’t succumb to the sunk cost fallacy. Even if I’ve spent time writing the question,
I usually delete it because I deem that it’s not, after all, of general interest.
240
12.2 Defects
When a bug appears, make it a priority to address it. Stop what you’re doing5
and fix the defect instead.
If, on the other hand, the test succeeds, the experiment failed. This means that
your hypothesis was wrong. You’ll need to revise it so that you can design a
new experiment. You may need to repeat this process more than once.
When you finally have a failing test, ‘all’ you have to do is to make it pass. This
can occasionally be difficult, but in my experience, it usually isn’t. The hard
part of addressing a defect is understanding and reproducing it.
I’ll show you an example from the online restaurant reservation system. While
I was doing some exploratory testing I noticed something odd when I updated
a reservation. Listing 12.1 shows an example of the issue. Can you spot the
problem?
The problem is that the email property holds the name, and vice versa. It
seems that I accidentally switched them around somewhere. That’s the initial
hypothesis, but it may take a little investigation to figure out where.
Have I not been following test-driven development? Then how could this
happen?
5. Isn’t it wonderful that with Git you can simply stash your current work?
241
Chapter 12 Troubleshooting
Listing 12.1 Updating a reservation with a PUT request. A defect is manifest in this interaction.
Can you spot it?
PUT /reservations/21b4fa1975064414bee402bbe09090ec HTTP/1.1
Content-Type: application/json
{
"at": "2022-03-02 19:45",
"email": "[email protected]",
"name": "Phil Anders",
"quantity": 2
}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": "21b4fa1975064414bee402bbe09090ec",
"at": "2022-03-02T19:45:00.0000000",
"email": "Phil Anders",
"name": "[email protected]",
"quantity": 2
}
Even so, you can still make mistakes even when the cyclomatic complexity is 1.
Listing 12.2 shows the offending code. Can you spot the problem?
Given that you already know what the problem is, you can probably guess that
the Reservation constructor expects the email argument before the name.
Since both parameters are declared as string, though, the compiler doesn’t
complain if you accidentally swap them. This is another example of stringly
typed code [3], which we should avoid7 .
242
12.2 Defects
Listing 12.2 The offending code fragment that causes the defect shown in listing 12.1. Can you
spot the programmer error?
(Restaurant/d7b74f1/Restaurant.RestApi/SqlReservationsRepository.cs)
using var rdr =
await cmd.ExecuteReaderAsync().ConfigureAwait(false);
if (!rdr.Read())
return null;
It’s easy enough to address the defect, but if I can make the mistake once, I can
make it again. Thus, I want to prevent a regression. Before fixing the code, write
a failing test that reproduces the bug. Listing 12.3 shows the test I wrote. It’s an
integration test that verifies that if you update a reservation in the database and
subsequently read it, you should receive a reservation equal to the one you
saved. That’s a reasonable expectation, and it reproduces the error because the
ReadReservation method swaps name and email, as shown in listing 12.2.
8. Proponents of object-relational mappers (ORMs) might argue that this makes the case for such a tool.
As I’ve stated elsewhere in this book, I consider ORMs a waste of time: they create more problems than
they solve. If you disagree, then feel free to skip this subsection.
243
Chapter 12 Troubleshooting
Assert.Equal(expected, actual);
}
The time it takes to execute a test suite matters, particularly for developer tests
that you continually run. When you refactor with the test suite as a safety net,
it doesn’t work if it takes half an hour to run all tests. When you follow the
Red Green Refactor process for test-driven development, it doesn’t work if
running the tests takes five minutes.
The maximum time for such a test suite should be ten seconds. If it’s much
longer than that, you’ll lose focus. You’ll be tempted to look at your email,
Twitter, or Facebook while the tests run.
You can easily eat into such a ten-second budget if you involve a database.
Therefore, move such tests to a second stage of tests. There are many ways you
can do this, but a pragmatic way is to simply create a second Visual Studio
244
12.2 Defects
solution to exist side-by-side with the day-to-day solution. When you do that,
remember to also update the build script to run this new solution instead, as
shown in listing 12.4.
Listing 12.4 Build script running all tests. The Build.sln file contains both unit and integration
tests that use the database. Compare with listing 4.2. (Restaurant/645186b/build.sh)
#!/usr/bin/env bash
dotnet test Build.sln --configuration Release
The Build.sln file contains the production code, the unit test code, as well as
integration tests that use the database. I do day-to-day work that doesn’t
involve the database in another Visual Studio solution called Restaurant.sln.
That solution only contains the production code and the unit tests, so running
all tests in that context is much faster.
The test in listing 12.3 is part of the integration test code, so only runs when I
run the build script, or if I explicitly choose to work in the Build.sln solution
instead of in Restaurant.sln. It’s sometimes practical to do that, if I need to
perform a refactoring that involves the database code.
I don’t want to go into too much detail about how the test in listing 12.3
works, because it’s specific to how .NET interacts with SQL Server. If you’re
interested in the details, they’re all available in the accompanying example
code base, but briefly, all the integration tests are adorned with a
[UseDatabase] attribute. This is a custom attribute that hooks into the
xUnit.net unit testing framework to run some code before and after each test
case. Thus, each test case is surrounded with behaviour like this:
1. Create a new database and run all DDL9 scripts against it.
2. Run the test.
3. Tear down the database.
9. Data Definition Language, typically a subset of SQL. See listing 4.18 for an example.
245
Chapter 12 Troubleshooting
Yes, each test creates a new database only to delete it again some milliseconds
later10 . That is slow, which is why you don’t want such tests to run all the time.
Defer slow tests to a second stage of your build pipeline. You can do it as
outlined above, or by defining new steps that only run on your Continuous
Integration server.
You peruse the application logs11 and finally figure it out. Overbooking is a
possible race condition. If a day is approaching capacity and two reservations
arrive simultaneously, the ReadReservations method might return the same
set of rows to both threads, indicating that a reservation is possible. As figure
12.2 shows, each thread determines that it can accept the reservation, so it
adds a new row to the table of reservations.
This is clearly a defect, so you should reproduce it with a test. The problem is,
however, that this behaviour isn’t deterministic. Automated tests are supposed
to be deterministic, aren’t they?
It is, indeed, best if tests are deterministic, but do entertain, for a moment, the
notion that nondeterminism may be acceptable. In which way could this be?
10. Whenever I explain this approach to integration testing with a database, I’m invariably met with the
reaction that one can, instead, test by rolling back transactions. Yes, except that this means that you
can’t test database transaction behaviour. Also, using transaction rollback may be faster, but have you
measured? I have, once, and found no significant difference. See also section 15.1 for my general position
on performance optimisation.
11. See subsection 13.2.1.
246
12.2 Defects
Tests can fail in two ways: A test may indicate a failure where none is;
this is called a false positive. A test may also fail to indicate an actual error;
this is called a false negative.
Listing 12.5 Apparently, there’s a bug in this code that allows overbooking. What could be the
problem? (Restaurant/dd05589/Restaurant.RestApi/ReservationsController.cs)
[HttpPost]
public async Task<ActionResult> Post(ReservationDto dto)
{
if (dto is null)
throw new ArgumentNullException(nameof(dto));
await Repository.Create(r).ConfigureAwait(false);
await PostOffice.EmailReservationCreated(r).ConfigureAwait(false);
return Reservation201Created(r);
}
Read
Reservations reservations Create OK
THREAD 1 TIME
DATABASE
THREAD 2
Read reservations Create OK
Reservations
Figure 12.2 A race condition between two threads (e.g. two HTTP clients) concurrently trying
to make a reservation.
False positives are problematic because they introduce noise, and thereby
decrease the signal-to-noise ratio of the test suite. If you have a test suite that
often fails for no apparent reason, you stop paying attention to it [31].
247
Chapter 12 Troubleshooting
False negatives aren’t quite as bad. Too many false negatives could decrease
your trust in a test suite, but they introduce no noise. Thus, at least, you know
that if a test suite is failing, there is a problem.
One way to deal with the race condition in the reservation system, then, is to
reproduce it as the non-deterministic test in listing 12.6.
This test method is only an orchestrator of the actual unit test. It keeps
running the PostTwoConcurrentLiminalReservations method in listing 12.7
for 30 seconds, over and over again, to see if it fails. The assumption, or hope,
is that if it can run for 30 seconds without failing, the system may actually
have the correct behaviour.
There’s no guarantee that this is the case. If the race condition is as scarce as
hen’s teeth, this test could produce a false negative. That’s not my experience,
though.
When I wrote this test, it only ran for a few seconds before failing. That gives
me some confidence that the 30-second timeout is a sufficiently safe margin,
but I admit that I’m guessing; it’s another example of the art of software
engineering.
It turned out that the system had the same bug when updating existing
reservations (as opposed to creating new ones), so I also wrote a similar test
for that case.
248
12.2 Defects
Listing 12.7 The actual test method orchestrated by the code in listing 12.6. It attempts to
post two concurrent reservations. The state of the system is that it’s almost sold out (the
capacity of the restaurant is ten, but nine seats are already reserved), so only one of those
reservations should be accepted.
(Restaurant/98ab6b5/Restaurant.RestApi.SqlIntegrationTests/ConcurrencyTests.cs)
private static async Task PostTwoConcurrentLiminalReservations(
DateTime date)
{
date = date.Date.AddHours(18.5);
using var service = new RestaurantService();
var initialResp =
await service.PostReservation(new ReservationDtoBuilder()
.WithDate(date)
.WithQuantity(9)
.Build());
initialResp.EnsureSuccessStatusCode();
These tests are examples of slow tests that ought to be included only as
second-stage tests as discussed in subsection 12.2.2.
There are various ways you can address a defect like the one discussed here.
You can reach for the Unit of Work [33] design pattern. You can also deal with
the issue at the architectural level, by introducing a durable queue with a
single-threaded writer that consumes the messages from it. In any case, you
need to serialise the reads and the writes involved in the operation.
249
Chapter 12 Troubleshooting
TransactionScope effectively serialises12 the reads and writes. That solves the
problem.
Listing 12.8 The critical part of the Post method is now surrounded with a
TransactionScope, which serialises the read and write methods. The highlighted code is new
compared to listing 12.5. (Restaurant/98ab6b5/Restaurant.RestApi/ReservationsController.cs)
using var scope = new TransactionScope(
TransactionScopeAsyncFlowOption.Enabled);
var reservations = await Repository
.ReadReservations(r.At)
.ConfigureAwait(false);
if (!MaitreD.WillAccept(DateTime.Now, reservations, r))
return NoTables500InternalServerError();
await Repository.Create(r).ConfigureAwait(false);
await PostOffice.EmailReservationCreated(r).ConfigureAwait(false);
scope.Complete();
12.3 B i s e c t i o n
Some defects can be elusive. When I developed the restaurant system I ran into
one that took me most of a day to understand. After wasting hours following
several false leads, I finally realised that I couldn’t crack the nut only by staring
long enough at the code. I had to use a method.
12. Serialisability, here, refers to making sure that database transactions behave as though they were
serialised one after another [55]. It has nothing to do with converting objects to and from JSON or
XML.
250
12.3 Bisection
Fortunately, such a method exists. We can call it bisection for lack of a better
word. In all its simplicity, it works like this:
You can use an automated test to detect the problem, or use some ad hoc way
to detect whether the problem is present or absent. The exact way you do this
doesn’t matter for the technique, but I find that an automated test is often the
easiest way to go about it, because of the repetition involved.
I often use this technique when I rubber duck by writing a question on Stack
Overflow. Good questions on Stack Overflow should come with a minimal
working example. In most cases I find that the process of producing the
minimal working example is so illuminating that I get unstuck before I have a
chance to post the question.
I’d added a secure resource to the REST API to list the schedule for a
particular day. A restaurant’s maître d’ can make a GET request against that
resource to see the schedule for the day, including all reservations and who
arrives when. The schedule includes names and emails of guests, so it
shouldn’t be available without authentication and authorisation13 .
This particular resource demands that a client presents a valid JSON Web
Token (JWT). I’d developed this security feature with test-driven development
and I had enough tests to feel safe.
13. For an example of what this looks like, see subsection 15.2.5.
251
Chapter 12 Troubleshooting
Then one day, as I was interacting with the deployed REST API, I could no
longer access this resource! I first thought that I’d supplied an invalid JWT, so
I wasted hours troubleshooting that. Dead end.
It finally dawned on me that this security feature had worked. I’d interacted
with the deployed REST API earlier and seen it work. At one time it worked,
and now it didn’t. In between these two known states a commit must have
introduced the defect. If I could identify that particular code change, I might
have a better chance of understanding the problem.
Unfortunately, there was some 130 commits between those two extremes.
Fortunately, I’d found an easy way to detect the problem, if given a commit.
This meant that I could use Git’s bisect feature to identify the exact commit
that caused the problem.
Git can run an automated bisection for you if you have an automated way to
detect the problem. Usually, you don’t. When you bisect, you’re looking for a
commit that introduced a defect that went unnoticed at the time. This means
that even if you have an automated test suite, the tests didn’t catch that bug.
For that reason, Git can also bisect your commits in an interactive session. You
start such a session with git bisect start, as shown in listing 12.9.
Listing 12.9 The start of a Git bisect session. I ran it from Bash, but you can run it in any shell
where you use Git. I’ve edited the terminal output by removing irrelevant data that Bash tends to
show, so that it fits on the page.
˜/Restaurant ((56a7092...))
$ git bisect start
˜/Restaurant ((56a7092...)|BISECTING)
This starts an interactive session, which you can tell from the Git integration
in Bash (it says BISECTING). If the current commit exhibits the defect you’re
investigating, you mark it as shown in listing 12.10
252
12.3 Bisection
˜/Restaurant ((56a7092...)|BISECTING)
If you don’t provide a commit ID, Git is going to assume that you meant the
current commit (in this case 56a7092).
You now tell it about a commit ID that you know is good. This is the other
extreme of the range of commits you’re investigating. Listing 12.11 shows how
that’s done.
Listing 12.11 Marking a commit as good in a bisect session. I’ve trimmed the output a little to
make it fit on the page.
$ git bisect good 58fc950
Bisecting: 75 revisions left to test after this (roughly 6 steps)
[3035c14...] Use InMemoryRestaurantDatabase in a test
˜/Restaurant ((3035c14...)|BISECTING)
Notice that Git is already telling you how many iterations to expect. You can
also see that it checked out a new commit (3035c14) for you. That’s the
half-way commit.
You now have to check whether or not the defect is present in this commit. You
can run an automated test, start the system, or any other way you’ve identified
to answer that question.
In my particular case, the half-way commit didn’t have the defect, so I told
Git, as shown in listing 12.12.
Listing 12.12 Marking the half-way commit as good in a bisect session. I’ve trimmed the output
a little to make it fit on the page.
$ git bisect good
Bisecting: 37 revisions left to test after this (roughly 5 steps)
[aa69259...] Delete Either API
˜/Restaurant ((aa69259...)|BISECTING)
253
Chapter 12 Troubleshooting
Again, Git estimates how many more steps are left and checks out a new
commit (aa69259).
Listing 12.13 Finding the commit responsible for the defect, using a Git bisect session.
$ git bisect bad
Bisecting: 18 revisions left to test after this (roughly 4 steps)
[75f3c56...] Delete redundant Test Data Builders
˜/Restaurant ((75f3c56...)|BISECTING)
$ git bisect good
Bisecting: 9 revisions left to test after this (roughly 3 steps)
[8f93562...] Extract WillAcceptUpdate helper method
˜/Restaurant ((8f93562...)|BISECTING)
$ git bisect good
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[1c6fae1...] Extract ConfigureClock helper method
˜/Restaurant ((1c6fae1...)|BISECTING)
$ git bisect good
Bisecting: 2 revisions left to test after this (roughly 1 step)
[8e1f1ce] Compact code
˜/Restaurant ((8e1f1ce...)|BISECTING)
$ git bisect good
Bisecting: 0 revisions left to test after this (roughly 1 step)
[2563131] Extract CreateTokenValidationParameters method
˜/Restaurant ((2563131...)|BISECTING)
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[fa0caeb...] Move Configure method up
˜/Restaurant ((fa0caeb...)|BISECTING)
$ git bisect good
2563131c2d06af8e48f1df2dccbf85e9fc8ddafc is the first bad commit
commit 2563131c2d06af8e48f1df2dccbf85e9fc8ddafc
Author: Mark Seemann <[email protected]>
Date: Wed Sep 16 07:15:12 2020 +0200
Restaurant.RestApi/Startup.cs | 32 +++++++++++++++++++-------------
1 file changed, 19 insertions(+), 13 deletions(-)
˜/Restaurant ((fa0caeb...)|BISECTING)
254
12.4 Conclusion
I repeated the process for each step, marking the commit as either good or
bad, depending on whether or not my verification step passed. This is shown
in listing 12.13.
After just eight iterations, Git found the commit responsible for the defect.
Notice that the last step tells you which commit is the ‘first bad commit’.
Once I saw the contents of the commit, I immediately knew what the problem
was and could easily fix it. I’m not going to tire you with a detailed description
of the error, or how I fixed it. If you’re interested, I wrote a blog post [101]
with all the details, and you can also peruse the Git repository that
accompanies the book.
The bottom line is that bisection is a potent technique for finding and isolating
the source of an error. You can use it with or without Git.
12.4 C o n c lu s i o n
There’s a significant degree of personal experience involved in
troubleshooting. I once worked in a team where a unit test failed on one
developer’s machine, while it passed on another programmer’s laptop. The
exact same test, the same code, the same Git commit.
We could have just shrugged and found a workaround, but we all knew that
making the symptom go away without understanding the root cause tends to
be a myopic strategy. The two developers worked together for maybe half an
hour to reduce the problem to a minimal working example. Essentially, it
boiled down to string comparison.
On the machine where the test failed, a comparison of strings would consider
"aa" less than "bb", and "bb" less than "cc". That seems fine, doesn’t it?
On the machine where the test succeeded, however, "bb" was still less than
"cc", but "aa" was greater than "bb". What’s going on?
At this point, I got involved, took one look at the repro and asked both
developers what their ‘default culture’ was. In .NET, the ‘default culture’ is an
255
Chapter 12 Troubleshooting
Ambient Context [25] that knows about culture-specific formatting rules, sort
order, and so on.
As I expected, the machine that considered "aa" greater than "bb" was
running with the Danish default culture, whereas the other machine used US
English. The Danish alphabet has three extra letters (Æ, Ø, and Å) after Z,
but the Å used to be spelled Aa in the old days, and since that spelling still
exists in proper nouns, the aa combination is considered to be equivalent to å.
Å being the last letter in the alphabet is considered greater than B.
It took me less than a minute to figure out what the problem was, because I’d
run into enough problems with Danish sort orders earlier in my career. That’s
still the shifting sands of individual experience—the art of software
engineering.
I’d never been able to identify the problem if my colleagues hadn’t first used a
methodology like bisection to reduce the problem to a simple symptom. Being
able to produce a minimal working example is a superpower in software
troubleshooting.
256
I n d ex
349
Index
350
Index
351
Index
352
Index
353
Index
team, 282 HTTP, 62, 92, 247, 325, redundant, 98, 187
warnings as errors, 25 326 removal, 237, 251
chef de cuisine, 3, 169 test, 324, 325 reuse, 40, 319
children’s book, 38 postcondition, 103 self-documenting, 161,
chunk, 115, 141, 262 SDK, 326 170
abstraction, 141, 148, cloud, 20, 21, 45, 295 shared, see code
149, 152, 175, 265 co-author, 190 ownership
code, 114, 151 coaching, xxv, 10, 88, 191 simplest possible, 52
hex flower, 312 code, see also production transformation, 88, 89,
pathways, 138, 152 code 119
short-term memory, 112, auto-generated, 25, 40, unfamiliar, 146
141 54, 72, 153 unmaintainable, xxiv
slot, 136, 148, 149 bad, 259–261 unsurprising, 310
Circuit Breaker, see design block, 139 code analysis
pattern complexity, 152 rule, 27, 28, 59, 66, 67, 75,
claim decomposition, 154, 139
role, 297, 298 155 static, 32, 57, 58, 75
class, 27 small, 134, 144, 257 driver, 53, 81
base, 122, 229, 230, 232, calling, 108, 214 false positive, 29, 60
315 dead, 7 like automated code
concrete, 213 defensive, 28, 108, 109 review, 28
declaration, 27, 268, 270, deletion, 7, 26, 27, 132, suppression, 67
310, 311 187, 238 turn on, 31
ephemeral, 19 warnings as errors, 29
delete, 221
high-level, 305 tool, 24
Domain Model, 145
high-quality, 153 code base
field, 108, 139, 153, 206
humane, 46, 174 example, see example
immutable, 265
imperfect, see also greenfield, 307
instance, 139
imperfection, 91, 123 memorise, 111
Humble Object, 123
incomplete, 204 table of contents, 314
immutable, 72, 107, 173
liability, 47 unfamiliar, 323
inheritance, 315 low-level, 154 Code Complete (book), 139,
instance, 75, 76 malicious, 126 288
member, 143, 150 metric, see metric code ownership, 187
instance, 67, 139, 142 minimal, 20 collective, 187–190, 194,
name, 163, 169, 316 multithreaded, 250 195, 197, 199
nested, 76, 232 network-facing, 293 weak, 199
private, 76 obscure, 43 code quality, see quality
sealed, 27 organisation, 43, 51, 52, code reading, 196, 197
clean code, 160 59 code review, 28, 189, 190,
Clean Code (book), 288 layer, 51 192–199, 285, 295,
client, 49, 111, 284 quality, see quality 308
API, 66 read more than written, big, 195
code, 156, 326 39, 44, 160 civilised, 197
concurrent, 183, 184 readable, 40, 41, 135, 196, initial, 193
external, 61 281 on-the-go, 190
354
Index
355
Index
356
Index
357
Index
358
Index
359
Index
360
Index
361
Index
362
Index
363
Index
364
Index
365
Index
366
Index
367
Index
368
Index
369
Index
370
Index
371
Index
372
Index
373
Index
374
Index
zero bugs, 240 zoom, 148, 149, 151, 152, example, 175, 265
zero tolerance, 25 154 navigation, 314, 317,
zone, see also flow, 42, 277, context, 314, 317, 324, 325
278 325 out, 265
375