2 Iterate to achieve good design
Good software design emerges through iteration, not one-shot brilliance. The chapter frames development as repeatedly traversing a decision tree: you choose a path, discover wrong turns, backtrack, and try again. Short design–code–test cycles (with the option of test-driven development) help manage change and complexity, the core challenges of design. Progress isn’t always linear, but with experience the tree shrinks, iterations become faster, and continuous improvement leads to better outcomes. A key aim is to anticipate change and prevent it from rippling through the code by isolating what varies.
The chapter demonstrates this with a book catalog application. The first iteration introduces cohesive Book and Catalogue classes, supporting case-insensitive searches and don’t-care attributes. When new attributes (year, genre) are required, the second iteration encapsulates all book data and matching logic in a dedicated Attributes class and uses enums for constrained values. Catalogue now delegates matching to Attributes, achieving loose coupling and clarity of responsibilities. These steps illustrate principles such as Single Responsibility, Encapsulate What Varies, Delegation, and the Principle of Least Knowledge.
Expanding to new book types initially leads to an explosion of subclasses (Fiction/Cookbook plus corresponding attribute classes), runtime type checks, and duplicated code—signs the design won’t scale despite nods to the Open-Closed and Code to the Interface principles. The team backtracks in a fourth iteration to a simpler, more extensible approach: a single Attributes class backed by a key–value dictionary (with enum keys like KIND, TITLE, YEAR, GENRE, REGION, SUBJECT), defensive type validation, and generic matching over provided pairs. Book stays minimal and Catalogue consolidates to one find method. The result centralizes change in one place, removes duplication and type checks, and shows how iterative refinement—and the willingness to correct missteps—yields a robust, flexible design.
A design decision tree and the application development path shown as the dotted line. Each tree node denotes a juncture during development when we must decide to choose a path (i.e., branch) on which to continue. While designing and coding, if we head down a branch that leads to a dead-end (the lumps of coal), we must backtrack to the previous decision node and try another branch. After trying all the branches at a node, we must backtrack to the next higher node and try the next branch from there. We hope to find the path to a successfully completed application (the pot of gold).
Iterations during application development. Each iteration stands for going down a branch in the decision tree, including backing up and trying another branch. Each iteration includes designing, coding, and testing to incrementally improve or add more features to the application. The iterations can take different amounts of time, as short as a few hours or up to around two weeks.
In our first development iteration, we created two classes: Book and Catalogue. The dotted line shows our development path. The application fulfills its current requirements.
The second development iteration adds class Attributes, and class Catalogue delegates attribute matching to class Attributes.
A diagram of our application architecture as we’ve designed it thus far, showing our classes and subclasses and their relationships.
The third development iteration added subclasses Fiction, Cookbook, FictionAttrs, and CookbookAttrs. This increase in the number of subclasses plus the added runtime type checking tell us that we took a wrong decision path.
It took four iterations, including some backtracking, to find the development path to an application design that successfully encapsulates changes to the kinds of books and their attributes.
Summary
- It usually takes several development iterations to achieve a well-designed program. Be willing to backtrack from bad design decisions.
- The Single Responsibility Principle (SRP) states that a class should be cohesive and have only one primary responsibility.
- The Encapsulate What Varies Principle states that code that can vary should be isolated to keep its changes from causing other code to change.
- The Delegation Principle states that one class can perform work on the behalf of another class, where the work belongs to a more suitable cohesive class.
- The Principle of Least Knowledge states that classes should not know about each other’s implementation, and so the classes are loosely coupled with few, if any, dependencies on each other.
- The Open-Closed Principle states that a class should be closed for modification but open for extension to provide both stability and flexibility.
- The Code to the Interface Principle states that for runtime flexibility, we should not write code that can work only with a specific subclass but use polymorphism and write code that can work with multiple subclasses.
- The Don’t Repeat Yourself Principle states that well-designed code does not contain duplicate copies of code.
- OOP is important for good application design, but it’s not a panacea to all design problems. Be smart about how to apply its concepts. For example, do not create too many subclasses unnecessarily and thereby make your application too complex.
- Using better data structures can simplify complex code.
FAQ
What does “iterate to achieve good design” mean in this chapter?
It means building the application in small, repeatable cycles of design–code–test. Each iteration explores a branch of a design decision tree, accepts that some paths will be wrong, and uses backtracking to choose a better path. This incremental approach increases the odds of ending with a clean, working design.How are iterations structured and how long should they take?
Each iteration includes designing, coding, and testing a small increment of functionality. Only start the next iteration after all tests pass. Typical durations range from a few hours up to about two weeks; longer often signals you’re attempting too much at once.What is test-driven development (TDD) and how does it fit into the iterations?
TDD flips the iteration order to test–design–code. You write failing tests first for the features in scope for the iteration, then design and implement just enough to make those tests pass. It gives fast feedback and clarifies intent before coding.What does “don’t let changes leak out” mean, and how do we prevent it?
When one part changes, other parts shouldn’t need to change. Encapsulate what varies behind stable interfaces so modifications are localized. In the example, moving attribute data and matching logic into an Attributes class prevents Catalogue from changing when attributes change.Which design principles are highlighted in this chapter?
- Single Responsibility: each class does one primary thing (Book stores attributes; Catalogue manages a list and searches).- Encapsulate What Varies: put changing attribute data and matching in Attributes.
- Delegation: Catalogue delegates matching to Attributes.
- Principle of Least Knowledge (loose coupling): minimize inter-class dependencies via private data and narrow interfaces.
- Open–Closed: keep stable code closed to modification but open to extension (used, then reconsidered).
- Code to the Interface: operate on general types rather than specific subclasses.
- DRY: eliminate duplicate find methods and runtime type checks.
- Defensive programming: validate attribute datatypes at object creation.
Software Design for Python Programmers ebook for free