reading notes on "design patterns: elements of reusable object-oriented software"

2021-06-01

 | 

~5 min read

 | 

901 words

I’m actively reading the Gang of Four (aka Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides’s Design Patterns: Elements of Reusable Object-Oriented Software). Below are some of my notes. I’ll continue to fill this in as I make progress / learn more.

More than anything else, the uncovering and mastery of powerful organizational techniques accelerates our ability to create large, significant programs.

Harold Abelson, Gerald Jay Sussman, Julie Sussman. Structure and Interpretation of Computer Programs, Second Edition.

Structural Patterns

Composite Pattern

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

Simplify the client code by allowing the client to not know (or care) about the differences between elements in the composition.

The two component pieces are Composites and Leafs. Composites are components that can have children.

Much of the discussion is around where to put declaration and implementation for certain methods that are not equally applicable to all subclasses, e.g., add and remove. There’s a trade-off between safety and transparency:

Implement methods in … Safety Transparency
Root ❌ - Leaves may try to add children, which doesn’t make sense ✅ - All classes share the same API
Composite ✅ - Compile time checks ensure that disallowed methods are caught ❌ - Composite and Leaves have different interfaces

This tradeoff reveals a tension with the Principle of Class Hierarchy Design which says a class should only define operations that are meaningful to its subclasses. One clever solution: have the root (Component) define children, but the default implementation is return no children, leaving it to the Composite to implement its own method. In this way all classes have the same API, but it only performs the desired / expected behavior where it should. This, however, opens up a new plane for bugs in that a Leaf could try to access its children and while it won’t work, it shouldn’t have attempted in the first place.

Determining where / how to share components is another potential design decision of the Composite pattern. To ensure that the parent/child relationship is stable, it’s useful to ensure that the only places where the relationship is modified is in the add / remove methods. However, this becomes trickier if you also want to share components (i.e., not create multiple instances and therefore reduce storage requirements). By implication, this means that a child can now have multiple parents.

Behavioral Patterns

Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use them.

Use the Strategy pattern if you have many classes that all implement a similar interface, but differ in how they perform a single algorithm. In this way, the Strategy pattern provides an alternative to subclassing. Instead of defining the different implementation in each subclass, you can abstract the algorithmic differences to Strategy subclasses and allow your main class to evolve independently.

The “Composition” (the class that’s being invoked) sends a request to the “Compositor” (the class that declares the common interface for the various strategies). The context of that request should be sufficient for the Compositor to know which algorithms to invoke (i.e., which strategy is called for).

In this way, there are three participants in the Strategy Pattern:

  1. The Compositor (Strategy) - which defines the common interface for all supported algorithms and is used by the Context.
  2. ConcreteStrategy - the implementation of the algorithms using the Compositor’s interface
  3. Context (Composition) - configured to use a ConcreteStrategy, with a reference to the Strategy object and which may define an interface that grants Strategy access to its data

Some of the benefits / drawbacks of the Strategy pattern:

  • Group related algorithms: by utilizing the Strategy pattern, it’s clear to see a group of algorithms as peers within a hierarchy.
  • Alternative to subclassing: instead of writing the different variants of the algorithms directly into a subclass, the Strategy pattern enables this to be abstracted. Consequently, the Context is simpler, easier to reason through, and extend.
  • Context needs to be aware: In order to use the Strategy pattern , clients must understand the different strategies in order to select one.
  • Clients can choose the implementation: Because the Context has to be aware, the Client can make the decision on which algorithm it wants to use, accounting for space/time trade-offs higher up the stack where more context may be available to make an informed decision.
  • Mitigate case statements: By delegating the work to task to the Strategy Object, the Context will not need to include a case statement to determine which algorithm to use.

When implementing a Strategy pattern, there are three primary modes of defining the communication between the Context and ConcreteStrategy (by way of the Strategy):

  1. Pass parameters: This is the most explicit, however, because the Strategy class requires a common interface, the risk is that you will send extraneous information to certain algorithms.
  2. Context passes itself: By passing itself, the Context defers to the Strategy class to extract the necessary information.
  3. Strategy maintains a reference to Context: With this approach, the Context passes nothing and relies on the Strategy to extract what it needs.

Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!