Skip to main content

Section 9.9 Iterative Refinement of Class Hierarchy Across Steps 0–2

Software design is rarely perfect on the first attempt. While Steps 0, 1, and 2 of the Design Recipe provide a structured approach to designing class hierarchies, they aren’t strictly linear. Instead, you should anticipate cycling through these steps iteratively. As you refine your understanding (Step 0), document data definitions (Step 1), and outline method signatures (Step 2), you will naturally discover opportunities to adjust and improve your class hierarchy.

Subsection 9.9.1 Recognizing the Need to Iterate

As you develop your understanding of the problem domain, you might discover:
  • New shared attributes or behaviors among a subset of your classes
  • Different ways to categorize your objects
  • Edge cases that don’t fit neatly into your original hierarchy
  • New requirements that suggest different abstractions
When these discoveries occur, it’s a signal to revisit Steps 0-2 of the design recipe and refine your hierarchy.
For example, in our social media application, you might initially design separate implementations of a tagUser() method in each subclass—TextPost, PhotoPost, and VideoPost. However, upon reviewing these implementations, you realize the code is identical in each class. This redundancy signals an opportunity to move the tagUser() method up to the common superclass, Post.
This realization might prompt you to revisit Steps 0-2:
  • Step 0 (Restatement): Update your problem restatement to reflect the new understanding that all posts must support tagging users.
  • Step 1 (Data Definitions): Move taggedUsers field and related invariants to the superclass.
  • Step 2 (Method Signatures): Clearly document the shared tagUser() method and any invariants or constraints associated with it.
Iterative refinement is crucial to catching design issues early, making your inheritance hierarchy stronger and more maintainable.

Subsection 9.9.2 Example of Iterative Refinement

Let’s see a concrete example of iterative refinement with our social media post hierarchy:
Post (abstract)
 ├── TextPost
 ├── PhotoPost
 └── VideoPost
In our initial design, we had a single Post superclass with three direct subclasses.
Post (abstract)
 ├── TextPost
 └── MediaPost (abstract)
      ├── PhotoPost
      └── VideoPost
After further analysis, we recognized that photo and video posts share media-related functionality, so we introduced an intermediate abstract class.
Initially, your PhotoPost and VideoPost classes each contained similar methods for handling media attributes:
// PhotoPost initially
public class PhotoPost extends Post {
    private String imageFile;
    private String caption;
    private int fileSize;
    private String resolution;
    
    public String generateThumbnail() {
        // Photo-specific thumbnail generation
        return "thumbnail_" + imageFile;
    }
    
    public void updateResolution(String newResolution) {
        if (newResolution == null || !isValidResolution(newResolution)) {
            throw new IllegalArgumentException("Invalid resolution format");
        }
        this.resolution = newResolution;
    }
    
    private boolean isValidResolution(String resolution) {
        // Check if resolution is in format "WidthxHeight"
        // For example: "1920x1080"
        if (resolution == null) {
            return false;
        }
        
        // Split the string by the 'x' character
        String[] parts = resolution.split("x");
        
        // Check if we have exactly two parts (width and height)
        if (parts.length != 2) {
            return false;
        }
        
        try {
            // Try to convert both parts to numbers
            int width = Integer.parseInt(parts[0]);
            int height = Integer.parseInt(parts[1]);
            
            // Make sure both width and height are positive
            return width > 0 && height > 0;
        } catch (NumberFormatException e) {
            // If we can't convert to numbers, the format is invalid
            return false;
        }
    }
}

// VideoPost initially
public class VideoPost extends Post {
    private String videoFile;
    private String description;
    private int duration;
    private int fileSize;
    private String resolution;
    
    public String generateThumbnail() {
        // Video-specific thumbnail generation
        return "thumbnail_" + videoFile;
    }
    
    public void updateResolution(String newResolution) {
        if (newResolution == null || !isValidResolution(newResolution)) {
            throw new IllegalArgumentException("Invalid resolution format");
        }
        this.resolution = newResolution;
    }
    
    private boolean isValidResolution(String resolution) {
        // Check if resolution is in format "WidthxHeight"
        // For example: "1920x1080"
        if (resolution == null) {
            return false;
        }
        
        // Split the string by the 'x' character
        String[] parts = resolution.split("x");
        
        // Check if we have exactly two parts (width and height)
        if (parts.length != 2) {
            return false;
        }
        
        try {
            // Try to convert both parts to numbers
            int width = Integer.parseInt(parts[0]);
            int height = Integer.parseInt(parts[1]);
            
            // Make sure both width and height are positive
            return width > 0 && height > 0;
        } catch (NumberFormatException e) {
            // If we can't convert to numbers, the format is invalid
            return false;
        }
    }
}
Recognizing the duplication in media-related functionality, we refine our hierarchy by creating an intermediate MediaPost abstract class:
// Refined design with intermediate class
public abstract class MediaPost extends Post {
    protected int fileSize;
    protected String resolution;
    
    // Shared media-specific functionality
    public abstract String generateThumbnail();
    
    public void updateResolution(String newResolution) {
        // This validation logic is shared by all media posts
        if (newResolution == null || !isValidResolution(newResolution)) {
            throw new IllegalArgumentException("Invalid resolution format");
        }
        this.resolution = newResolution;
    }
    
