Section9.9Iterative 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.
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.
// 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.
// 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:
// 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.
Subsection9.9.4Updating 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.
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.
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.
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:
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: