Beck K., Implementation Patterns (2007)

 |  Book Reviews

The book is about code design—literally, how we construct meanings through code. It concisely presents a wide set of techniques and conventions that improve readability, communicate intent clearly, and make code easier to modify and use.

There is a number of great basic books on the subject (1, 2, 3). This one is a good addition in a concise form.

Contents

Summary

The book is pretty short and provides a directory of techniques and conventions on writing code in Classes, reflecting State and Behavior using Methods, forming Collections. On top it considers the issues of writing frameworks vs client code (ch. 10).

book overview

Theory of Programming

Values

[Beck K.] Three values that are consistent with excellence in programming are communication, simplicity, and flexibility. While these three sometimes conflict, more often they complement each other. The best programs offer many options for future extension, contain no extraneous elements, and are easy to read and understand (p. 27).

[HINT] Add the SVG Navigator extension to your Chrome to comfortably view the diagrams with zoom and pan in the separate tab.

Values

Principles

[Beck K.] Principles are another level of general ideas, more specific to programming than the values, that also form the foundation of the patterns...

Clear principles can lead to new patterns, just as the periodic table of the elements led to the discovery of new elements. Principles can provide an explanation for the motivation behind a pattern, one connected to general rather than specific ideas. Choices about contradictory patterns are often best discussed in terms of principles rather than the specifics of the patterns involved. Finally, understanding principles provides a guide when encountering novel situations.

Principles

Patterns

Class

[Beck K.] Classes are important for communication because they describe, potentially, many specific things. Class-level patterns have the largest span of any of the implementation patterns. Design patterns, by contrast, generally talk about the relationships between classes (p. 39).

Pattern Description
Class Use a class to bundle related data and logic that change at different rates, clarifying structure and enhancing communication.
Simple Superclass Name Name the roots of class hierarchies with simple, metaphor-driven names to make them central and memorable concepts.
Qualified Subclass Name Name subclasses by adding modifiers to the superclass name to clearly show their relationship and distinction.
Abstract Interface Separate the interface from the implementation. Employ an abstract interface (Java interface or superclass) to hide implementation details and gain flexibility where needed.
Interface Specify an abstract interface which doesn't change often with a Java interface. Use a Java interface for abstract interfaces unlikely to change often, effectively hiding implementation details from users.
Versioned Interface Extend interfaces safely by introducing a new sub-interface. Create a new sub-interface extending an old one to add operations without breaking existing implementors.
Abstract Class Specify an abstract interface which will likely change with an abstract class. Use an abstract class for abstract interfaces where you anticipate adding methods with default implementations later, easing future evolution.
Value Object Write an object that acts like a mathematical value. Use Value Objects to represent unchanging, mathematical values in situations where state persistence isn't needed, creating clear, timeless data structures.
Specialization Clearly express the similarities and differences of related computations. Use techniques like subclassing or delegation to clearly communicate how related computations are similar yet different, aiding understanding and modification.
Subclass Express one-dimensional variation with a subclass. Use a subclass to define a variation of an existing class, ideally by overriding just one method, to clearly express similarity with specific differences.
Implementor Override a method to express a variant of a computation. Implement the same protocol (e.g., override a method) in different classes to express variations polymorphically, allowing callers to abstract away implementation details.
Inner Class Bundle locally useful code in a private class. Use a private inner class to encapsulate code that is only relevant and used within a single outer class, providing structure with minimal overhead.
Instance-specific Behavior Vary logic by instance. Apply patterns like Conditional or Delegation when individual instances need different behavior, acknowledging the added complexity in reading the code.
Conditional Vary logic by explicit conditionals. Use if/then or switch statements for simple variations in logic based on data, keeping the behavior definition localized, but be wary of duplication and complexity.
Delegation Vary logic by delegating to one of several types of objects. Employ delegation to vary logic per instance by having the instance pass work to different helper objects, separating common behavior from variations.
Pluggable Selector Vary logic by reflectively executing a method. Use a pluggable selector when you need instance-specific behavior for one or two methods by storing method names and invoking them reflectively, accepting reduced static analysis.
Anonymous Inner Class Vary logic by overriding one or two methods right in the method that is creating a new object. Use an anonymous inner class for simple, localized instance-specific behavior overriding minimal methods, trading testability for conciseness.
Library Class Represent a bundle of functionality that doesn't fit into any object as a set of static methods. Group functionality that doesn't belong to a specific object into a class with static methods, but recognize its limitations for scalability and object-oriented benefits.

State

[Beck K.] The patterns in this chapter describe how to communicate your use of state. Objects are convenient packages of behavior which is presented to the outside world and state which is used to support that behavior (p. 60).

Pattern Description
State Compute with values that change over time. Use state to model values that change over time, managing complexity by grouping similar state within objects.
Access Maintain flexibility by limiting access to state. Control how state is accessed (directly vs. indirectly) to balance clarity, flexibility, and object independence.
Direct Access Directly access state inside an object. Use direct variable access for clarity when referring to stored values, but be aware it reduces flexibility and may expose implementation details.
Indirect Access Access state through a method to provide greater flexibility. Use accessor methods (getters/setters) to hide state access, offering flexibility to change storage details, but be mindful of complexity and clarity trade-offs.
Common State Store the state common to all objects of a class as fields. Represent data elements shared by all instances of a class as fields, clearly defining the essential state for an object.
Variable State Store state whose presence differs from instance to instance as a map. Use a map to store state that varies between instances of the same class, providing flexibility but reducing clarity about required data elements.
Extrinsic State Store special-purpose state associated with an object in a map held by the user of that state. Keep state relevant to only a specific part of the system (not core to the object) external to the object, typically in a map held by the user, to maintain object clarity and symmetry.
Variable Variables provide a namespace for accessing state. Variables provide access to state; focus on communicating their role through simple names, relying on context for scope, lifetime, and type.
Local Variable Local variables hold state for a single scope. Use local variables for state within a single scope, declaring them just before use for clarity; common roles include collecting results, counting, explaining complex expressions, reusing values, and holding collection elements.
Field Fields store state for the life of an object. Use fields to store state for the lifetime of an object, declaring them together for clarity; common roles include helpers, flags, strategies, state implementations, and components.
Parameter Parameters communicate state during the activation of a single method. Pass state between objects using parameters; this creates weaker coupling than permanent references, but many parameters may indicate a need for restructuring.
Collecting Parameter Pass a parameter to collect complicated results from multiple methods. Use a mutable object passed as a parameter to collect and merge complex results from multiple method calls.
Optional Parameter Pass a parameter to collect complicated results from multiple methods. Structure method/constructor overloads to allow callers to omit trailing parameters, using defaults.
Var Args Consolidate frequently used long parameter lists into an object. Use Java's `...` syntax to accept a variable number of arguments of a type.
Parameter Object Store state that doesn't vary as a constant. Group frequently passed parameters into a dedicated object to clarify intent and shorten signatures.
Constant Return boolean values with methods named asXXX. Store unchanging data values needed widely as `static final` variables.
Role-Suggesting Name Declare a general type for variables. Name variables based on their role in computation, relying on context for other details.
Declared Type Initialize variables declaratively as much as possible. Declare variables with the most general type that indicates intended usage, not specific implementation.
Initialization Initialize fields at instance creation time. Initialize variables to a known state before use, favoring declarative initialization.
Eager Initialization Initialize fields whose values are expensive to calculate just before they are first used. Initialize variables immediately upon creation (declaration or constructor).
Lazy Initialization Compute with values that change over time. Defer expensive variable initialization until first access for performance.

Behavior

(p. 80)

