Section9.10Inheritance, DRY Principle, and Code Reusability
One of the primary benefits of carefully planning your class hierarchy, particularly through the iterative refinement approach discussed earlier, is that it directly supports the DRY (Don’t Repeat Yourself) principle. The DRY principle emphasizes that you should avoid unnecessary duplication of code, logic, or data throughout your system. Proper use of inheritance is a powerful tool for adhering to this principle.
If multiple subclasses (like TextPost, PhotoPost, and VideoPost) share identical fields (author, timestamp, likeCount, taggedUsers) or methods (like(), tagUser(), addComment()), these are prime candidates to move up into a common superclass (Post).
If you find specific subsets of subclasses (like PhotoPost and VideoPost) sharing specialized attributes or behaviors (such as handling fileSize, resolution, or generating thumbnails), consider introducing an intermediate class (MediaPost).
public class Car {
private double speed;
private double positionX;
private double positionY;
public void accelerate(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount must be positive");
}
speed += amount;
}
public void brake(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount must be positive");
}
speed = Math.max(0, speed - amount);
}
// Other car-specific methods...
}
public class Bicycle {
private double speed;
private double positionX;
private double positionY;
public void accelerate(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount must be positive");
}
speed += amount;
}
public void brake(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount must be positive");
}
speed = Math.max(0, speed - amount);
}
// Other bicycle-specific methods...
}
public abstract class Vehicle {
protected double speed;
protected double positionX;
protected double positionY;
public void accelerate(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount must be positive");
}
speed += amount;
}
public void brake(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount must be positive");
}
speed = Math.max(0, speed - amount);
}
// Other common methods...
}
public class Car extends Vehicle {
// Only car-specific fields and methods
private double engineSize;
// Car-specific methods...
}
public class Bicycle extends Vehicle {
// Only bicycle-specific fields and methods
private int numberOfGears;
// Bicycle-specific methods...
}
Notice how the inheritance-based approach eliminates duplicated fields and methods, resulting in cleaner, more maintainable code.
Introducing new subclasses (e.g., Motorcycle or Scooter) becomes straightforward because the superclass already contains the essential shared behaviors.
public abstract class Vehicle {
// Existing fields and methods...
// New functionality added once, benefits all subclasses
private List<Position> positionHistory = new ArrayList<>();
public void recordPosition() {
positionHistory.add(new Position(positionX, positionY));
}
public List<Position> getRouteHistory() {
return new ArrayList<>(positionHistory); // Return defensive copy
}
}
With this single change, every vehicle type now has position tracking capabilities, without having to modify any subclasses.
Subsection9.10.3Using Interfaces to Complement Inheritance
While inheritance handles "is-a" relationships, interfaces are ideal for capturing cross-cutting capabilities. For example, suppose your traffic simulation application also supports other entities like TrafficLight and PedestrianCrossing, both of which can be "located on a map" or "toggled." However, these entities don’t share a direct inheritance relationship with Vehicle.
// Interface defining a capability to be placed on a map
public interface Mappable {
double getPositionX();
double getPositionY();
void setPosition(double x, double y);
}
// Interface defining something that can be turned on/off
public interface Toggleable {
void turnOn();
void turnOff();
boolean isOn();
}
// Vehicle superclass implements Mappable
public abstract class Vehicle implements Mappable {
protected double positionX;
protected double positionY;
@Override
public double getPositionX() {
return positionX;
}
@Override
public double getPositionY() {
return positionY;
}
@Override
public void setPosition(double x, double y) {
this.positionX = x;
this.positionY = y;
}
}
// TrafficLight class implements both Mappable and Toggleable
public class TrafficLight implements Mappable, Toggleable {
private double positionX;
private double positionY;
private boolean isOn;
// Mappable implementation
@Override
public double getPositionX() { return positionX; }
@Override
public double getPositionY() { return positionY; }
@Override
public void setPosition(double x, double y) {
this.positionX = x;
this.positionY = y;
}
// Toggleable implementation
@Override
public void turnOn() { isOn = true; }
@Override
public void turnOff() { isOn = false; }
@Override
public boolean isOn() { return isOn; }
}
Using interfaces in conjunction with inheritance gives you the flexibility to model shared behaviors across unrelated class hierarchies, further enhancing reusability and adhering to DRY.
Remember that inheritance creates both an is-a relationship (abstraction) and a mechanism for code reuse (implementation). The abstraction aspect should drive your design decisions, with code reuse as a beneficial consequence.
A common mistake is to create inheritance hierarchies solely for code reuse without a meaningful abstraction. This leads to confusing designs where the "is-a" relationship doesn’t make intuitive sense. Always ask: "Does it make sense to say that SubclassX is a SuperclassY?" If not, consider composition or interfaces instead.