Book Chat: Growing Object-Oriented Software Guided By Tests

Growing Object-Oriented Software Guided By Tests is an early text on TDD. Since it was published in 2010, the code samples are fairly dated, but the essence of TDD is there to be expressed. So, you need to look past some of the specific listings since their choice of libraries (JUnit, jMock, and something called Window Licker I had never heard of) seem to have fallen out of favor. Instead, focus on the listings where they show all of the steps and how their code evolved through building out each individual item. It’s sort of as if you are engaged in pair programming with the book, in that you see the thought process and those intermediate steps that would never show up in a commit history, sort of like this old post on refactoring but with the code intermixed.

This would have been mind blowing stuff to me in 2010, however the march of time seems to have moved three of the five parts of the book into ‘correct but commonly known’ territory. The last two parts cover what people are still having trouble with when doing TDD.

Part 4 of the book really spoke to me. It is an anti-pattern listing describing ways they had seen TDD go off the rails and options for how to try to deal with each of those issues. Some of the anti-patterns were architectural like singletons, some were specific technical ideas like patterns for making test data, and some were more social in terms of how to write the tests to make the more readable or create better failure messages.

Part 5 covers some advanced topics like how to write tests for threads or asynchronous code. I haven’t had a chance to try the strategies they are showing but they do look better than the ways I had coped with these problems in the past. There is also an awesome appendix on how to write a hamcrest matcher which when I’ve had to do it in the past was more difficult to to do the first time than it would look.

Overall if you are doing TDD and are running into issues, checking out part 4 of this book could easily help you immediately. Reading parts 1 through 3 is still a great introduction to the topic if you aren’t already familiar. I didn’t have a good recommendation book on TDD before and while this isn’t amazing in all respects I would recommend it to someone looking to get started with the ideas.

Book Chat:The Mikado Method

The Mikado Method describes a way to discover how to accomplish a particular refactoring. The method itself asks that you first attempt to do what you want “naively” and identifying the problems with that approach. Then you roll the code base back to the original state in order to tackle one of those problems and iterate on the process until you can begin to resolve the problems in a bottom-up fashion, resulting in multiple small refactorings rather than one big one. This strategy means you can merge or push with the master branch more frequently since the codebase is regularly in a working state. This avoids the rabbit hole of making changes and more changes and never being sure how close you are to having compiling software with a passing test suite again.

The actual description of the technique and examples is only about 60 pages of the roughly 200 pages of the book; most of the rest is other tips and tricks for working on refactoring. There is also a rather long appendix on technical debt that I found expressed some ideas I had been thinking about recently; it describes four techniques for tackling the sources of technical debt.

The four techniques listed are absolve, resolve, solve, and dissolve. Absolve is essentially normalizing the practice and saying it is okay to do things this way. This would be something like lowering the automated test coverage necessary during a hard scheduled push. Resolve is reverting a change in the current processes and environments that had unintended negative effects. This would be something like getting rid of an internal bug bounty if it was being abused. Solving is changing the incentive schemes in order to put groups into alignment. For instance, having development teams on call in order to help align their incentives with the operations teams. Dissolving is the sort of radical solution that completely removes the friction between groups and makes the problem disappear completely. To continue with the previous example this would be a sort of devops culture where operations and developers are all on the same team and there is less distinction between the two. Each of these techniques could be applied to various means that create technical debt, or even to other sorts of problems.

The actual Mikado technique doesn’t seem book-worthy in the sense that it isn’t complex enough to warrant an entire book on it’s own. The other refactoring techniques weren’t anything particularly novel to people who are already familiar with Refactoring Legacy Code or other similar material. Overall it was a quick read and enjoyable but not the sort of thing I would be recommending to others strongly.

Theories of Technical Debt

There are a couple of different major causes of technical debt even on the best run projects.

  1. Schedule pressure
  2. Changed requirements
  3. Lack of understanding of the domain

You can choose to take on debt strategically to accommodate an aggressive schedule, you can accumulate debt from having things change, or you can collect debt from doing what appeared to be the right thing but turned out not to be once you learned more about the underlying situation. There are plenty of other reasons that technical debt can be acquired, but most of those can be avoided. Changing requirements can’t really be avoided; things can change, that’s the nature of life. Understanding of the domain is a tricky issue, since you can spend more time upfront to better understand the domain but you will still uncover new aspects as you go.