Pattern Description
Control Flow Express computations as a sequence of steps, using the language to decide how the flow is expressed (sequence, conditionals, loops, messages, exceptions).
Main Flow Clearly express the main flow of control for your program, which is typically the path to follow despite decisions and exceptions.
Message Express control flow and logic by sending a message, acknowledging that change is the base state of programs and allowing the receiver to be changed without changing the sender.
Choosing Message Send a polymorphic message to vary the implementors and express choices that take place at runtime, leading to code with few explicit conditionals.
Double Dispatch Cascade two choosing messages to vary the implementors along two independent dimensions and express cascading choices.
Decomposing (Sequencing) Message Group related steps in a complicated algorithm by sending a message to break complicated calculations into cohesive chunks, using the message to invoke a sub-sequence of steps.
Reversing Message Introduce a helper method and send a sequence of messages to the same receiver (e.g., `this`) to make control flows symmetric and improve readability.
Inviting Message Send an appropriately named message to communicate the possibility of later refinement in a subclass, inviting future variation by sending a message that can be implemented in different ways.
Explaining Message Send a message named after the problem you are solving to communicate intention and explain the purpose of a clump of logic, especially useful when tempted to comment a single line.
Exceptional Flow Express the unusual or less-frequently executed paths of control as clearly as possible without obscuring the main flow, using mechanisms like guard clauses and exceptions.
Guard Clause Use an early return or `continue` to express local exceptional flows, simple and local situations with purely local consequences, without complex control structures.
Exception Use exceptions to express non-local exceptional flows, jumps in program flow that span levels of function invocation, when a problem occurs high on the stack and can only be handled lower down.
Checked Exception Declare exceptions explicitly and use compiler checking to ensure that exceptions are caught, preventing abrupt program termination due to uncaught exceptions.
Exception Propagation Propagate exceptions up the call stack, wrapping low-level exceptions in higher-level ones and transforming them as necessary, so the information is appropriate to the catcher and useful for diagnosis.

Methods

(p. 92)

Pattern Description
Composed Method Compose methods out of calls to other methods, ensuring each method is at roughly the same level of abstraction, which helps readability by avoiding abrupt shifts in detail and allows for easier specialization.
Intention-Revealing Name Name methods based on the purpose they serve to a potential invoker, communicating the intent of the computation rather than its implementation strategy, and ensuring the name helps tell the story in the calling code.
Method Visibility Control the visibility of methods (public, package, protected, private) to balance the need to reveal functionality with the need to maintain future flexibility, generally restricting visibility as much as possible initially and revealing it only as necessary.
Method Object Refactor a long, complex method with many parameters and temporary variables into its own object, simplifying the original method and providing a place to organize the details, making the code more readable and easier to refactor further.
Overridden Method Use overridden methods in subclasses as a clear way to express variations of similar calculations defined in the superclass, providing potential hooks for specialized behavior, particularly effective when the superclass methods are well-composed.
Overloaded Method Declare methods with the same name but different parameter types to provide alternative formats for method parameters, which should serve the same purpose and generally avoid varying by return type, relieving callers of parameter conversion responsibility.
Method Return Type Choose a return type that expresses the intention of the method, preferring the most abstract type possible to preserve flexibility for future changes and to hide implementation details, unless a specific concrete type is necessary.
Method Comment Add comments to communicate information that is not easily read from the code, such as constraints between methods or the purpose of methods and classes (like javadoc), although clear naming and structure are preferred where possible.
Helper Method Create small, private methods as a consequence of using composed methods to make larger computations more readable, hiding temporarily irrelevant details and expressing intention through their names; they can also serve to eliminate common sub-expressions.
Debug Print Method Override the `toString()` method to provide a programmer-friendly representation of an object's internal state for debugging purposes, keeping other string renderings separate for different purposes.
Conversion Express the conversion from one type of object (source) to another (destination) cleanly, considering the number of conversions needed and dependencies between classes, and focusing on communicating the programmer's intention.
Conversion Method Represent conversion as a method on the source object, useful for simple, limited conversions between objects of similar type, where the method returns the converted destination object.
Conversion Constructor Provide a constructor on the destination object's class that takes the source object as a parameter, useful when converting one source object into many different destinations, as it avoids cluttering the source object with numerous conversion methods.
Creation Express object creation clearly, balancing clear and direct expression with the need for flexibility for future changes. This involves deciding how objects are made and what information is needed for a well-formed object.
Complete Constructor Provide constructors that ensure objects are returned in a fully formed state ready to compute, communicating the necessary prerequisites to potential users and funneling initialization to a single master constructor to ensure invariants are satisfied.
Factory Method Represent more complex object creation as a static method on a class rather than solely using constructors, allowing for returning more abstract types or named creation methods that communicate intention, and are helpful when creation involves caching or runtime subclass selection.
Internal Factory Create a private method within a class to handle the creation of a helper object when that creation is complex or may need to change (potentially in subclasses), often used in lazy initialization or when a computation uses different data structures.
Collection Accessor Method Provide controlled access to internal collections within an object through specific methods (like add, remove, size, or iterators) rather than exposing the entire collection directly, which helps protect the object's internal state and provides opportunities for a richer protocol.
Boolean Setting Method Offer methods to set boolean state, potentially providing two intention-revealing methods (e.g., `valid()`, `invalid()`) instead of a single `setValid(boolean)` method, especially when all calls use constant boolean values, to improve readability.
Query Method Provide methods that return boolean values about an object's state, typically named with prefixes like "is" or "has" (e.g., `isVisible()`), although excessive use can indicate that logic dependent on the state might be better placed within the object itself.
Equality Method Implement the `equals()` and `hashCode()` methods together when objects need to be compared by value rather than identity (e.g., for use in hash tables), ensuring that objects considered equal have the same hash code and basing computations only on the data used for equality comparison.
Getting Method Provide a method (conventionally prefixed with "get") to return an object's state, useful when algorithms in separate objects need access to data, or when a public method happens to return a field's value; overuse can suggest misplaced logic or implementation leakage.
Setting Method Provide a method (conventionally prefixed with "set") to set the value of a field, generally less visible than getting methods and ideally named after the client's problem rather than the implementation; can be useful internally for updating dependent information but can make code brittle if overly exposed.
Safe Copy Create a copy of an object before returning it from a getting method or storing it in a setting method to prevent aliasing problems where multiple objects assume exclusive access to the same object, useful as a palliative but often indicative of deeper design issues.

