Programming/Principles
Principles
Make Interfaces Easy to Use Correctly and Hard to Use Incorrectly
Scott Meyers in [H10]:
One of the most common tasks in software development is interface specification. Interfaces occur at the highest level of abstraction (user interfaces), at the lowest (function interfaces), and at levels in between (class interfaces, library interfaces, etc.). Regardless of whether you work with end users to specify how they'll interact with a system, collaborate with developers to specify an API, or declare functions private to a class, interface design is an important part of your job. If you do it well, your interfaces will be a pleasure to use and will boost others' productivity. If you do it poorly, your interfaces will be a source of frustration and errors.
Good interfaces are:
- Easy to use correctly
People using a well-designed interface almost always use the interface correctly, because that's the path of least resistance. In a GUI, they almost always click on the right icon, button, or menu entry, because it's the obvious and easy thing to do. In an API, they almost always pass the correct parameters with the correct values because that's what's most natural. With interfaces that are easy to use correctly, things just work.
- Hard to use incorrectly
Good interfaces anticipate mistakes people might make, and make them difficult—ideally, impossible—to commit. A GUI might disable or remove commands that make no sense in the current context, for example, or an API might eliminate argument-ordering problems by allowing parameters to be passed in any order.
A good way to design interfaces that are easy to use correctly is to exercise them before they exist. Mock up a GUI—possibly on a whiteboard or using index cards on a table—and play with it before any underlying code has been created. Write calls to an API before the functions have been declared. Walk through common use cases and specify how you want the interface to behave. What do you want to be able to click on? What do you want to be able to pass? Easy-to-use interfaces seem natural, because they let you do what you want to do. You're more likely to come up with such interfaces if you develop them from a user's point of view. (This perspective is one of the strengths of test-first programming.)
Making interfaces hard to use incorrectly requires two things. First, you must anticipate errors users might make and find ways to prevent them. Second, you must observe how an interface is misused during early release and modify the interface—yes, modify the interface!—to prevent such errors. The best way to prevent incorrect use is to make such use impossible. If users keep wanting to undo an irrevocable action, try to make the action revocable. If they keep passing the wrong value to an API, do your best to modify the API to take the values that users want to pass.
Above all, remember that interfaces exist for the convenience of their users, not their implementers.
Keep it simple, stupid (KISS)
The acronym was reportedly coined by Kelly Johnson, lead engineer at the Lockheed Skunk Works (creators of the Lockheed U-2 and SR-71 Blackbird spy planes, among many others).
The Unix philosophy
The Unix philosophy is documented by Doug McIlroy in the Bell System Technical Journal from 1978:
1. Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new "features". 1. Expect the output of every program to become the input to another, as yet unknown, program. Don't clutter output with extraneous information. Avoid stringently columnar or binary input formats. Don't insist on interactive input. 1. Design and build software, even operating systems, to be tried early, ideally within weeks. Don't hesitate to throw away the clumsy parts and rebuild them. 1. Use tools in preference to unskilled help to lighten a programming task, even if you have to detour to build the tools and expect to throw some of them out after you've finished using them.
Easier to change (ETC)
From [TH20]:
Good Design Is Easier to Change Than Bad Design.
A thing is well designed if it adapts to the people who use it. For code, that means it must adapt by changing. So we believe in the ETC principle: Easier to Change. ETC. That's it.
As far as we can tell, every design principle out there is a special case of ETC.
Why is decoupling good? Because by isolating concerns we make each easier to change. ETC.
Why is the single responsibility principle useful? Because a change in requirements is mirrored by a change in just one module. ETC.
Why is naming important? Because good names make code easier to read, and you have to read it to change it. ETC!
ETC Is a Value, Not a Rule.
Values are things that help you make decisions: should I do this, or that? When it comes to thinking about software, ETC is a guide, helping you choose between paths. Just like all your other values, it should be floating just behind your conscious thought, subtly nudging you in the right direction.
Don't Repeat Yourself (DRY)
From [TH20]:
We feel that the only way to develop software reliably, and to make our developments easier to understand and maintain, is to follow what we call the DRY principle:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Why do we call it DRY?
DRY—Don't Repeat Yourself.
[...]
DRY Is More Than Code
[...]
DRY is about the duplication of knowledge, of intent. It's about expressing the same thing in two different places, possibly in two totally different ways.
Here's the acid test: when some single facet of the code has to change, do you find yourself making that change in multiple places, and in multiple different formats? Do you have to change code and documentation, or a database schema and a structure that holds it, or...? If so, your code isn't DRY.
Bertrand Meyer's Five Criteria for modular design
From [M97]:
Modular decomposability
A software construction method satisfies Modular Decomposability if it helps in the task of decomposing a software problem into a smaller number of less complex subproblems, connected by a simple structure, and independent enough to allow further work to proceed separately on each of them.
Modular composability
A method satisfies Modular Composability if it favors the production of software elements which may then be freely combined with each other to produce new systems, possibly in an environment quite different from the one in which they were initially developed.
Modular understandability
A method favors Modular Understandability if it helps produce software in which a human reader can understand each module without having to know the others, or, at worst, by having to examine only a few of the others.
Modular continuity
A method satisfies Modular Continuity if, in the software architectures that it yields, a small change in a problem specification will trigger a change of just one module, or a small number of modules.
Modular protection
A method satisfies Modular Protection if it yields architectures in which the effect of an abnormal condition occurring at run time in a module will remain confined to that module, or at worst will only propagate to a few neighbouring modules.
Bertrand Meyer's Five Rules which we must observe to ensure modularity
From [M97]:
Direct Mapping
The modular structure devised in the process of building a software system should remain compatible with any modular structure devised in the process of modeling the problem domain.
Few Interfaces
Every module should communicate with as few others as possible.
Small Interfaces (weak coupling)
If two modules communicate, they should exchange as little information as possible.
Explicit Interfaces
Whenever two modules and communicate, this must be obvious from the text of or or both.
Information Hiding
The designer of every module must select a subset of the module's properties as the official information about the module, to be made available to authors of client modules.
Bertrand Meyer's Five Principles of software construction
From [M97]:
Linguistic Modular Units principle
Modules must correspond to syntactic units in the language used.
Self-Documentation principle
The designer of a module should strive to make all information about the module part of the module itself.
Uniform Access principle
All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.
Open-Closed principle
Modules should be both open and closed.
The Single Choice principle
Whenever a software system must support a set of alternatives, one and only one module in the system should know their exhaustive list.
Bibliography
- [H10] Kevin Henney. 97 Things Every Programmer Should Know. O'Reilly, 2010.
- [M97] Bertrand Meyer. Object-Oriented Software Construction, second edition. Prentice Hall, 1997.
- [TH20] David Thomas, Andrew Hunt. The Pragmatic Programmer, 20th Anniversary Edition. Addison–Wesley, 2020.