Design Patterns «Prev Next»

Lesson 2 Designing reusable classes is challenging
Objective Learn the problem design patterns solve and how to apply them to build reusable classes.

Challenges When Designing Reusable Classes

“Code reuse” is often framed as just subclass and override. In practice, reuse fails when classes are tightly coupled to concrete implementations, hidden assumptions, or a single workflow. The GoF patterns offer proven ways to separate roles from implementations and to compose behavior flexibly in languages like Java and C++.

Why Reuse Is Hard (and Common Failure Modes)

How Design Patterns Help

Patterns make variation explicit and localize change:

A Practical OO Workflow for Reusable Designs

  1. Capture use cases and variability: Identify actors, responsibilities, and where behavior must vary.
  2. Define interfaces first: Model roles and contracts (preconditions, postconditions, invariants) before picking classes.
  3. Prefer composition: Inject collaborators (strategies, policies, data sources) via constructors or setters.
  4. Prototype, then refactor with patterns: Build a simple, working slice; refactor hotspots into patterns as variability emerges.
  5. Stabilize APIs: Add tests, examples, and deprecation notes; document thread-safety and error semantics.

Abstract Data Types, Interfaces, and Contracts

An Abstract Data Type (ADT) defines what you can do-its operations and guarantees-without exposing how it’s implemented. Classes (Java/C++) are a common way to implement ADTs: keep representation private, expose a minimal, coherent set of operations, and enforce invariants through method contracts.

Figure 2.2 - Abstract Data Type (ADT): data values are hidden; clients access state only via the ADT’s public operations.
Figure 2.2 - Diagram of an ADT (Abstract Data Type).

Mini Examples: ADT + Contracts

Java - a minimal Stack ADT (composition-friendly, no leaks of representation):


// Pre: element != null
// Post: size() == oldSize + 1
public interface Stack<T> {
  void push(T element);
  // Pre: !isEmpty()
  // Post: returns last pushed element; size() == oldSize - 1
  T pop();
  T peek();      // Pre: !isEmpty()
  boolean isEmpty();
  int size();
}

C++ - strong RAII and clear ownership (no exposure of internals):


class IntStack {
public:
  void push(int v);              // pre: true; post: size == old_size + 1
  int  pop();                    // pre: size > 0; post: size == old_size - 1
  int  top() const;              // pre: size > 0
  bool empty() const noexcept;
  std::size_t size() const noexcept;

private:
  std::vector<int> data_;       // representation hidden (ADT boundary)
};

Checklist: Making a Class Reusable


Subclass: A class derived from another, inheriting its interface and (optionally) behavior.

Abstract Data Type (ADT): A type defined by its observable behavior (operations and contracts), not its representation.


SEMrush Software 2 SEMrush Banner 2