Posts

Showing posts with the label Software Design

The Timeline of Errors

Image
As software developers, we see errors every day. They manifest as exceptions or segfaults or error codes, telling us that our code has gotten into a state we didn't expect. Their appearance often portends bugs. Though we groan at an unexpected stack trace, we should see an error as a form of automated feedback . Feedback can be fast or slow. We can put the discovery of errors onto a timeline. Errors appear at many points over the lifetime of the code, starting at the moment it is compiled. The further to the right that an error appears, the longer it takes for the feedback to appear. Compile time Compile time is the earliest we can receive feedback about an error. The compiler automatically does a number of checks to make sure the code makes some semblance of sense. The most powerful tool for compile-time feedback is the type-checker. The type-checker makes sure that only values of the expected type are passed around to the places that expect them. This guards ...

The Cone of Code Uncertainty

Image
As hurricanes approach the shores of North America, we are barraged with maps of possible tracks those storms can take. Those tracks start pretty close together, but diverge widely the further into the future they predict. Simplified, those storm tracks form a cone of uncertainty . A storm in the Atlantic could slide into the Gulf of Mexico and land anywhere from the Texas-Mexico border to Panama City Beach, FL. It all depends on a myriad of tiny factors to turn the storm this way or that. As time progresses, the storm moves along, and the cone of uncertainty shrinks. Its eventual location becomes clearer to predict, and we can more effectively target resources. Cone of Uncertainty for Hurricane Ike, source Much like hurricanes, running code lives in a cone of uncertainty. A function or method call is a mysterious black box until we have a result. If something goes wrong, we must delve into this cone of code uncertainty to find the problem. We use log statements and de...

Assuming Maintainability