Collections

Concepts (Issues)

(p. 118)

  • Size: Whether the collection's size is fixed (like an array) or can change.
  • Order: Whether the sequence of elements is important, possibly based on insertion or some external comparison.
  • Uniqueness: Whether duplicate elements are allowed or if the collection guarantees uniqueness (like a Set).
  • Access Method: How elements are typically accessed, either by iterating over them or by retrieving them using a key (like in a Map).
  • Performance Considerations: The choice of collection also communicates decisions related to the performance characteristics of operations like searching, adding, or removing elements.

Interfaces

The interface declaration tells the reader about the collection: whether the collection is in a particular order, whether there are duplicate elements, and whether there is any way to look up elements by key or only through iteration (p. 120).

Interface Name Description Key Characteristics/Purpose Implications for Reader
Array The simplest and least flexible collection. Doesn't have the same protocol as other collections, making conversion harder. Fixed size, set at creation. Simple accessing syntax. More efficient in time and space for simple operations compared to other collections. Reader knows the size is fixed upon creation. Access is fast. Conversion to other collection types is more difficult. Can be a handy trick for performance in small parts of an application.
Iterable The basic collection interface. Says that the collection contains multiple values. Basis for the Java 5 for loop. Allows iteration over elements. Does not provide a way to measure size. Its helper, Iterator, allows removal of elements from the underlying Iterable. Reader knows they can iterate over the elements. Cannot assume anything about size from the interface alone. There's no declarative way to state if it shouldn't be modified, and elements can be removed via its Iterator's remove() method.
Collection Inherits from Iterable. Offers more useful behavior than just iteration. Adds methods to add, remove, search for, and count elements. Leaves many options for the implementation class. Allows testing for inclusion (like mathematical sets). Reader knows they can perform basic operations like adding, removing, searching, and getting the size. Declaring a variable as Collection retains freedom to change the implementation class later.
List Adds the idea that elements are in a stable order. Elements are ordered. Elements can be accessed by their location (index). Reader knows the sequence of elements is preserved and can access elements by index. Important when elements interact or external users rely on sequence (like arrival order).
Set A collection that contains no duplicates. Corresponds closely to the mathematical notion of set regarding uniqueness. Contains no elements that are equal() to each other. Discards information about the number of times an element appears. Elements are in no particular order. Reader knows duplicate elements are not permitted. Useful when only the presence or absence of an element matters. Cannot rely on the order of elements. Adding elements modifies the collection.
SortedSet Combines ordering and uniqueness attributes. Stores ordered-but-unique elements. Ordering is provided by a Comparator or the elements' natural order. Reader knows elements are unique and kept in a specific order (either natural or via a provided Comparator). Useful when you need ordered elements without duplicates.
Map A hybrid of the other interfaces. Stands alone as it doesn't completely fit other collection interfaces. Stores values by key. Keys must be unique. Values can contain duplicates. Elements are in no particular order (like a Set). Provides a collection of keys connected to a collection of values. Reader knows elements are accessed using a key rather than just iteration or index. Keys are unique, but values may not be. Order is not guaranteed. Useful for implementing concepts like extrinsic state or variable state.