Schedule pressure is the most demanding of the three. You can consciously say that technical debt should be taken on by doing something the expedient way. You can also have implicit schedule pressure that pervades the decision making process. This sort of pervasive pressure causes people to value different things. If leadership discusses the schedule day in, day out, but doesn’t mention quality, it ends up lacking.

Technical debt is fundamentally a lack of quality; not in the defect sense but in the lack of craftsmanship sense. All of those 5,000 line classes got written by other engineers who were doing the best they could within the constraints of the environment at the time. But some engineers look at that and don’t want to touch it afraid of what it is and how hard it is to change. Some other engineers look at it and see a mountain to be climbed, or a wilderness to be civilized. The problem code is something to be taken and broken to your will. Each kind of engineer has a place in the development lifecycle.

If a company needs to hit a product window and is only full of the kind of engineers who see debt as a challenge to be dealt with they might not be able to make that tradeoff. If you only have the engineers who are concerned with maximum velocity but leave behind chaos in their wake you will have trouble as the codebase matures. Every engineer seems to have a range on the spectrum where they are most comfortable. Where in that range they land day to day seems to be controlled by the environment around them.

If you are surrounded by one side or the other you might lean towards that side of the range. The messages management sends out about the quality of software relative to the messages they send out about schedule of the project is another important factor. If they keep hammering home to get more done, people will find corners they think can get cut. What those corners are will differ between people, but they will take on debt in various corners of the codebase. This sort of debt is really insidious since it was never consciously decided on. If you decide that you will defer good error messages, or avoid building out an abstraction now, but you do it explicitly because of the schedule is the right business choice, then since the team discussed and decided to do it, they know as a whole that’s not the best technical solution but is the best business solution, whereas if someone just does it everyone else may not even be aware of the other options.

Fix vs Replace

I was thinking about when to fix a piece of software versus replace it as part of our normal software lifecycle, prompted by a discussion at work of a particular piece of software. The software in question works great, but nobody is confident that they are successful when changes do need to be made. Part of the lack of confidence is because changes are only made once or twice a year, so getting it set up and running locally and testing it is a chore, plus it turns into a complex process with many chances of failure. The other part is it runs on top of another library that nobody is really familiar with; the library isn’t used anywhere else in our stack, so nobody develops any familiarity with it. This particular piece of software is also critical to expanding our business, as it’s the primary way new client data is loaded into the system.

If we wanted to fix it, we could take the existing solid software and enhance it to make the setup and testing easier, or we could clean up the usage of the underlying library so it’s more intuitive. However, the decision was instead made to replace it with something totally new. I wasn’t involved in the decision, but became involved with the original piece of software after the fact to make a change while the replacement is still being developed. It seems like this software could be rehabilitated at first glance, but clearly someone else thought otherwise.

I know in general I’m biased towards fixing existing software. I’ve spent most of my career working on brownfield applications and building oddly shaped pegs to fit back into the oddly shaped holes of those applications. I think that I’ve done this because I enjoy it; building everything from scratch is almost too easy since there are so many fewer constraints involved. I get a different sort of satisfaction from it. I know I’m not the only one who has this particular tendency; the folks at Corgibytes are specializing in this sort of work. I’ve even been nostalgic for a codebase I’ve worked on – not the application but the codebase itself.

I feel like most organizations are biased towards replacing software because it lets you just say the entire thing is bad and try again instead of having to pick a particular thing to do or change. You don’t have to agree on what’s wrong with it, or get into details of what to change. This flexibility of scope leads to the quid pro quo rewrite where a piece of software is replaced with a new version that also contains a major new feature; this concept was introduced to me by Re-Engineering Legacy Software. Re-Engineering Legacy Software describes this as a bargaining tool to enable you to gain acceptance of a plan to do the rewrite, but I’ve always seen the business bring up the rewrite with the idea that they’re unhappy with the team’s ability to change a piece of software and this would clean up the underlying causes of the problems.

That’s the big problem: the current software has problems due to something. And unless you deal with whatever that “something” is, the new software probably is not going to be significantly better than the old. It may not have had the chance to become crufty yet, so it seems better when it’s new, but given a few years goes back to the same sorts of problems you had the last time. You need normal software processes that enable you to create and maintain quality software even as requirements change.