Bugs are bad assumptions. Making assumptions explicit leads to more direct, easier to maintain code. Bugs are bad assumptions. Therefore, to reduce bugs and make code more maintainable, we need to make our assumptions as explicit as we can. We make our assumptions explicit in five ways: comments, tests, conditionals, assertions, and encapsulation with the type system. Each of these ways gives us progressively more assurance that our explicit assumptions are enforced in the code base. They also give us progressively earlier feedback when our code fails to live up to our assumptions. Comments Comments are the simplest way to express an assumption in the code. We write out, in plain language, what our assumption is. We can then read those assumptions quickly when need to use that piece of code. For example, we can state assumptions in method-level comments that describe our parameters. /** * Divide the dividend by the divisor. * @param dividend the number to d...

Assuming Debugging

Bugs are bad assumptions . Debugging is the process of testing our assumptions until we discover the invalid one(s). Once found, we can then correct those bad assumptions. The following code generates blog post author statistics for a popular blog site. The statistics show which days of the week authors are making posts. Most of the authors post every day, and extra on Fridays and Saturdays to capture the weekend audience. It has at least one bug. @GET @Path("/authors/{email}/weekly-stats") public UserWeeklyStats calculateWeeklyStats(@PathParam("email") final String email) { final User user = this.userService.byEmail(email); final Map<LocalDate, List<Post>> postsByDate = this.postService.thisWeekGroupedByDateFor(user.id()); if (postsByDate.isEmpty()) { return null; } final Map<DayOfWeek, List<Post>> postsByDayOfWeek = new HashMap<>(); postsByDate.forEach((date, post) -> post...

Assuming the Bugs

Writing code is all about making assumptions. Sometimes those assumptions are explicit, but more often we make those assumptions implicitly. We make assumptions about what kinds of parameters we will receive. We make assumptions about what kinds of values methods and functions will return. We make assumptions about the global state. The correctness of our code relies on the correctness of these assumptions. Bad assumptions can wreck our applicatons. We assumed that method never returns null, but turns out it does. Now we have an unexpected NullPointerException crashing through our stack. We assume that we will always average at least one element. Turns out someone wants to average zero elements. Boom, division by zero, ArithmeticException . We make so many assumptions through our codebases, at least a few are going to be wrong. Really, there are many, many assumptions that are wrong. These are bugs. These bad assumptions are all of our bugs. Usually, our assumptions are i...

Name Your Problems

Sometimes when developing code, I run into problems. Ok, a lot of the times I run into problems, but there's a certain kind of problem I run into pretty often. The problem is that I want a thing that doesn't exist. I am writing code, and it is just getting gnarlier and more tangled around a pile of logic. At some point, I hit a wall with it. It is too complicated to really reason about, and I don't want to make things any worse. I've found a trick to solving these problems, though. I take those problems and give them a name. Once I do that, I can get a handle on them, manipulate them, or even sequester them off into a corner. In almost every case, this drastically reduces the complexity back down to a reasonable level, and I often get the side benefit of offering extensibility I didn't know I needed. To demonstrate, let's say we are working on an e-commerce site. The company is based in Alabama, and sells to customers in all 50 states in the US. We have a met...

Magic Numbers, Semantics, and Compiler Errors

Image
Magic numbers are considered such a scourge upon code bases that there tools out there to automatically find them and root them out. After all, who knows what setValue(6); really means? There are also many approaches to giving better names to magic numbers. Not all of them are good, though. For example, we have a callback that sets a completion percentage on a status: void callback(Status status) { status.updateProgress(0, "Starting"); // Do some stuff... status.updateProgress(25, "Some stuff done"); // Do some more stuff status.updateProgress(50, "More stuff done"); // Reticulating splines status.updateProgress(75, "Splines reticulated"); // Validate everything status.updateProgress(100, "Complete"); } Those percentages would get flagged as magic numbers by the automatic tools. The developer would need to extract those out to constants to make that tool happy. A thoughtful developer would create ...

Typing != Complexity

Developers of all experience levels commonly share a false belief: more typing means more complexity. The most powerful idioms and tools often require a few extra keystrokes, but dramatically reduce complexity. The final keyword in Java is a bit of extra typing. The final keyword can be applied to variables, method parameters, instance members, static members, methods, and classes. By making these final, their values will not depend on their context. They will be known constants through their lifetime. Their constancy removes a large cognitive load. The developer no longer needs to worry if the value was not set or if it was set multiple times. Many developers forgo this benefits to avoid typing the extra keyword, to their own detriment. Later, they spend too much time chasing down defects caused by mutable state. Developers will often shun creating small types and interfaces because of the extra typing involved. Interfaces can abstract away many different implementations behind...

Respect Your Callers' Intentions

When we write code, we write it to be used. We call it from other places in our application. Others call it from places in their application. When we are writing code, we should show what we mean through our code, an idea known as intention-revealing interfaces . We reveal intentions through the external surface we expose. But what intentions are we exposing? We write our code in a way that we think will be useful, but we are mostly writing implementation. We are writing what the code should do, and then from there how others would use it. The implementation drives the interface. The implementation does not always have the right intentions, though. Here's a simple class that stores historic temperature data for processing: public class TemperatureHistory { private int[] temperatures; public TemperatureHistory(int[] temperatures) { this.temperatures = temperatures; } public int[] getTemperatures() { return temperatures; } } This is a simpl...

I'm Almost Always Wrong (The First Time)

When I write a chunk of code, it is wrong. I know it is wrong, even as I am writing it. But there's nothing I can do about it. I don't, and can't know how it is wrong when I am writing it. I can only know that I am writing wrong code. I am not phased in the least. As professionals, we are expected to write code with a certain quality of maintainability so that our code can be easily changed in the future. This is expected because of how often code does need to change, often in unexpected ways. We cannot predict all the ways the code needs to change. The future is inherently unpredictable. We must write our code in a way that makes changes as straightforward as possible. I don't worry about writing wrong code. I write it in a way that I can make right later. I design seams in places that seem most likely to change. I know them by the uncertainty that lives in them. I abstract and encapsulate that uncertainty behind methods and classes and interfaces so that later,...

When Technical Debt Costs You MORE!

We've all heard of the metaphor of technical debt by now. Maintaining poorly written software costs more in the long term, even if it saves in the short term. Thinking of this as a form of debt helps explain this in terms business-oriented stakeholders can understand. Metaphors are valuable because they can meaningfully convey a concept through a different context. They bridge the gap between two different ways of thinking. Sometimes we can take the metaphors a little too far, and they become disconnected from what they are actually trying to represent. This has happened with the metaphor of technical debt. Other times, we can explore the metaphor to find deeper relationships between the real concepts and the metaphorical ones. Financial debt has the concept of interest in it. Interest is an extra charge on top of the original value lent. It covers risk and provides incentive to the lender. It is calculated as a percentage on top of the principle amount of the loan. So a loan ...

Untangling Nested Code

Image
There is code out there that is ugly. I mean really ugly. I know, can you believe it? But it's true. One such example of really ugly code is code that has lots of deeply nested calls. The tests are trying to test a dozen different methods in every case. The methods look so simple with just a few lines and a few calls. And then those methods called have a few lines and a few calls. And then again, a dozen or more different calls deep. We want to use private helper methods to make our code clearer and cleaner. Calling methods is also largely what programming is about. But sometimes we overdo it. Deep call hierarchies are difficult to follow and they can mask what the code is really doing. Each call means that we have to look all over a class to see what actually happens. This makes debugging hard. This makes testing even harder. Say we have the following code: class DataLoader { void loadNationalData(Nation nation, int year, DataReader reader) { for (State state ...