    protected boolean isValidResolution(String resolution) {
        // This method checks if resolution is in format "WidthxHeight"
        // For example: "1920x1080"
        // All media posts need this validation, so we put it in the superclass
        
        if (resolution == null) {
            return false;
        }
        
        // Split the string by the 'x' character
        String[] parts = resolution.split("x");
        
        // Check if we have exactly two parts (width and height)
        if (parts.length != 2) {
            return false;
        }
        
        try {
            // Try to convert both parts to numbers
            int width = Integer.parseInt(parts[0]);
            int height = Integer.parseInt(parts[1]);
            
            // Make sure both width and height are positive
            return width > 0 && height > 0;
        } catch (NumberFormatException e) {
            // If we can't convert to numbers, the format is invalid
            return false;
        }
    }
}

public class PhotoPost extends MediaPost {
    private String imageFile;
    private String caption;
    
    @Override
    public String generateThumbnail() {
        // Photo-specific implementation
        return "thumbnail_" + imageFile;
    }
}

public class VideoPost extends MediaPost {
    private String videoFile;
    private String description;
    private int duration;
    
    @Override
    public String generateThumbnail() {
        // Video-specific implementation
        return "thumbnail_" + videoFile;
    }
}
This refinement allows us to place media-related fields and methods in the MediaPost class, where they logically belong, while keeping photo-specific and video-specific functionality in their respective subclasses.

Subsection 9.9.3 Another Example: Recognizing Identical Methods

Similarly, you might initially implement user tagging independently in each post class, only to discover the implementation is identical:
// Initial implementation in each post class:

// In TextPost initially
public void tagUser(User user) {
    // This exact code was duplicated in each post class
    if (user == null) {
        throw new IllegalArgumentException("Tagged user must not be null");
    }
    taggedUsers.add(user);
}

// In PhotoPost - notice the duplication!
public void tagUser(User user) {
    // Same code duplicated
    if (user == null) {
        throw new IllegalArgumentException("Tagged user must not be null");
    }
    taggedUsers.add(user);
}
This duplication is a clear signal to refine your hierarchy. You would revisit the first three steps:
  • Step 0 revisited: Update your problem statement to note that "all post types support user tagging."
  • Step 1 revisited: Move the taggedUsers field up to the Post superclass.
  • Step 2 revisited: Move tagUser() up to the superclass, so it’s implemented only once.
After refinement, you’d have a single implementation in the superclass:
// In Post superclass after refinement
public void tagUser(User user) {
    // Now implemented only once, for all post types
    if (user == null) {
        throw new IllegalArgumentException("Tagged user must not be null");
    }
    taggedUsers.add(user);
}
This cycle ensures your class hierarchy stays well-organized and reduces code duplication.

Subsection 9.9.4 Updating Data Definitions and Method Signatures

When you refine your hierarchy, you need to revisit and update your Step 1 (data definitions) and Step 2 (method signatures) artifacts. This ensures your documentation and design remain in sync with your evolving understanding.
For each refinement:
  • Update your class diagrams or tables to reflect the new hierarchy
  • Move fields to their appropriate level in the hierarchy
  • Adjust method signatures to reflect new class relationships
  • Revise purpose statements to clarify new responsibilities
Here’s an example of a revised data definition table after creating the MediaPost intermediate class:
Class Fields Invariants
Post (abstract)
author: String
timestamp: Date
likeCount: int
comments: List<Comment>
taggedUsers: List<User>
author not null or empty
timestamp not null
MediaPost (abstract)
fileSize: int
resolution: String
fileSize > 0
resolution in format "WidthxHeight"
TextPost
textContent: String
wordCount: int
textContent not null
PhotoPost
imageFile: String
caption: String
imageFile not null or empty
VideoPost
videoFile: String
description: String
duration: int
videoFile not null or empty
duration > 0
Notice how fileSize and resolution have been moved from PhotoPost and VideoPost to the new intermediate MediaPost class, and taggedUsers has been added to the Post superclass.

Subsection 9.9.5 Signals That Refinement Is Needed

How do you know when to refine your hierarchy? Look for these signals:
  • Duplicated code in multiple subclasses that could be unified
  • Conditional logic based on object type (suggesting missing subclasses)
  • Fields or methods that only apply to some subclasses but not others
  • Conceptual groupings that aren’t reflected in your hierarchy
  • Awkward method overrides that feel forced or unnatural
For example, if you notice that both PhotoPost and VideoPost have similar methods for generating thumbnails and managing media resolution, this suggests they share functionality that could be unified in a common parent class.

Insight 9.9.1. Iterative Refinement Saves Time.

Iterating through Steps 0–2 as soon as you identify duplication or overlooked shared behavior prevents costly refactoring later. While refinement requires some upfront investment, it pays significant dividends by:
  • Reducing code duplication and maintenance burden
  • Improving clarity of the design
  • Making future extensions more straightforward
  • Creating more robust, bug-resistant code
Each iteration clarifies your design further, reducing the likelihood of errors and improving code quality.

Subsection 9.9.6 Balancing Refinement with Progress

While refinement is valuable, excessive perfectionism can impede progress. You don’t need a perfect hierarchy before moving to implementation. Here are some guidelines for balancing refinement with forward progress:
  • Focus on refinements that eliminate significant duplication
  • Prioritize changes that improve understanding or maintainability
  • Consider deferring refinements that can be made easily later
  • Use interfaces to capture cross-cutting concerns without disrupting the hierarchy
Remember that iteration is normal. No design is perfect on the first try, and the goal is improvement, not perfection.
Next, we’ll explore how well-designed class hierarchies directly support the DRY principle and improve code reusability.
You have attempted of activities on this page.