Skip to content

Chapter 19: Software Trends

Judge every popular practice by one question: does it reduce complexity in large systems? Interface inheritance, incremental development, and unit tests help. Implementation inheritance, test-driven development, pattern overuse, and getters and setters often add complexity or encourage tactical programming.

  • Object-oriented programming and inheritance:
    • Private methods and variables support information hiding, but OOP doesn’t guarantee good design. Shallow classes, complex interfaces, and exposed internal state still produce complexity.
    • Interface inheritance (a parent defines signatures, subclasses implement them) gives leverage: knowledge of one implementation, such as disk-file I/O, transfers to another, such as network sockets. “the more different implementations there are of an interface, the deeper the interface becomes.”
    • Implementation inheritance (the parent also provides default code) reduces change amplification by avoiding duplicated methods. But parent and children share instance variables, which leaks information. In the worst case, changing any class requires knowing the whole hierarchy below the parent.
    • How to use it: first consider composition, with small helper classes that the original classes build on. If inheritance is unavoidable, have the parent manage its own state, with subclasses reading it or using parent methods only.
  • Agile development: its incremental, iterative approach matches the book’s. Its risk is tactical programming, because it focuses on features rather than abstractions and on putting off design.
    • Rule: “the increments of development should be abstractions, not features.” Put off thinking about an abstraction until a feature needs it, then design it cleanly and somewhat general-purpose (ch06).
  • Unit tests: they make refactoring safe, and refactoring is how designs improve. Without tests, developers minimize changes, complexity builds up, and mistakes stay. Unit tests give higher coverage than system tests, so they find more bugs.
  • Test-driven development: “I am not a fan.” It “focuses attention on getting specific features working, rather than finding the best design,” which is “tactical programming pure and simple.” It is too incremental, with no obvious time for design.
    • Exception: when fixing a bug, write a failing test first, then fix the bug and see the test pass. That is the only way to know the test triggers the bug.
    • Once you need an abstraction, design it all at once, or at least enough for a reasonably comprehensive set of core functions.
  • Design patterns: mostly good, since they are agreed clean solutions to common problems. The greatest risk is over-application: “the notion that design patterns are good doesn’t necessarily mean that more design patterns are better.”
  • Getters and setters: better than public variables if you must expose state, but “it’s better not to expose instance variables in the first place.” They are shallow methods that clutter the interface; their popularity in Java shows the risk of assuming a pattern is good and using it everywhere.
  • Interface inheritance: one interface, many implementations; a source of depth.
  • Implementation inheritance: shared code and shared state between parent and subclasses; a source of leakage.
  • Composition: building classes on small helper classes instead of a parent class.
  • Unit test vs. system test: small, isolated tests of a method, written by developers, versus whole-application tests in a production-like environment, often written by QA. System tests are also called integration tests.
  • Test-first bug fixing: the one place the author recommends writing tests before code.
  • Ask of any new paradigm: “does the proposal really help to minimize complexity in large software systems?” Some proposals that sound good make complexity worse.
  • Think of tests as what makes strategic programming possible in practice, because they make refactoring safe.
  • Treat a popular pattern as a tool that fits some problems, not as a goal.
  • Deep class hierarchies with shared mutable state in the parent.
  • “implement a minimal special-purpose mechanism to start with, and refactor into something more generic later”, when applied as a rule. It argues against investment.
  • Writing the test, then hacking in just enough code, as the default way to design classes.
  • Forcing a problem into a pattern when a custom approach is cleaner.
  • Reflexive getters and setters for every field.

Tcl’s byte-code compiler. To improve performance, the Tcl team replaced Tcl’s interpreter with a byte-code compiler, a change that affected almost every part of the core engine. Tcl had an excellent unit test suite, and the team ran it against the new engine. The tests found the bugs so effectively that only a single bug turned up after the alpha release. This is the argument for unit tests: they let a team make a huge structural change with confidence.

Applying “abstractions, not features” (an illustration that follows the chapter’s rules). A sprint needs undo for text insertion.

  • Feature-sized, TDD-style increment: write a test for undoing an insert, then add just enough code to the text class to pass it. Next sprint, add the cursor, then the selection, each squeezed into the text class.
  • Abstraction-sized increment: when the first undo feature arrives, design the undo abstraction once, with enough core functions to be reasonably complete. That is a general history mechanism with actions and grouping (ch09’s History class), with text-specific actions in the text class. Later features add new action classes without changing the mechanism.
  1. Prefer interface inheritance; use implementation inheritance cautiously, and try composition first.
  2. Develop incrementally, but make each increment a cleanly designed abstraction.
  3. Write thorough unit tests so that refactoring is safe.
  4. Don’t use TDD as a design method; do write a failing test before fixing a bug.
  5. Use design patterns where they fit, and don’t force them.
  6. Avoid exposing instance variables, through getters and setters or otherwise.
  • Ch 1: incremental development.
  • Ch 3: tactical versus strategic programming.
  • Ch 5: private is not the same as hidden; information hiding within a hierarchy.
  • Ch 6: somewhat general-purpose abstractions.
  • Ch 7: decorators as an overused pattern.
  • Ch 9: the History undo design.
  • Ch 16: refactoring when modifying existing code.
  • Ch 17: design patterns as a form of consistency.
  • Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, and Vlissides), which popularized the idea.