C.A.R. Hoare wrote, “There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.”
It is a popular belief that "design" is a stage you complete before moving on to writing code. The truth is that the act of writing code is a design activity. Practitioners of Extreme Programming advocate that design is the code. Writing code tests before any code as a design verification tool is a wise idea (test-driven design). This does not mean you shouldn't think (design) before you start typing.
The most important design act is the architectural design. This looks at the system as a whole, identifies the main subsystems, and works out how they communicate. This design has the most influence on the performance and characteristics of the system as a whole and the least impact on specific lines of code.
The architectural subsystems usually need to be broken down into comprehensible modules. It is very easy to be vague about design at the module level. Module may mean many different things depending on the design approach. It could be a class hierarchy or a free-standing executable. This design stage often produces published interfaces. These may be difficult to change later on as they form strict contracts between code modules and teams of programmers.
Interface design tends to be less formal and easier to change behind the module. Resist the urge to do this micro design at the keyboard. Do not write the first code that comes into your head.
Functions are the lowest design level but are still important.
Sloppy design may be due to inexperienced programmers but may more often be caused by commercial pressures of the software factory. The irony is that in almost every case, a lack of a good design costs more than properly doing it would have.
A sound design has many benefits. It makes the code easier to write because there is a well-defined plan of attack and it's clear how things will fit together. It is easier to understand. It is easier to fix as you can identify the locations of problems more easily. It is less likely to harbor bugs/ It is more resilient to change.
The best design approaches are iterative, cautious, realistic, and informed. Avoid too many nasty surprises by doing a small amount of design, implementing it, assessing the implications, and feeding it to the next design round. Don't try to do too much design at once. Small, sure design steps are more likely to succeed than large, clumsy ones. Be realistic. The outcome of a prescriptive design process may depend on the quality of the established requirements, the team's experience, and the rigor that the process is applied. You must fully understand all requirements and motivating principles to be clear about the problem you are solving. You must fully understand the important qualities of the right solution. If you don't, you will solve the wrong problem.
Remember that there is no right or wrong design. At best, there are good designs and bad designs.
Good designs have a number of attractive characteristics.
The single most important characteristic of well-designed code is simplicity. A simple design is simple to understand, easy to implement, coherent, and consistent.
Simple code is as small as possible but no smaller.
This takes some doing, as the mathematician Blaise Pascal appreciated: “I am sorry for the length of my letter, but I had not the time to write a short one.”
Laziness can pay off. Work your design to concentrate on the immediate problems. Strive for simple code that does a lot with a little (less is more). Well-designed code may look obvious but it may take an awful lot of thought (and refactoring) to make it that simple. Do not assume that an obvious-looking code structure was easy to design.
Elegance is another characteristic of well-designed code. Code that is confusingly clever or overly complex is not elegant. The design should not be riddled with special cases. A single, small change in one place should not lead to modifications in many other places.
It is natural to divide a design problem into parts, each of which is called a {{c1::module}} (or component). The quality of this decomposition is paramount. Key qualities of modularity are cohesion and coupling. What we want are modules with strong cohesion and low coupling.
{{c1::Cohesion}} is a measure of how related functionality is gathered together and how well the parts inside the module work as a whole. Weakly cohesive modules are a sign of bad decomposition. Each module must have a clearly defined role and not a grab bag of unrelated functionality.
{{c1::Coupling}} is a measure of the interdependency between modules. In the simplest designs, modules have little coupling and so are less reliant on one another. Modules of course cannot be totally decoupled because they would be unable to work together. Still, good design limits the lines of communication to only those absolutely necessary.
{{c1::Modular design}} helps split tasks between programmers, but take care to not decompose based on team organization ({{c2::Conway's law}}). A {{c1::module}} should be designed to have high {{c2::cohesion}} and minimal {{c3::coupling}}.
Modularity also helps splitting tasks between programmers. Take care that you do not decompose modules based on team organization (Conway's law). Design modules to be cohesive with minimal coupling. The decomposition must represent a valid partition of the problem space.
Each {{c1::module}} defines an {{c2::interface}} which is the public facade that hides an internal {{c3::implementation}}; the set of available operations is often called an {{c4::API}}.
Bad design that puts operations in the wrong place make it a nightmare to follow the application logic and difficult to extend the design.
Well-designed code clearly defines roles and responsibilities. Each main actor in the system that have clear responsibilities can ensure they have crisp interfaces.
When designing an interface, you create an abstraction. Choose carefully what is important for the user and what can be hidden from them.
Abstractions can form a hierarchy. View the different levels of abstraction.
Compression is the ability of an interface to represent a large operation with something simpler and is often the result of making good abstractions. Bad abstractions can lead to more verbose code.
Substitutability is possible when interfaces meet the same contract. A soft interface can have different sorting algorithms sitting behind it. This is possible with class inheritance hierarchies where objects can be substituted for its supertype.
Well-designed code is extensible: it allows extra functionality to be slotted in at appropriate places when necessary. The danger is of over-engineered code that tries to cope with any potential future modification.
Extensibility can be accommodated through software scaffolding. This includes dynamically loaded plug-ins, carefully chosen class hierarchies with abstract interfaces at the top, the provision of useful callback functions, and a fundamentally logical and malleable code structure.
Well-designed code contains no duplication. It never has to repeat itself. {{c1::Duplication}} is the enemy of elegant and simple design and usually is due to {{c2::cut-and-paste programming}} or more subtly when programmers do not understand the whole system.
A good design is not necessarily portable. A lot can be done to prevent platform dependence, but compromising code for unnecessary portability is bad design. A good design is appropriately portable. Think about it early to avoid expensive rework to fix old assumptions. A common approach is to create a platform abstraction layer.
A good design naturally employs best practices involving both the design methodology and the language's idioms. Given an implementation language, you must understand how to use it will.
A good design should be documented. Do not leave readers to infer the structure by themselves. This is even more important at higher levels of design. The documentation should be small because the design should be simple. At one end of the spectrum, architectural designs are documented in a specification, and on the other end, functions can be as self-documenting as possible, and in the middle, you will probably want to use literate programming for API documentation.
How to Design Code
To be a good designer, you must understand what constitutes a good design and learn to avoid the characteristics of bad design. Then practice. For a long time.
Modern design methods fall into two main families of fundamental design philosophies: structured design and object-oriented design.
Structured design is fundamentally about functional decompression: breaking up the functionality of the system into a series of smaller operations. Routines are the main structuring devices and the design is composed of a hierarchy of routines. Structured design is characterized by the divide-and-conquer approach. The two main lines of attack are top-down and bottom-up.
A top-down approach starts with the entire problem and breaks it into smaller units until no more division is necessary. A bottom-up design starts with the smallest units of functionality or the simple things you know the system must do. In practice, both can occur at the same time and the design process ends where they meet.
Object-oriented design focuses instead on the data within the system (rather than the operations a system must perform). {{c1::Object-oriented design}} models the software as an interacting set of individual units called objects (How to Design Code).
An object-oriented design identifies the primary objects in the problem domain and determines their characteristics. The behavior of the objects and operations they provide are established and then the objects are weaved into the design along with any implementation domain objects needed.
{{c1::Object-oriented programming}} was hailed as the savior of the software design world, largely lived up to the hype, and allowed software to manage the {{c2::complexity}} of far larger problems.
Design Tools
The Unified Modeling Language is currently the most popular and well-specified notation. {{c1::UML}} provides a standard way to model and document practically every artifact generated in software development. {{c1::UML}} has grown to the point that it models more than just software: it can model hardware, business processes, and even organizational structures.
{{c1::Design patterns}} provide a vocabulary of proven design techniques and are worthy of study.
Flowcharts are good to visualize algorithms and high-level overview. They should be used sparingly because they are less precise than code and become another thing to be kept in sync with code changes.
Pseudocode can help draft function implementations. Program Design Language is a formalized pseudocode.
{{c1::Design patterns}} provide a vocabulary of proven design techniques and are worthy of study.
Flowcharts are good to visualize algorithms and high-level overview. They should be used sparingly because they are less precise than code and become another thing to be kept in sync with code changes.
Pseudocode can help draft function implementations. Program Design Language is a formalized pseudocode.
I guess it made sense to somebody at the time. I'd love to have seen their pseudocode compiler.
Design in code: you can capture interfaces in code without implementing them (just stub values and put comments describing what should be done).
Computer-aided software engineering tools can be used to generate code from pictures and modify pictures when you modify the code, which is known as round-trip engineering.
In a Nutshell
Good code is well designed. It has a certain aesthetic appeal that makes it feel good. You must plan a design before beginning to write code, or you’ll end up with an unpleasant mess. Consider things like clean structure, possible future extensions, correct interfaces, appropriate abstractions, and portability requirements. Aim for simplicity and elegance. Design involves a strong element of craftsmanship. The best designs come from experienced and skilled hands. Ultimately, a good designer is what makes a good design. Mediocre programmers do not produce excellent designs.
Article notes
What is the single most important characteristic of well-designed code according to Code Craft?
Simplicity
What does Code Craft note you should not assume about obvious-looking code?
That it was easy to design
What does Code Craft call a measure of how related functionality is gathered together and how well the parts inside a module work as a whole?
Cohesion
What does Code Craft call a measure of the interdependency between modules?
Coupling
What idea does Code Craft mention when saying not to decompose modules based on team organization?
Conway's law
Code Craft: sloppy design is most often due to the commercial pressures of the software factory. The irony is that in almost every case:
The costs of doing it properly are lower
What must you do in order to have a clear design for the problem you are solving?
Fully understand all requirements and motivating principles
What characteristic of well-designed code is not present if it is confusingly clever, overly complex, or riddled with special cases?
Elegance
Each [...] defines an [...] which is the public facade that hides an internal [...]; the set of available operations is often called an [...].
Each module defines an interface which is the public facade that hides an internal implementation; the set of available operations is often called an API.
[...] is a measure of how related functionality is gathered together and how well the parts inside the module work as a whole.
Cohesion is a measure of how related functionality is gathered together and how well the parts inside the module work as a whole.
[...] provide a vocabulary of proven design techniques and are worthy of study.
Design patterns provide a vocabulary of proven design techniques and are worthy of study.
[...] provides a standard way to model and document practically every artifact generated in software development.
UML provides a standard way to model and document practically every artifact generated in software development.
[...] helps split tasks between programmers, but take care to not decompose based on team organization ([...]).
Modular design helps split tasks between programmers, but take care to not decompose based on team organization (Conway's law).
It is natural to divide a design problem into parts, each of which is called a [...] (or component).
It is natural to divide a design problem into parts, each of which is called a module (or component).
[...] has grown to the point that it models more than just software: it can model hardware, business processes, and even organizational structures.
UML has grown to the point that it models more than just software: it can model hardware, business processes, and even organizational structures.
[...] was hailed as the savior of the software design world, largely lived up to the hype, and allowed software to manage the [...] of far larger problems.
Object-oriented programming was hailed as the savior of the software design world, largely lived up to the hype, and allowed software to manage the complexity of far larger problems.
[...] models the software as an interacting set of individual units called objects (How to Design Code).
Object-oriented design models the software as an interacting set of individual units called objects (How to Design Code).
[...] is a measure of the interdependency between modules.
Coupling is a measure of the interdependency between modules.
[...] is the enemy of elegant and simple design and usually is due to [...] or more subtly when programmers do not understand the whole system.
Duplication is the enemy of elegant and simple design and usually is due to cut-and-paste programming or more subtly when programmers do not understand the whole system.
A [...] should be designed to have high [...] and minimal [...].
A module should be designed to have high cohesion and minimal coupling.