IndexArchivesAtomRSS

Growing Object-Oriented Software, Guided by Tests

This book focuses on how to do test-driven development with object-oriented software, but it also ended up introducing to me several general principles about software design and defining vocabulary that are routinely referenced in articles and blog posts about software. In other words, it is dense and provides a lot to digest.

The first third of the book lays out the concepts and then most of the last two-thirds work through an example of building an auction sniper application.

Definitions

  • acceptance test: tests the functionality of a feature and how the whole system operates

  • integration test: tests how code interacts with external/invariant code

  • unit test: tests the functionality of a single object

  • end to end test: tests the system as if it were a black box and only interacts with it through the UI

  • edge to edge test: tests every step from build to deployment to release

  • coupling: change in one component forces change in another (i.e. the modularity, or lack thereof, of the system)

  • cohesion: responsibilities form a meaningful unit

  • role: related group of responsibilities

  • responsibility: obligation to either perform a task or know information

  • collaboration: interaction between objects or roles

  • mockery: jMock term that refers to the context of the object being tested

  • mock object: a test object that substitutes for objects that interact with the object under test

  • expectations: rules that define how mock objects should be invoked

  • "walking skeleton": most minimal implementation necessary to have an end-to-end test

  • encapsulation: behavior of an object can only be affected through methods for interaction with other objects

  • information hiding: how object functions remains internal and invisible to other objects

  • aliasing: sharing references to mutable objects, breaks encapsulation

  • peers: objects with which a given object communicates

  • dependencies: services from peers without which the object cannot function

  • notifications: peers that need to be updated with the object's behavior or status

  • adjustments: peers that adjust the object's behavior to work with the rest of the system

  • context independence: object has no internal knowledge about its environment

  • interface: "whether two components will fit together"

  • protocol: "whether two components will work together"

  • spike: initial code written to figure out what to do, later rolled back and rewritten more cleanly

  • implementation layer: describes how the code will do something

  • declarative layer: describes what the code will do

Key Ideas

  • In general, we want low coupling and high cohesion.

  • Object-oriented design can be thought of as the network of communications among the objects in your software system.

  • Objects are mutable; values are immutable.

  • Interfaces help define an object's roles.

  • "Tell, Don't Ask" or "Law of Demeter"

  • Mock objects are used to test interactions between objects.

  • Begin with a "walking skeleton".

  • Start each new feature with an acceptance test to determine how the new feature will function.

  • Separate acceptance tests for completed features to catch bugs vs acceptance tests for new features in progress.

  • Write unit tests for object behavior rather than the object's methods.

  • Unit tests check the internal quality of the code; acceptance tests check the external quality.

  • Something that is difficult to test is probably badly designed.

  • Single Responsibility Principle:

Our heuristic is that we should be able to describe what an object does without using any conjunctions ("and," "or").

  • Interacting with the composite object should be simpler than interacting with the components that compose it.

  • "Mock an object's peers [...] but not its internals."

  • Techniques for introducing new objects:

    • "Breaking out": when code for an object becomes too complex, separate it into smaller units
    • "Budding off": placeholder for a new object, to be filled in with more implementation details later
    • "Bundling up": creating a new object for a group of objects that are always used together
  • When to break out:

Break up an object if it becomes too large to test easily, or if its test failures become difficult to interpre. Then unit-test the new parts separately.

  • When to bud off:

When writing a test, we ask ourselves, "If this worked, who would know?" If the right answer to that question is not in the target object, it's probably time to introduce a new collaborator.

  • When to bundle up:

When the test for an object becomes too complicated to set up [...] consider bundling up some of the collaborating objects.

  • Use interfaces to name roles played by objects. Keep interfaces narrow in scope.

  • Goal is to move to "higher-order" programming: "composing programs from smaller programs".

  • Don't use mocks for third-party code, since it is usually not changeable. Use an adapter layer to implement interactions with third-party code.

© Hana Lee. Built using Pelican. Theme adapted from Giulio Fidente's pelican-svbhack.