- Consulting / Coaching
- Jeff’s Blog
Two Rules for Mobbing Success (25-Apr-2019)
In this blog post for Ranorex, I provide two key rules for making mob programming a successful and enjoyable way to work. You’ll figure out most of the rest of the guidelines for mobbing yourself: Things like respect for your teammates, guidelines about people coming and going, and so on. But these two rules–while simple to employ–aren’t necessarily obvious, and they’ll make all the difference in the world between a tedious, protracted mobbing session and something you’ll actually enjoy quite a bit.
To some seasoned developers, test-driven development (TDD) can initially seem like the dumbest thing ever. Once you’ve written a failing test, you are supposed to write only as much code as needed to make the test to pass. No speculation about what you think you need in the future–a week from now, an hour from now, or even ten minutes from now. Per Bob Martin’s three laws of TDD, write no more production code than “sufficient to pass the one failing unit test.”
“But I know I will want a hashmap in the next test or two, because I’ll have a bunch of keys and values…”
You’re a seasoned developer, so you’re probably right most of the time when you say you’ll need it. And yes, if following TDD, for now you must provide a simpler implementation. “That seems stupid; providing the simpler solution now means that I’ll be reworking it later to create the right one.”
As with most things in computing, TDD is a tradeoff. It trades off your current way of working–likely a code-test-fix cycle–with a test-code/fix cycle.
In a code-test-fix cycle, you write what you think the proper code is, you design and run tests (whether they are manual or automated), and you fix any problems that you discover. The duration of each step (code, test, and fix) usually varies dramatically, anywhere from minutes to hours (and sometimes longer). The tests you write usually cover a large subset of the behaviors in the code you just wrote. A perfect developer ends up with no fix cycle segment in code-test-fix (aka test-after development or TAD).
In a test-code/fix cycle, you define completion criteria for the code to be written in the form of automated tests. You write the code you think meets the behavior demonstrated in the tests, and fix any attempts that do not make the tests pass. You also clean the code. The duration of each step (test, code/fix, and refactor, also known as red-green-refactor in TDD) is fairly consistent and very short–ideally no more than 5 minutes. A perfect developer ends up writing code forthwith that passes the test.
A key distinction of test-code/fix, then, is that the test you write determines the scope of the code to write. Your goal is to code only logic for the behavior within the scope of this test. If your code is insufficient, the tests do not pass.
Any more code than specified by the tests falls outside the scope that they define. You can choose to write additional tests to describe (and vet) these “excess” behaviors, but you are now out of the TDD rhythm: Such tests will pass when you execute them.
You can of course choose not to write additional tests, in which case some unknown amount of the excess behaviors will be untested. It is a choice, but at this point you are no longer doing TDD, by definition.
So what if you’re not doing TDD? So what if you’re not testing everything? Breaking the TDD rhythm (by writing code in excess of the tests defined) carries the same ramifications as doing TAD in general, ones that you’ve already learned to accept as a seasoned developer.
Confidence in code correctness is but one reason to practice TDD, however. The tests TDD creates can also document the voluminous choices you make as a developer. When you choose to add behaviors without providing tests, you encode this behavior in way that is often not easily decoded: It can take a long time to uncover intent in the midst of any volume of code.
Well-designed tests can immediately impart the choices you make about the behaviors you designed into the code. They act as trustworthy documentation on the intended capabilities of the system.
Even with well-designed tests that document choices made in the code, you can produce code that resists easy comprehension. You’re not likely a perfect writer: When you first write anything (an article, an email, a blog post, a tweet, etc.), you often bloat it with unnecessary words, or create text that’s difficult to decipher. Good writing is a process of getting your thoughts down, then returning to edit these thoughts for clarity.
And so it is with code. Even if you excel at writing the correct (test-passing) code out of the gate, chances are good that it’s a little or even a lot messy. Perhaps it is code that others find difficult to understand. Perhaps it duplicates other concepts already in your system. Perhaps it violates your team standards. Perhaps it could be written more concisely (maybe using a construct you’re vaguely aware of, but you wanted to get the code working first). Perhaps you realize a slightly-better name for the variable you chose, particularly once you re-read the code to yourself.
Getting ideas down in some form, then cleaning them up, is how most of us do and should work. The realization of prose on paper, or code in an editor, makes it easier for you (and others) to see the messiness in all its glory. Once it’s in our face, we know that we should clean it up.
TDD builds this editing process into the cycle. Once you produce code that works, you can immediately and safely shape it into something that will help decrease the cost of its maintenance.
Back to test-after: Adding untested code reduces your confidence about making changes to that code. Consciously or otherwise, you will similarly reduce the amount of code editing you do.
Suppose you’re tasked with building a stock portfolio. Along with supporting the ability to purchase shares of symbols (e.g. AAPL or IBM), the portfolio should answer the number of distinct symbols.
You’re an experienced developer. “I’m going to create this hashmap now to capture the number of shares purchased for each stock symbol, because that’s the solution I’m going to end up with.”
If the only test you’ve written so far is around purchasing shares for a given stock symbol, the following potential tests pass as soon as you run them–if you even think to write them:
The immediately-passing tests put you out of the realm of TDD. So yes, to answer this section’s titular question, your speculative hashmap represents excess code.
As suggested earlier, most of the time you’re probably right about the generalities you think you need. When you’re right, it may seem like a waste of time to incrementally rework code (by starting with a specific solution and generalizing it a bit with each test). Still, the incremental solution keeps you on a rhythm, creates documentation for all intents in the system, prevents you from injecting defects, and allows you to keep the code clean incrementally.
Every once in a while, however, you aren’t right about the generalities needed. In the cases where you aren’t right, the incorrect and unneeded generality will cost you in the interim: The additional, unnecessary complexity can increase the effort required to read and maintain the code, over and over again across the lifetime of the system. (It can also raise questions about “why” and intent that can be hard to answer.) And when it comes time to support new behavior, it will usually take longer with an overly complex implementation than the simplest possible one.
You might view the incremental path that TDD promotes as a means of exploration. Seeking a simpler solution might open your mind up to other possibilities–things that you might not dream up if you race to the more comfortable, habitual solution that seems like it’s a foregone conclusion.
For the stock portfolio, a hashmap might seem like the proper projection, but it turns out that using a time series is better suited to historical data and can also result in simpler code.
It’s possible you’re claiming foul right about now: “If I had all the requirements up front for the portfolio, particularly ones around tracking purchase history and auditing, I might have come up with the best possible design.” Maybe. It’s also possible that your predisposition to certain kinds of solutions might have led you to a design that boxed you in to a constrained and inflexible solution.
A key goal of agile software development is to support and embrace change. With each iteration, a product owner can introduce new features–things never previously conceived. These interests can come about as a result of feedback from a number of events, including changes in the marketplace, changes to what a specific customer seeks, technology advances, and education regarding better techniques.
For example, no major U.S. airline carrier had supported baggage fees before 2008; it’s likely that few airlines had ever imagined them. When American Airlines introduced baggage fees in May of that year, the other carriers scrambled to incorporate a feature that their systems likely didn’t support so easily.
The TDD cycle in a sense is a microcosm of a well-executed agile process:
Both TDD and agile iterative development are feedback-driven: A key goal for each is the ability to change directions easily if new information demands it.
In agile, you don’t build support for features that the product owner doesn’t ask for. Similarly, to succeed with TDD, you must adhere to and trust a core rule of TDD: Once you’ve watched a test fail, you may write only the code necessary to make the tests pass.
I started doing TDD in 1999. I decided to tweet twenty random thoughts on TDD, roughly one per business day starting 2019 February 14, using the hashtag #20YearsTDD. Each tweet represents a good topic for an article that I should write (and might already have written).
Here are the tweets from #20YearsTDD. They appear in the order posted:
I contributed two chapters to Robert C. Martin’s best-selling book Clean Code: one named “Clean Classes,” and one chapter on emergent design. That doesn’t make me an authority on the definition of clean, but it does mean that at least one other person (Bob) agreed with my perspective on clean.
Actually, Bob edited my chapter on Clean Classes to put his stamp on it. That meant adding a few more ideas and bits of code. It also meant applying a different take on a few things, such as how methods should be organized within a file-based class declaration. In other words, Bob and I didn’t agree on everything.
We (Bob, the other Object Mentor authors of the book, and me) did have a general consensus on what “clean” meant, and it’s in line with most other folks who wrote books on (primarily OO) software design. There is consensus around core clean code concepts such as small classes and functions, intention-revealing names, eschewing comments in favor of code with higher clarity, and minimizing duplication. Clean Code promoted these concepts, which are considered good design because they result in systems considered easier to understand and maintain. (The book also promoted many stylistic preferences–things we could debate until the cows came home, and for which an efficiency/effectiveness winner is likely purely contextual.)
(Note that I’m not saying there aren’t a small number of authorities who counter these core concepts. Consensus does not imply unanimity.)
The challenge: Even if we knew that everything in Clean Code was the absolute, research-proven way to do things, it doesn’t mean that everyone can immediately function more effectively in code written this way. In fact, productivity can initially go down for a developer until they’ve made the paradigm shift–the light bulb moment when things
make sense, what Satir called the “transforming idea.”
Some people prefer longer functions (perhaps because they read more easily top-to-bottom), some people are ok with obscure names (perhaps because they’ve grown comfortable with them over the years in their codebase), some people still insist on comments that redundantly summarize the code, and some people find copy-paste-alter easier than touching existing code to introduce delegation. These preferences may represent “good enough for my current context.” But all of these preferences are for designs that are less than ideal.
My primary job as a technical coach is to help developers build software in a more effective manner. That means helping them take their messy software and shape it into something better. The cleanup involves Clean Code concepts, but also concepts from simple design, refactoring, design patterns, SOLID, The Pragmatic Programmer, and many other places. In general, these concepts do not contradict each other. They sometimes complement each other, they often overlap each other, but perhaps they are best looked at different perspectives toward the same end goal.
We do not disagree on what good design is, although it is often a matter of balance (everything in software is a tradeoff). Yes, short, single-responsibility functions are ideal for many reasons, but we can debate how we count the number of reasons a function has to change. And per simple design rule #4 (minimizing the number of moving parts), some people think it’s possible to go too far when it comes to “short.” But at least we agree that shorter is better most of the time.
The problem with the phrase most of the time is that it leads to a relativist mentality, which in turn provides fodder for excuses. People are good at convincing themselves they need not change their habits once given an opening. The excuses then lead to (you guessed it) crappy code, which is everywhere, almost an accepted conclusion for most people. And they know it’s crappy! They grouse about it, they fear touching it, they avoid it, and they break things unwittingly when they don’t fear it enough.
Part of being an effective coach means not overwhelming people. I hand-hold developers for a short while, gradually introducing new concepts to ease the transition toward more effective code looks like. For example, we might live with longer methods for the time being, until how they are better able to navigate the ravioli design. Or I won’t make a sweeping pass to delete all the doc-comments (e.g. Javadoc) until they’ve personally felt how unnecessary they are.
But I also ensure that the message is clear: The code in its current form is costing all of us. We have defects, late-night support calls, headaches, frustrated testers, slower rate of delivery, risk of losing customers, risk of going out of business, angry management, and risk of losing our job partly as a result of the crappy code we helped build. I talk about how and why clean code can improve things.
Feathers (not Michael) may be ruffled at the truth, but the nice thing about ruffling stiff feathers gently is you get a little air under them, which feels kind of cool.
To summarize: Contextual reality impacts our ability to create and work in a clean codebase. Individual experiences, team composition, existing codebases, and other factors all impact our ability to be clean. And yes, various perspectives on how to talk about clean exist. But the definition of clean–at least for OO software development–has been settled for a while.
Clean code ideals are not contextual.
One Thing At a Time (18-Feb-2019)
In this blog post for Ranorex, I present OTAAT: One Thing At A Time, one of the most effective habits you can adopt as a software developer. From tackling stories to designing tests to refactoring, doing more than OTAAT is a guaranteed way to waste time. I’ll talk about how OTAAT instead helps makes life easier in general.
With the goal of delivering quality software, I can help you with:
Want to hear more? Call 719-287-GEEK or use the Contact Me form to the left.