Table of contents
- Chapter 1 - Clean Code
- Chapter 2 - Meaningful Names
- Chapter 3 - Functions
- Chapter 4 - Comments
- Chapter 5 - Formatting
- Chapter 6 - Objects and Data Structures
- Chapter 7 - Error Handling
- Chapter 8 - Boundaries
- Chapter 9 - Unit Tests
- Chapter 10 - Classes
- Chapter 11 - Systems
- Chapter 12 - Emergence
- Chapter 13 - Concurrency
- Chapter 14 - Successive Refinement
- Chapter 15 - JUnit Internals
- Chapter 16 - Refactoring SerialDate
- Chapter 17 - Smells and Heuristics
Chapter 1 - Clean Code
Code is really the language in which we ultimately express the requirements. We may create languages that are closer to the requirements. We may create tools that help us parse and assemble those requirements into formal structures. But we will never eliminate necessary precision—so there will always be code.
You will not make the deadline by making a mess. Indeed, the mess will slow you down instantly and will force you to miss the deadline. The only way to make the deadline—the only way to go fast—is to keep the code as clean as possible at all times.
Writing clean code is a lot like painting a picture. Most of us know when a picture is painted well or badly. But being able to recognize good art from bad does not mean that we know how to paint. So too being able to recognize clean code from dirty code does not mean that we know how to write clean code!
Grady Booch, author of Object Oriented Analysis and Design with Applications says : "Clean code is simple and direct. Clean code reads like well-written prose. Clean code never obscures the designer’s intent but rather is full of crisp abstractions and straightforward lines of control".
Dave Thomas, founder of OTI, the godfather of the Eclipse strategy says : "Clean code can be read, and enhanced by a developer other than its original author. It has unit and acceptance tests. It has meaningful names. It provides one way rather than many ways for doing one thing. It has minimal dependencies, which are explicitly defined, and provides a clear and minimal API. Code should be literate since depending on the language, not all necessary information can be expressed clearly in code alone".
- Michael Feathers, author of Working Effectively with Legacy Code says : "I could list all of the qualities that I notice in clean code, but there is one overarching quality that leads to all of them. Clean code always looks like it was written by someone who cares. There is nothing obvious that you can do to make it better. All of those things were thought about by the code’s author, and if you try to imagine improvements, you’re led back to where you are, sitting in appreciation of the code someone left for you—code left by someone who cares deeply about the craft".
- Ward Cunningham, inventor of Wiki, inventor of Fit, coinventor of eXtreme Programming says : "Motive force behind Design Patterns. Smalltalk and OO thought leader. The godfather of all those who care about code. You know you are working on clean code when each routine you read turns out to be pretty much what you expected. You can call it beautiful code when the code also makes it look like the language was made for the problem".
Chapter 2 - Meaningful Names
Take care with your names and change them when you find better ones. Everyone who reads your code (including you) will be happier if you do.
The name of a variable, function, or class, should answer all the big questions. It should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent.
Good example :
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
Programmers must avoid leaving false clues that obscure the meaning of code. We should avoid words whose entrenched meanings vary from our intended meaning. For example,
hp
,aix
, andsco
would be poor variable names because they are the names of Unix platforms or variants. Even if you are coding a hypotenuse andhp
looks like a good abbreviation, it could be disinformation. For example, do not refer to a grouping of accounts as anaccountList
unless it’s actually aList
. The word list means something specific to programmers.It is not sufficient to add number series or noise words, even though the compiler is satisfied. If names must be different, then they should also mean something different.
Number-series naming
(a1, a2, .. aN)
is the opposite of intentional naming. Such names are not disinformative—they are noninformative; they provide no clue to the author’s intention.
- Make your names pronounceable. If you can’t pronounce it, you can’t discuss it without sounding like an idiot. For example “Well, over here on the bee cee arr three cee enn tee we have a pee ess zee kyew int, see?” can you see what is wrong here? This matters because programming is a social activity.
Compare :
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
/* ... */
};
to
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;;
private final String recordId = "102";
/* ... */
};
- Use searchable names as the author prefers that single-letter names can ONLY be used as local variables inside short methods. The length of a name should correspond to the size of its scope.
If a variable or constant might be seen or used in multiple places in a body of code, it is imperative to give it a search-friendly name.
Once again compare :for (int j=0; j<34; j++) { s += (t[j]*4)/5; }
to
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
Avoid encoding as you should be using an editing environment that highlights or colourizes members to make them distinct.
People quickly learn to ignore the prefix (or suffix) to see the meaningful part of the name. The more we read the code, the less we see the prefixes.
Eventually, the prefixes become unseen clutter and a marker of older code.Readers shouldn’t have to mentally translate your names into other names they already know. This problem generally arises from a choice to use neither problem domain terms nor solution domain terms.
Classes and objects should have noun or noun phrase names like
Customer
,WikiPage
,Account
, andAddressParser
. Avoid words likeManager
,Processor
,Data
, orInfo
in the name of a class. A class name should not be a verb.Methods should have verb or verb phrase names like
postPayment
,deletePage
, orsave
. Accessors, mutators, and predicates should be named for their value and prefixed withget
,set
, andis
according to the javabean standard.Pick one word for one abstract concept and stick with it. For instance, it’s confusing to have
fetch
,retrieve
, andget
as equivalent methods of different classes. How do you remember which method name goes with which class? Sadly, you often have to remember which company, group, or individual wrote the library or class in order to remember which term was used.
- Use solution domain names, and remember that the people who read your code will be programmers. So go ahead and use computer science (CS) terms, algorithm names, pattern names, math terms, and so forth.
It is not wise to draw every name from the problem domain because we don’t want our coworkers to have to run back and forth to the customer asking what every name means when they already know the concept by a different name.
The hardest thing about choosing good names is that it requires good descriptive skills and shared cultural background. This is a teaching issue rather than a technical, business, or management issue. As a result, many people in this field don’t learn to do it very well.
Chapter 3 - Functions
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.
How short should your functions be? They should usually be shorter than this :
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
blocks within
if
statements,else
statements,while
statements, and so on should be one line long. Probably that line should be a function call.Functions should do one thing. They should do it well. They should do it only.
To know that a function is doing more than “one thing” is if you can extract another function from it with a name that is not merely a restatement of its implementation.
We want the code to read like a top-down narrative.5 We want every function to be followed by those at the next level of abstraction so that we can read the program, descending one level of abstraction at a time as we read down the list of functions.
Don’t be afraid to make a name long. A long descriptive name is better than a short enigmatic name. A long descriptive name is better than a long descriptive comment.
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn’t be used anyway.
Flag arguments are ugly. Passing a boolean into a function is a truly terrible practice. It immediately complicates the signature of the method, loudly proclaiming that this function does more than one thing. It does one thing if the flag is true and another if the flag is false!
Reducing the number of arguments by creating objects out of them may seem like cheating, but it’s not.
it is better to extract the bodies of the try and catch blocks out into functions of their own.
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }
The Don't Repeat Yourself (DRY) principle states that duplication in process should be eliminated via automation.
Chapter 4 - Comments
Clear and expressive code with few comments is far superior to cluttered and complex code with lots of comments. Rather than spend your time writing the comments that explain the mess you’ve made, spend it cleaning that mess.
Keep in mind, however, that the only truly good comment is the comment you found a way not to write.
A comment like this can sometimes be useful, but it is better to use the name of the function to convey the information where possible.
Sometimes it is just helpful to translate the meaning of some obscure argument or return value into something that’s readable. In general it is better to find a way to make that argument or return value clear in its own right; but when its part of the standard library, or in code that you cannot alter, then a helpful clarifying comment can be useful.
It is sometimes reasonable to leave “To do” notes in the form of //TODO comments. In the following case, the TODO comment explains why the function has a degenerate implementation and what that function’s future should be.
//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model
protected VersionInfo makeVersion() throws Exception{
return null;
}
This subtle bit of misinformation, couched in a comment that is harder to read than the body of the code, could cause another programmer to blithely call this function in the expectation that it will return.
Sometimes you see comments that are nothing but noise. They restate the obvious and provide no new information. These comments are so noisy that we learn to ignore them.
/**
* Default constructor.
*/
protected AnnualDateRule() {
}
Don’t Use a Comment When You Can Use a Function or a Variable
Don’t put interesting historical discussions or irrelevant descriptions of details into your comments.
Chapter 5 - Formatting
Code formatting is important. It is too important to ignore. Code formatting is about communication, and communication is the professional developer’s first order of business.
Variables should be declared as close to their usage as possible. Because our functions are very short, local variables should appear a the top of each function.
- A team of developers should agree upon a single formatting style, and then every member of that team should use that style. We want the software to have a consistent style. We don’t want it to appear to have been written by a bunch of disagreeing individuals.
Chapter 6 - Objects and Data Structures
Hiding implementation is not just a matter of putting a layer of functions between the variables. Hiding implementation is about abstractions! A class does not simply push its variables out through getters and setters. Rather it exposes abstract interfaces that allow its users to manipulate the essence of the data, without having to know its implementation.
Objects hide their data behind abstractions and expose functions that operate on that data. Data structure expose their data and have no meaningful functions.
Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions.
There is a well-known heuristic called the Law of Demeter that says a module should not know about the innards of the objects it manipulates. As we saw in the last section, objects hide their data and expose operations. This means that an object should not expose its internal structure through accessors because to do so is to expose, rather than to hide, its internal structure.
Code is often called a train wreck because it look like a bunch of coupled train cars. Chains of calls like this are generally considered to be sloppy style and should be avoided. It is usually best to split them up as follows:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
Objects expose behaviour and hide data. This makes it easy to add new kinds of objects without changing existing behaviours. It also makes it hard to add new behaviours to existing objects. Data structures expose data and have no significant behavior. This makes it easy to add new behaviours to existing data structures but makes it hard to add new data structures to existing functions.
In any given system we will sometimes want the flexibility to add new data types, and so we prefer objects for that part of the system. Other times we will want the flexibility to add new behaviours, and so in that part of the system, we prefer data types and procedures. Good software developers understand these issues without prejudice and choose the approach that is best for the job at hand.
Chapter 7 - Error Handling
Back in the distant past, there were many languages that didn’t have exceptions. In those languages the techniques for handling and reporting errors were limited. You either set an error flag or returned an error code that the caller could check.
try
blocks are like transactions. Yourcatch
has to leave your program in a consistent state, no matter what happens in thetry
. For this reason, it is good practice to start with atry-catch-finally
statement when you are writing code that could throw exceptions. This helps you define what the user of that code should expect, no matter what goes wrong with the code that is executed in thetry
.
Checked exceptions can sometimes be useful if you are writing a critical library: You must catch them. But in general application development, the dependency costs outweigh the benefits.
Each exception that you throw should provide enough context to determine the source and location of an error. In Java, you can get a stack trace from any exception; however, a stack trace can’t tell you the intent of the operation that failed.
An advantage of wrapping is that you aren’t tied to a particular vendor’s API design choices. You can define an API that you feel comfortable with.
If you work in a code base with code like this, it might not look all that bad to you, but it is bad! When we return
null
, we are essentially creating work for ourselves and foisting problems upon our callers. All it takes is one missingnull
check to send an application spinning out of control.Returning
null
from methods is bad, but passing null into methods is worse. Unless you are working with an API which expects you to pass null, you should avoid passingnull
in your code whenever possible.Clean code is readable, but it must also be robust. These are not conflicting goals. We can write robust clean code if we see error handling as a separate concern, something that is viewable independently of our main logic. To the degree that we are able to do that, we can reason about it independently, and we can make great strides in the maintainability of our code.
Chapter 8 - Boundaries
There is a natural tension between the provider of an interface and the user of an interface. Providers of third-party packages and frameworks strive for broad applicability so they can work in many environments and appeal to a wide audience. Users, on the other hand, want an interface that is focused on their particular needs. This tension can cause problems at the boundaries of our systems.
Third-party code helps us get more functionality delivered in less time. Where do we start when we want to utilize some third-party package? It’s not our job to test the third-party code, but it may be in our best interest to write tests for the third-party code we use.
Instead of experimenting and trying out the new stuff in our production code, we could write some tests to explore our understanding of the third-party code.
The learning tests end up costing nothing. We had to learn the API anyway, and writing those tests was an easy and isolated way to get that knowledge. The learning tests were precise experiments that helped increase our understanding.
Interesting things happen at boundaries. Change is one of those things. Good software designs accommodate change without huge investments and rework. When we use code that is out of our control, special care must be taken to protect our investment and make sure future change is not too costly.
Code at the boundaries needs clear separation and tests that define expectations. We should avoid letting too much of our code know about the third-party particulars. It’s better to depend on something you control than on something you don’t control, lest it end up controlling you.
- We manage third-party boundaries by having very few places in the code that refer to them.
Chapter 9 - Unit Tests
- First Law You may not write production code until you have written a failing unit test.
- Second Law You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
- Third Law You may not write more production code than is sufficient to pass the currently failing test.
These three laws lock us into a cycle that is perhaps thirty seconds long. The tests and the production code are written together, with the tests just a few seconds ahead of the production code.
- Test code is just as important as production code. It is not a second-class citizen. It requires thought, design, and care. It must be kept as clean as production code.
It is unit tests that keep our code flexible, maintainable, and reusable.
if your tests are dirty, then your ability to change your code is hampered, and you begin to lose the ability to improve the structure of that code. The dirtier your tests, the dirtier your code becomes. Eventually, you lose the tests, and your code rots.
What makes tests readable is the same thing that makes all code readable: clarity, simplicity, and density.
F.I.R.S.T. Rules
Clean tests follow five other rules that form the F.I.R.S.T. acronym :
Fast Tests should be fast. They should run quickly. When tests run slow, you won’t want to run them frequently. If you don’t run them frequently, you won’t find problems early enough to fix them easily. You won’t feel as free to clean up the code. Eventually the code will begin to rot.
Independent Tests should not depend on each other. One test should not set up the conditions for the next test. You should be able to run each test independently and run the tests in any order you like. When tests depend on each other, then the first one to fail causes a cascade of downstream failures, making diagnosis difficult and hiding downstream defects.
Repeatable Tests should be repeatable in any environment. You should be able to run the tests in the production environment, in the QA environment, and on your laptop while riding home on the train without a network. If your tests aren’t repeatable in any environment, then you’ll always have an excuse for why they fail. You’ll also find yourself unable to run the tests when the environment isn’t available.
Self-Validating The tests should have a boolean output. Either they pass or fail. You should not have to read through a log file to tell whether the tests pass. You should not have to manually compare two different text files to see whether the tests pass. If the tests aren’t self-validating, then failure can become subjective and running the tests can require a long manual evaluation.
Timely The tests need to be written in a timely fashion. Unit tests should be written just before the production code that makes them pass. If you write tests after the production code, then you may find the production code to be hard to test. You may decide that some production code is too hard to test. You may not design the production code to be testable.
Chapter 10 - Classes
Class Organization
Public functions should follow the list of variables. We like to put the private utilities called by a public function right after the public function itself. This follows the stepdown rule and helps the program read like a newspaper article.
The name of a class should describe what responsibilities it fulfills. In fact, naming is probably the first way of helping determine class size. If we cannot derive a concise name for a class, then it’s likely too large.
We should also be able to write a brief description of the class in about 25 words, without using the words “if,” “and,” “or,” or “but.”
The Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to change. This principle gives us both a definition of responsibility and guidelines for class size. Classes should have one responsibility—one reason to change.
We want our systems to be composed of many small classes, not a few large ones. Each small class encapsulates a single responsibility, has a single reason to change, and collaborates with a few others to achieve the desired system behaviors.
For most systems, change is continual. Every change subjects us to the risk that the remainder of the system no longer works as intended. In a clean system we organize our classes so as to reduce the risk of change.
Our restructured
Sql
logic represents the best of all worlds. It supports the SRP. It also supports another key OO class design principle known as the Open-Closed Principle.
Chapter 11 - Systems
Software systems should separate the startup process, when the application objects are constructed and the dependencies are “wired” together, from the runtime logic that takes over after startup.
If we are diligent about building well-formed and robust systems, we should never let little, convenient idioms lead to modularity breakdown. The startup process of object construction and wiring is no exception. We should modularize this process separately from the normal runtime logic and we should make sure that we have a global, consistent strategy for resolving our major dependencies.
True Dependency Injection goes one step further. The class takes no direct steps to resolve its dependencies; it is completely passive. Instead, it provides setter methods or constructor arguments (or both) that are used to inject the dependencies.
The Spring Framework provides the best-known DI container for Java.4 You define which objects to wire together in an XML configuration file, then you ask for particular objects by name in Java code.
The EJB2 architecture comes close to the true separation of concerns in some areas. For example, the desired transactional, security, and some of the persistence behaviours are declared in the deployment descriptors, independently of the source code.
the most full-featured tool for separating concerns through aspects is the
AspectJ
language, an extension of Java that provides “first-class” support for aspects as modularity constructs. The pure Java approaches provided by Spring AOP and JBoss AOP are sufficient for 80–90 percent of the cases where aspects are most useful.The power of separating concerns through aspect-like approaches can’t be overstated. If you can write your application’s domain logic using POJOs, decoupled from any architecture concerns at the code level, then it is possible to truly test drive your architecture.
Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for the industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve.
Domain-Specific Languages allow all levels of abstraction and all domains in the application to be expressed as POJOs, from high-level policy to low-level details.
Systems must be clean too. An invasive architecture overwhelms the domain logic and impacts agility. When the domain logic is obscured, quality suffers because bugs find it easier to hide and stories become harder to implement. If agility is compromised, productivity suffers and the benefits of TDD are lost.
Chapter 12 - Emergence
According to Kent, a design is “simple” if it follows these rules (given in order of importance.) : • Runs all the tests. • Contains no duplication. • Expresses the intent of the programmer. • Minimizes the number of classes and methods.
- Following the practice of simple design can and does encourage and enable developers to adhere to good principles and patterns that otherwise take years to learn.
Chapter 13 - Concurrency
Why Concurrency?
- Concurrency is a decoupling strategy. It helps us decouple what gets done from when it gets done. In single-threaded applications what and when are so strongly coupled that the state of the entire application can often be determined by looking at the stack backtrace.
concurrency is hard. If you aren’t very careful, you can create some very nasty situations. Consider these common myths and misconceptions :
• Concurrency always improves performance. Concurrency can sometimes improve performance, but only when there is a lot of wait time that can be shared between multiple threads or multiple processors. Neither situation is trivial.
• Design does not change when writing concurrent programs. In fact, the design of a concurrent algorithm can be remarkably different from the design of a single-threaded system. The decoupling of what from when usually has a huge effect on the structure of the system.
• Understanding concurrency issues is not important when working with a container such as a Web or EJB container. In fact, you’d better know just what your container is doing and how to guard against the issues of concurrent updates and deadlock described later in this chapter.
- Concurrency incurs some overhead, both in performance as well as writing additional code.
Correct concurrency is complex, even for simple problems.
Concurrency bugs aren’t usually repeatable, so they are often ignored as one-offs instead of the true defects, they are.
Concurrency often requires a fundamental change in design strategy.
Concurrency-related code has its own life cycle of development, change, and tuning.
Concurrency-related code has its own challenges, which are different from and often more difficult than nonconcurrency-related code.
The number of ways in which miswritten concurrency-based code can fail makes it challenging enough without the added burden of surrounding application code.
The author recommends Keeping your concurrency-related code separate from other code, and to take data encapsulation to heart; severely limit the access of any data that may be shared.
Write tests that have the potential to expose problems and then run them frequently, with different programmatic configurations and system configurations and load. If tests ever fail, track down the failure. Don’t ignore a failure just because the tests pass on a subsequent run.
Learn your library and know the fundamental algorithms. Understand how some of the features offered by the library support solving problems similar to the fundamental algorithms.
More fine-grained recommendations : • Treat spurious failures as candidate threading issues. • Get your non-threaded code working first. • Make your threaded code pluggable. • Make your threaded code tunable. • Run with more threads than processors. • Run on different platforms. • Instrument your code to try and force failures.
Chapter 14 - Successive Refinement
It is not enough for code to work. Code that works is often badly broken. Programmers who satisfy themselves with merely working code are behaving unprofessionally. They may fear that they don’t have time to improve the structure and design of their code, but I disagree. Nothing has a more profound and long-term degrading effect upon a development project than bad code. Bad schedules can be redone, bad requirements can be redefined. Bad team dynamics can be repaired. But bad code rots and ferments, becoming an inexorable weight that drags the team down. Time and time again I have seen teams grind to a crawl because, in their haste, they created a malignant morass of code that forever thereafter dominated their destiny.
Of course, bad code can be cleaned up. But it’s very expensive. As code rots, the modules insinuate themselves into each other, creating lots of hidden and tangled dependencies. Finding and breaking old dependencies is a long and arduous task. On the other hand, keeping code clean is relatively easy. If you made a mess in a module in the morning, it is easy to clean it up in the afternoon. Better yet, if you made a mess five minutes ago, it’s very easy to clean it up right now. So the solution is to continuously keep your code as clean and simple as it can be.
Chapter 15 - JUnit Internals
- JUnit is one of the most famous of all Java frameworks. As frameworks go, it is simple in conception, precise in definition, and elegant in implementation. This chapter analyses the JUnit tool.
Chapter 16 - Refactoring SerialDate
This chapter is a study case. I recommendable completely reading it.
Chapter 17 - Smells and Heuristics
Comments
is inappropriate for a comment to hold information better held in a different kind of systems such as your source code control system, your issue tracking system, or any other record-keeping system.
A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments get old quickly. It is best not to write a comment that will become obsolete. If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible.
A comment is redundant if it describes something that adequately describes itself.
If you are going to write a comment, take the time to make sure it is the best comment you can write. Choose your words carefully. Use correct grammar and punctuation. Don’t ramble. Don’t state the obvious. Be brief.
-When you see commented-out code, delete it! Don’t worry, the source code control system still remembers it.
Environment
Building a project should be a single trivial operation. You should not have to check many little pieces out from source code control. You should not need a sequence of arcane commands or context-dependent scripts in order to build the individual elements.
You should be able to run all the unit tests with just one command.
Functions
Functions should have a small number of arguments.
Output arguments are counterintuitive. Readers expect arguments to be inputs, not outputs.
Boolean arguments loudly declare that the function does more than one thing. They are confusing and should be eliminated.
Methods that are never called should be discarded. Keeping dead code around is wasteful.
General
Following “The Principle of Least Surprise,” any function or class should implement the behaviours that another programmer could reasonably expect.
It is risky to override safeties.
It is important to create abstractions that separate higher-level general concepts from lower-level detailed concepts.
- Well-defined modules have very small interfaces that allow you to do a lot with a little. Poorly defined modules have wide and deep interfaces that force you to use many different gestures to get simple things done.
Thank you, and goodbye!