You need feedback into your initial processes of what caused the cruftiness to accumulate. This can be seen as a form of double-loop learning, where the feedback of what happened impacts how you see the world and thereby influences your decision-making process, not just the decisions themselves. If you are accumulating cruft because you put schedule pressure on the initial development resulting in a less modular design, the feedback to the decision-making process would be different than if you are accumulating cruft because the requirements changed radically. To make true long-term improvements, that’s the step you need to take, which sometimes might lead you to fix, and sometimes might lead you to replace.

NuGet Conversion Part 3

This is a follow up to my earlier posts about the NuGet conversion project I’ve been working on. Since my last post, my team put out the initial version of the package and updated the rest of the system to reference it. This magnified some of the other existing problems with the system; there is lots of app code leftover in some older parts of the system that makes it more difficult to understand if all of the dependencies are resolving in a way that will work correctly.

My team ended up running into some diamond dependency problems regarding versions of the newtonsoft.json serializer. There were three different major versions of newtonsoft.json in the system. One was the newest version in its major version line, which also happened to be the newest major version. The larger development team already had a couple of versions in the GAC which caused some complications, since the application would pick up a version from the GAC that was not necessarily the one you expected. Since some versions of newtonsoft.json do not maintain semantic versioning, combining that versioning issue with the app code resulted in code that indicated it was correct at compile time but was when the app code went to compile at runtime, failed. Unfortunately, the portions of the application using app code were all older, and also had few unit tests associated with them, if any.

When the larger development team was using ilmerge they had been instructing it not to merge the newtonsoft.json dll, because it was assumed to be in the GAC. My team had debated adding the newer versions of newtonsoft.json to the GAC to work around that deployment issue, but decided instead that we wanted to get away from using the GAC altogether, and this seemed like as good place to start as any. Unfortunately, that meant that we had to add literally hundreds of copies of the newtonsoft.json dll to source control, since it keeps a working copy of all of the various services and batch jobs. Previously the larger development team had been acting like every developer had the entire running system locally. The long term modularization effort will invalidate this assumption. However, in the short term, we need to let each team understand their own dependencies as they move out of the larger monolith.

As a part of this project, we ended up pushing the upgrade to the newest version of newtonsoft.json to most of the application, since as we converted everyone to start consuming our package, they took on the package dependency on newtonsoft.json. We also spotted an odd change to how xml documents got serialized to json. Previously the xml elements <foo/> and <foo></foo> serialized to the same json content of foo:null but now <foo></foo> serializes to foo:””. It’s not a big change, but it’s hard to tell what that sort of things this will impact, given the test coverage challenges in parts of the application.

My team seems to have hit the end of the road with this package. While we’ve got more packages to create, we expect most of them to be less complex, since they are only used in tens of projects rather than hundreds and they also don’t pull in other third party dependencies. The whole ordeal turned out to be much more complex than we thought. While we reap some tangible benefits – like being able to control the release cadence of the package – now most of the benefits are still further down the line. The intangible benefit is that we find and get to confront all of this technical debt we accrued over the years and do something to modernize the older portions of the application.

Actually About Refacotinrg

I wrote this little library recently to help generate some random dates. I made seven different commits to the project. I want to discuss what I did in each of them and the process to get from concept to completion.

In the first I knocked out a basic concept with a few tests, to show it probably  worked. It had one extension method, one helper class, and two tests. In the second commit I added another extension method with two more tests. In the third commit I added a third extension method and three more tests. This is all normal stuff, the fourth commit is where the interesting stuff starts.

In the fourth commit I refactored the before and after methods to reduce the duplication between them. That’s the obvious part that most people would do. I also refactored the tests that were generating a bunch of random dates to use a common helper method. This is the part that most people miss in Red-Green-Refactor – they refactor the business code but skip the test code.

The fifth commit refactored to allow chaining multiple conditions together. I put in an implicit conversion from the builder to DateTime. I was using the builder pattern, but for whatever reason I didn’t call it a builder at the time. So, I changed it to call it a builder  in the sixth commit along with updating it to allow me to generate multiple dates in the same range with one builder. Previously, the builder would only generate one date so the implicit conversion worked reasonably.

In the seventh commit I added overloads to the various extension methods for DateTime? as opposed to just DateTime. It was a little enhancement but it made it much nicer in some situations by pushing a bunch of null checking into the library itself.

There’s nothing revolutionary in either the software or the process but often times when talking about writing software, we talk in terms of  fully formed libraries springing forth from the mind and not about the arduous process that you go through to get there.