Parallel Inheritance Hierarchies

Description (What)

The structure of your program is such that creating a sub-class for a class leads you to having to create a sub class for another class.

How to Locate It (Where)

Look for inheritance hierarchies that are paralleled throughout your program. Commonly occurs when you have a two or more different interfaces and their respective classes which appear logically similar but are structured in such a way that making changes to one makes you think about making changes to the other.

How It Manifests (Why)

Often when creating a new inheritance hierarchy there is a subconscious need to create inheritance hierarchies of objects that are used in the new inheritance hierarchy or for the sake of creating interfaces for future extendability.

This can be manageable when having small hierarchies but when classes and interfaces increase, it can become hard to manage.

How to Fix It (Possible Treatments)

In Example 1, a combination of two treatments, Move Field and Move Method are used to create a new simpler interface that combines two previous separated interfaces.

This way, new classes created to inherit the interface no longer need to be made twice for both previous interfaces.

In Example 2, also apply a combination of Move Field and Move Method, since the fields used in some methods need to move with those methods,

and two parallel class structure could be managed through one central platform, to which the methods and fields move to.

Other treatments are also possible based on the specific scenario, they can be found here

Example 1

Before:

There is a parallel hierarchy between the Weapon interface and Enemy interface where, each time a new enemy is made (for eg. Werewolf), a new weapon needs to be made to complement it (WerewolfWeapon).

Observed Code Smells:
- Parallel Inheritance Hierarchies (lines 66-118)

After:

Removed hierarchy in VampireWeapon and GhoulWeapon by replacing them with a BasicWeapon and moved their fields and methods to the new BasicWeapon class using Move Method and Move Field.

Refactoring Applied:
- Parallel Inheritance Hierarchies
    - Move Method and Move Field (BasicWeapon)
Observed Code Smells After Refactoring:
- None

Example 2

Before:

Suppose we have 2 operating systems in the Platform Enum, which are iOS and Android.

They both extends the same set of abstract classes(super classes), but now for the same super classes,

a newly-added child class for iOS must lead to a newly-added child class for Android.

This constitutes a Parallel Inheritance Hierarchies.

Observed Code Smells:
- Parallel Inheritance Hierarchies (lines 28-40, lines 46-58, lines 65-87)

After:

Instead of having separate classes for different operating systems to implement the same contract,

have a operatingSystem to extend the super class which defines the functions and use EnumMap inside that

operatingSystem class to switch among different operating systems.


Refactoring Applied:
- Parallel Inheritance Hierarchies:
   - Move Method and Move Field (lines 32-57, lines 63-88, lines 95-115)
Observed Code Smells After Refactoring:
- None

When to Ignore

Sometimes having parallel inheritance hierarchies can make sense, where applying the treatments can lead to messier code.

More

More about Parallel Inheritance Hierarchies

001  interface Enemy {
002   public void attack();
003  
004   public void defend();
005  }
006  
007  class Vampire implements Enemy {
008   private Weapon shotgun;
009   private int defense;
010  
011   public Vampire(Weapon shotgun, int defense) {
012   this.shotgun = shotgun;
013   this.defense = defense;
014   }
015  
016   @Override
017   public void attack() {
018   if(shotgun.inspectHealth() > 0) {
019   System.out.println("Vampire attacks. Damage done: " + this.shotgun.getDamage());
020   } else {
021   System.out.println("Weapon broken. Vampire can't repair weapon");
022   }
023   }
024  
025   @Override
026   public void defend() {
027   System.out.println("Blocking attack with defense: " + (this.defense + 10));
028   }
029  
030  }
031  
032  class Ghoul implements Enemy {
033   private Weapon bazooka;
034   private int defense;
035  
036   public Ghoul(Weapon bazooka, int defense) {
037   this.bazooka = bazooka;
038   this.defense = defense;
039   }
040  
041   @Override
042   public void attack() {
043   if(bazooka.inspectHealth() > 0) {
044   System.out.println("Ghoul attacks. Damage done: " + this.bazooka.getDamage());
045   } else {
046   System.out.println("Weapon broken. Repairing...");
047   this.bazooka.repair();
048   }
049   }
050  
051   @Override
052   public void defend() {
053   System.out.println("Blocking attack with defense: " + (this.defense + 20));
054   }
055  
056  }
057  
058  interface Weapon {
059   public int getDamage();
060  
061   public void repair();
062  
063   public int inspectHealth();
064  }
065  
066  class VampireWeapon implements Weapon {
067   private int damage;
068   private int health;
069  
070   public VampireWeapon(int damage) {
071   this.damage = damage;
072   this.health = 30;
073   }
074  
075   @Override
076   public int getDamage() {
077   this.health -= 10;
078   return damage;
079   }
080  
081   @Override
082   public void repair() {
083   this.health = 100;
084   }
085  
086   @Override
087   public int inspectHealth() {
088   return this.health;
089   }
090  
091  }
092  
093  class GhoulWeapon implements Weapon {
094   private int damage;
095   private int health;
096  
097   public GhoulWeapon(int damage) {
098   this.damage = damage;
099   this.health = 200;
100   }
101  
102   @Override
103   public int getDamage() {
104   this.health -= 10;
105   return damage;
106   }
107  
108   @Override
109   public void repair() {
110   this.health = 100;
111   }
112  
113   @Override
114   public int inspectHealth() {
115   return this.health;
116   }
117  
118  }
119  
120  public class PIHBE1 {
121   public static void main(String[] args) {
122   Weapon vampireShotgun = new VampireWeapon(20);
123   Weapon ghoulBazooka = new GhoulWeapon(30);
124  
125   Enemy vampire = new Vampire(vampireShotgun, 15);
126   Enemy ghoul = new Ghoul(ghoulBazooka, 5);
127  
128   vampire.attack();
129   ghoul.defend();
130   vampire.defend();
131   ghoul.attack();
132   ghoul.attack();
133   }
134  }
135