Eight Code Virtues (draft)
Some time ago, Jeff Langr and I came up with seven virtues for code in Agile in a Flash, and we wrote more about them over the years.
The original set is: Working, Unique, Simple, Clear, Easy, Developed, and Brief.
The virtues give us some words for what we like about good code, and they've been remarkably stable, with two exceptions:
1. I've added the 8th Virtue ("coherence")
2. I've dropped the ordering. Working is non-negotiable; the rest work in balance.
Since I've added an 8th and people have expressed interest, it seems prudent to produce a new, fully unified version of the list with more examples and suggestions to help people translate the rules into skills.
This write-up is for human consumption, of course, but there are notes and recommended readings that may help in other purposes.
Let's begin:
---
Evidence, Subjectivity, and Judgment
The virtues are not all subjective qualities.
Clear is subjective because clarity exists in the relationship between code and its intended readers. Code may be clear to one audience and obscure to another.
Easy is subjective because ease depends on the change being made and the people, tools, and system involved. The same design may make one change easy and another difficult.
The other virtues are observable and can be objectively evidenced.
- We can observe whether the software works.
- We can determine whether a single piece of knowledge has multiple representations.
- We can count operands, operations, and paths.
- We can see whether a concept is represented in the program or is repeatedly reconstructed from primitives.
- We can identify expressions that add no information or behaviour.
- We can compare the vocabulary, concepts, patterns, and representations used across a system and see whether they reinforce or contradict one another.
Objective does not mean obvious, trivial to measure, or reducible to a single score. After all, determining whether two different passages represent the same knowledge may require investigation. Recognising the underlying concept within a group of variables may require domain knowledge. Evidence can be incomplete, and our initial interpretation can be wrong.
An observation tells us that a virtue is under pressure. It does not tell us which refactoring to perform. Several changes may address the same problem, and each may affect the other virtues differently. Choosing among them requires taste, judgment, and knowledge of the system.
Of course, not every possible change is a wanted change. Some remedies are worse than the diseases! Any proposed change has to pass "the improvement test."
Does it preserve Working?
Given that it works, is the resulting code really better when we consider Unique, Simple, Clear, Easy, Developed, Brief, and Coherent together?
The virtues help us observe and describe the code. They give us criteria for comparing the result. They do not remove judgment from the work between those two points, nor do they tell us precisely what to do.
Programming, after all, is still a truly human practice even when AI-aided.
Working
The code has to work.
Working is different from all the other virtues because it isn’t a matter of design taste. We may accept a little duplication to keep something clear, write a few extra lines to make a change easier, or decide that a useful abstraction is worth some additional machinery.
We don’t make that kind of trade when it comes to Working.
Of course, “working” has to mean more than looking plausible. Code that compiled yesterday, passed a test once, or seems obvious to the person who wrote it doesn’t give us much assurance.
Tests provide repeatable, automated evidence. This is especially valuable when we are changing the design to accommodate new capabilities or to refactor it into better shapes. We also learn from demonstrations, experiments, monitoring, and actually using the software.
If the code has fast and useful tests, we run them before and after changes.
If there are no useful tests, we have a different problem. We may need to characterise the existing behaviour, find a seam, or make some careful preliminary change so that the important behaviour can be observed.
Refactoring means changing the structure without changing the behaviour. Without evidence about the behaviour, “refactoring” can become wishful and dangerous rewriting.
We rely on automated tests as evidence that the code works. When the tests are not sufficient to give us confidence, we add more or better tests. It's often the only feasible option, especially in the agentic age.
Unique
Unique is the Single Point of Truth virtue: each piece of knowledge should have one authoritative representation in the system.
This is easily mistaken for a prohibition against repeated text. Two identical values may represent different facts that happen to be equal-valued today. If they can change independently, combining them would couple things that have no business being coupled.
Duplication also isn’t always textual. The same rule might be expressed as a calculation in one place, a conditional in another, and a comment somewhere else. The code looks different, but the knowledge is repeated.
Suppose valid payment methods appear as an enum in the domain, a list in the user interface, strings in configuration, branches in validation, mappings in serialisation, and fixtures in tests. No two of those passages look alike, but they represent the same knowledge.
When we find simple repetition, we ask what it represents. Would these things have to change together because they express the same fact or rule? If so, the system probably wants one authoritative representation with the others derived from it.
Version history can help. Things that repeatedly change together may be telling us that some knowledge has been scattered. It isn’t proof, but it is a useful heuristic.
Text searches and duplication detectors are useful too, as long as we remember their limitations.
Simple
Simple is a structural property of code.
It is the number of operands, operations, and possible execution paths.
Operands are the values and entities the code has to coordinate. Operations are the things it does with them. Paths are the different routes execution can take through the code.
Given two implementations of the same behaviour at the same scope, the one with fewer operands, operations, and paths is simpler by this definition.
This isn’t a matter of familiarity, elegance, fashion, or preferred patterns. A solution does not become simple because it uses only elementary language features, nor because it follows a familiar design pattern. Those choices may make it Clear or Coherent to a particular audience, but Simple has a more literal meaning.
A long procedural solution built from assignments, loops, and conditionals may be familiar and still contain many operands, operations, and paths.
A table can replace a long conditional chain. The table may contain the same cases, but the program no longer has a separately constructed decision path for each case. It performs one lookup using data.
A collection can replace item1, item2, item3, and the operations required to coordinate them separately.
A state can replace several booleans and all the logic required to prevent or interpret invalid combinations.
A named type can replace a group of primitive operands repeatedly passed and manipulated together.
Removing a dead branch literally removes a path. Removing a repeated calculation literally removes operations. Replacing several coordinated values with a single representation literally reduces the number of operands.
Extracting a function does not necessarily make the program simpler overall. If it merely moves the same operations and paths elsewhere, the overall structure remains unchanged, but the local structure becomes simpler. The extracted function may well be simpler and more focused.
Patterns and abstractions do not automatically simplify code either. They introduce their own types, calls, relationships, and paths. They are worthwhile when the structure they remove exceeds the structure they add.
Simple code contains less machinery for producing the same behaviour.
Count the operands.
Count the operations.
Count the paths.
Reduce them.
Clear
Clear code communicates well with its intended readers. For any code base, the maintainers are the true, non-hypothetical readers. The author is one of the readers, but all code is clear to the author when it's written. The importance of this virtue is that the code is also clear to non-authors in the code's audience.
Those readers are not imaginary beginners who know nothing about programming, the language, the domain, or the system.
That makes clarity contextual. It is subjective to the group, not merely to any one individual in it.
Language idioms can improve clarity for people who know the language. Domain terms can say a great deal to people who understand the domain. Local conventions reduce the number of surprises in a codebase.
Code tends to be clear to an audience when it follows their local conventions and patterns. Thankfully, agents are pretty good at following patterns.
Clarity is also more than explaining mechanics. A comment can tell us what five lines of code do, but a better name or representation might tell us why that operation exists and how it belongs in the system.
Easy
Easy code is easy to change.
Clear code and easy code often occur together, but they aren’t the same thing. Some code is perfectly understandable and still makes an ordinary change unnecessarily difficult.
One of the best times to tend to Easy is just before adding a feature to existing code.
When we know what change we are about to make, we don’t have to speculate about what the system might someday need. We can see where the new behaviour would have to be threaded through awkward conditionals, duplicated policies, primitive data, or responsibilities with no obvious home.
Before adding the feature, we can refactor the existing code so the change fits naturally. Then we make the now-easy change. This was observed some time ago by Kent Beck (you likely recognize the origin).
Speculative preparation is different.
If we change the design to accommodate a possible future feature and that feature never arrives, we have added structure without any benefit.
Worse, we may have prepared the code for the wrong future. The real changes that arrive later may cut across the extension points, abstractions, and divisions we introduced. Instead of making those changes easier, our speculation has put additional structure in their way.
Consider a long if/then/else chain. Adding one more case means finding the right place in the chain, preserving its order, threading another block through the indentation, and checking its interactions with the surrounding conditions.
Perhaps those decisions would be better represented as a table.
The table may be considerably smaller, but brevity isn’t the only improvement. Adding another case is a straightforward addition of one row rather than careful surgery on the existing control flow. The representation has made the likely change easier.
A complicated condition that appears in several places creates a similar problem. If the policy changes, every copy must be found and changed correctly. Giving the condition a name and encapsulating it in a single function provides a single place for the policy to live and a single place to change it.
Repeated constants are another version of the same problem.
Suppose a feature allows “up to 3” of something, and the number 3is embedded throughout the code. When the policy changes from "up to 3" to “up to 10,” we must find every instance of "3 ” in the codebase and determine whether it refers to the same underlying fact. If we leave the new value inline as several 10s, we will repeat the exercise when the limit becomes 12.
A named constant or policy object gives the value a meaning and a home. We change the policy rather than search for numbers.
Code can also be difficult to change because it makes us reason through decisions that no longer exist.
A defunct conditional still asks the reader to understand both branches, determine which one can execute, and preserve the surrounding structure. A feature flag left behind after a feature has been permanently enabled is a classic example, leaving one live branch and one dead branch. The maintainer has to thread through the flag and its conditionals to discover that only one path is live.
Version control history can reveal other obstacles. Files that repeatedly change together, recurring repair commits, bugs in the same area, and changes that scatter across the system are evidence worth examining. They may reveal knowledge with no clear home or responsibilities divided along the wrong boundaries.
These improvements rarely affect Easy alone. The table may make the code Simple and Brief. Extracting the repeated condition or policy may make it Unique and Clear. Removing dead choices reduces the number of paths and makes the remaining design more Coherent.
The code becomes easier because it represents today’s decisions directly and gives the real change an obvious place to go.
Developed
Programs begin with what the language provides: strings, numbers, booleans, lists, maps, functions, classes, records, and so on.
As we learn about the problem, the program should develop concepts of its own.
Say we notice that several variables tend to travel together. They appear together in argument lists, an are manipulated inside loops and conditional branches. Several functions accept only those variables, or repeatedly pass the whole group onward.
That suggests that the code may be overly primitive. There is likely an undeclared data type hidden in that group of variables—something that should have a name, a meaning, and a home in the code.
Once we identify the missing type, the surrounding operations become interesting as well. Functions that work only with that group are likely operations of that type. A function that accepts the group twice may be an operation involving two instances of the type.
The point isn’t to move functions into classes for the sake of having classes, but to recognise that the program repeatedly assembles and operates on a concept without having represented it.
Adding a new type may improve several virtues together. Argument lists become shorter. Repeated manipulation disappears. Operations have an obvious home. The code speaks more directly to the problem domain and offers fewer opportunities for callers to mishandle values.
The result may be more Unique, Simple, Clear, Easy, Brief, and Coherent because it has become more Developed.
When a concept keeps reassembling itself throughout the code, the program is asking us to give it a name.
Brief
Brief code has a high signal-to-noise ratio.
This isn’t about having the fewest characters or lines. Cryptic code may be brief, but reads like some kind of puzzle. We don't need changes that make the code shorter at the expense of clarity, easy change, simplicity, uniqueness, etc.
Given the same behaviour and information, the implementation containing less expression is briefer.
On the other hand, a filter or list comprehension can do the same job that loops, conditional statements, and collector arrays do in a fraction of the space. In fact, this denser representation can often be comprehended at a glance, whereas longer expressions require more work to read and understand.
A shorter expression of the same idea may be quicker and easier to read, and may actually improve many other virtues in the codebase.
A small function can replace a repeated explanation. A better representation can make whole categories of plumbing disappear.
Brevity deserves its place in the virtues. Don't discount it or confuse it with "code golf."
Coherent
Coherence is the degree to which the concepts, vocabulary, abstractions, architecture, patterns, and representations of a system reinforce one another.
In a coherent system, learning compounds. Once we understand how one part of the system represents an idea, that knowledge helps us elsewhere. Names are used consistently. Similar relationships are expressed in similar ways. New work tends to strengthen the system's existing language rather than invent another local dialect.
This goes closely with Developed.
Suppose one module represents a customer category with an enum, another with strings, another with subclasses, and another with several booleans. Those modules may genuinely need different models. They may also be four dialects for the same concept.
We can compare their meanings and boundaries. If they represent the same knowledge, one representation may deserve to become part of the system’s language. If they represent different views in different contexts, forcing them together would reduce coherence rather than improve it.
A collection of flags may be an unnamed state. Several primitive values passed together may be an unnamed concept. A long series of conditional decisions may be knowledge that would be better represented as a table, rule set, map, graph, or state machine.
A better representation often improves several virtues at once. It may eliminate duplicated knowledge, remove branches, give an idea a useful name, make a likely change local, and reduce the amount of code we need.
That is more interesting than improving one virtue in isolation.
Coherence doesn’t require everything to look the same. Different problems deserve different representations. A system in which everything is forced through a single approved pattern may be consistent yet make very little sense.
Before adding a new abstraction, it is worth looking around. How does the rest of this system express similar ideas? Is there already a useful name, type, convention, or structure? Would the new design extend the system's language or introduce another way to say the same thing?
The parts should fit because they belong together.
Comments and the Virtues
Comments are interesting because they are not code. They aren’t executable, and adding one doesn't change the program's structure or behaviour.
A comment can prime a reader. This may be useful when the code contains a difficult algorithm, a surprising constraint, an unavoidable workaround, or a choice whose rationale cannot be expressed in the programming language. A link to the source of an algorithm may tell us something the implementation cannot. It can warn us against obvious-but-wrong implementation choices.
Comments can also hide design problems.
I use three rules:
- Comments are for things that cannot be expressed in code.
- Comments that restate the code must be deleted.
- If a comment says what the code could say, change the code to make the comment redundant, then delete the comment.
Suppose a comment tells us that the next ten lines obtain the active accounts. Perhaps those lines should become a function named active_accounts. If a comment tells us that x is the loan principal, perhaps x should be renamed. If it explains the positions in a tuple, perhaps the program needs a structure with named elements.
The comment has shown us something the program knows but does not yet represent very well.
This interacts with several virtues. Moving information from a comment into the code gives it a single executable representation, improving Unique. A good name or structure may make the code Clear, Simple, Developed, and Brief. Using the vocabulary already present in the system can make it more Coherent. If the new representation gives a concept an obvious home, the code may become Easy to change as well.
That doesn’t mean we should strip the comments from difficult code and declare victory. Comments are sometimes an indispensable coping mechanism. Removing them before improving the code leaves us with the same difficult program and less help understanding it.
When learning unfamiliar code, I may add comments as I work out what it does. Those comments externalize my developing understanding. Then I try to make them unnecessary through naming, extraction, and better representation.
Some comments survive because they contain information the code cannot carry. The rest act like "refactor here" signs.
From Virtues to Refactoring
A smell, a metric, an awkward change, or a surprising search result gives us a reason to investigate. It does not give us an order.
Suppose we notice that a change touches several conditionals, constants, and tests. That is an observation. It may suggest duplicated knowledge, a missing concept, or an awkward representation. Those are interpretations, and we should check them.
Start with the code involved in the real change. Search for its names and related terms. Find the constants, enums, branches, data structures, and tests that use them. Look for values that travel together and functions that work on the same groups of values. Examine nearby code to learn the vocabulary and representations the system already uses. When it is helpful, inspect the version history to see what has actually changed together.
Comments deserve inspection too:
- Does the comment merely restate nearby code?
- Is it inaccurate or obsolete?
- Could its information become a name, function, type, structure, or test?
- Does it record a source, constraint, rationale, or rejected alternative that the code cannot express?
- Is it temporary scaffolding that should guide a refactoring before being removed?
Then ask whether the concept has one owner.
If it doesn’t, consider the smallest representation that would give it one. This might be an extracted function, a collection, a table, a type, or a move to an existing module. We don’t have to leap directly from scattered primitives to an architecture.
Before making the change, separate what has been observed from what has been inferred. Explain which virtues are under pressure, what the proposed change improves, and what it might make worse.
Then work in small, behaviour-preserving steps. Keep the tests passing. Make one structural change at a time and commit it separately from unrelated behaviour changes. If the safety evidence is weak, improve it before attempting a broad refactoring.
Afterwards, look at the result rather than merely confirming that the intended refactoring was performed.
Does the software still work?
Is the knowledge represented in fewer places?
Are there fewer operands, operations, or paths?
Will the actual change be easier to make?
Has a previously primitive concept gained a name and a home?
Has noise been removed without hiding useful information?
Does the solution reinforce the language and representations of the system?
Will its intended readers understand it?
Most importantly: is the code better overall?
If not, revert it or try something else.
There is no virtue in completing a refactoring that leaves the software worse.
The Improvement Test
The virtues aren’t independent quantities, but virtues constrain one another.
Code always has to work. That's the given. Without that, nothing else is interesting.
Given working code, we consider Unique, Simple, Clear, Easy, Developed, Brief, and Coherent together. The better changes are those that improve several aspects at once, often by finding a representation that better expresses either the solution domain or the problem domain.
Sometimes there is a tradeoff. We may accept a little more structure because it makes an important concept clearer and gives a real change an obvious home.
Sometimes the code is better left alone.
The virtues give us language to investigate and discuss those choices. They don’t make the choices for us.
Resources
These are useful starting points for anyone who wants to take the virtues further or turn them into a practical refactoring skill.
The Virtues
Tim Ottinger and Jeff Langr’s How Virtuous Is Your Code? is the original article introducing the seven virtues.
Tim Ottinger’s 7 Code Virtues Explained gives the fuller explanation of the original seven virtues and their history in Agile in a Flash.
Tim Ottinger’s Time for an 8th Virtue: Coherence introduces Coherent and the revised relationship: Working is prime; the remaining virtues are peers.
Anthony Sciamanna and Tim Ottinger’s SPOT and Coincidental Duplication explores Unique in more depth, especially the distinction between repeated text, duplicated knowledge, and accidental similarity.
Tim Ottinger’s Simple v. Complicated develops the objective meaning of Simple in terms of operations, operands, and paths.
Tim Ottinger’s Rethinking Readability treats readability as a relationship between code and its actual audience, rather than as an intrinsic property that can be measured without context.
Tim Ottinger’s Meaningful Names Revisited connects naming with context, extraction, design, and representation.
Comments
Tim Ottinger and Jeff Langr’s Rules for Commenting is the original Agile in a Flash card on comments, additional information, and removing comments by improving the code.
Tim Ottinger’s Three Rules for Code Comments adds an important qualification: comments can be a necessary survival mechanism in complicated code, so deleting them before improving the code makes matters worse.
Stephen D. Ritchie’s Rules for Commenting Code at Ruthlessly Helpful explains the three rules with examples.
Tim Ottinger’s Code comments are not code; they’re not executable distinguishes making code readable from priming someone to read difficult code.
Tim Ottinger’s Apologize in Code is an earlier statement of the relationship among comments, naming, parameters, and design.
Jaime Buelta’s Commenting the Code discusses the rules and preserves Tim Ottinger’s responses to them.
Refactoring Safely
Martin Fowler’s Refactoring and the online Catalog of Refactorings provide a vocabulary and mechanics for small, behavior-preserving structural changes.
Michael Feathers' Working Effectively with Legacy Code is especially useful when Working cannot yet be demonstrated. It covers characterization tests, seams, dependency breaking, and getting difficult code under test.
Tim Ottinger’s TDD: Purposes and Practices connects working evidence, incremental development, iterative design, refactoring, and continuous integration.
Tim Ottinger’s What’s This About Micro-commits? describes a useful execution discipline: one intention at a time, tests passing, and structural changes kept separate from behavioral changes.
Developing Representations
Joshua Kerievsky’s Refactoring to Patterns presents patterns as possible destinations reached through evolutionary refactoring, rather than structures to install before the need for them appears.
Eric Evans' Domain-Driven Design Reference provides useful language for concepts, models, bounded contexts, and the relationship between the problem domain and its representation in software.
Martin Fowler’s Ubiquitous Language is a compact introduction to building a shared, rigorous language around a domain model.
Ward Cunningham’s Debt Metaphor explains why we revise software as our understanding improves, bringing what we have learned back into the code.
Agentic Application
- Tim Ottinger’s Gilded Rose with AI and PyCharm is one account of using an agent for testing and refactoring while constraining the work with existing code, tests, human judgment, and microcommits.
A refactoring skill derived from these materials should help an agent gather evidence, recognise pressures, propose small changes, preserve behaviour, and explain tradeoffs.
It should not merely teach the agent to chase smells.
Comments
Post a Comment