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
- Example 1
- Example 2
- Before
- After
001 interface Enemy {002 public void attack();003004 public void defend();005 }006007 class Vampire implements Enemy {008 private Weapon shotgun;009 private int defense;010011 public Vampire(Weapon shotgun, int defense) {012 this.shotgun = shotgun;013 this.defense = defense;014 }015016 @Override017 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 }024025 @Override026 public void defend() {027 System.out.println("Blocking attack with defense: " + (this.defense + 10));028 }029030 }031032 class Ghoul implements Enemy {033 private Weapon bazooka;034 private int defense;035036 public Ghoul(Weapon bazooka, int defense) {037 this.bazooka = bazooka;038 this.defense = defense;039 }040041 @Override042 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 }050051 @Override052 public void defend() {053 System.out.println("Blocking attack with defense: " + (this.defense + 20));054 }055056 }057058 interface Weapon {059 public int getDamage();060061 public void repair();062063 public int inspectHealth();064 }065066 class VampireWeapon implements Weapon {067 private int damage;068 private int health;069070 public VampireWeapon(int damage) {071 this.damage = damage;072 this.health = 30;073 }074075 @Override076 public int getDamage() {077 this.health -= 10;078 return damage;079 }080081 @Override082 public void repair() {083 this.health = 100;084 }085086 @Override087 public int inspectHealth() {088 return this.health;089 }090091 }092093 class GhoulWeapon implements Weapon {094 private int damage;095 private int health;096097 public GhoulWeapon(int damage) {098 this.damage = damage;099 this.health = 200;100 }101102 @Override103 public int getDamage() {104 this.health -= 10;105 return damage;106 }107108 @Override109 public void repair() {110 this.health = 100;111 }112113 @Override114 public int inspectHealth() {115 return this.health;116 }117118 }119120 public class PIHBE1 {121 public static void main(String[] args) {122 Weapon vampireShotgun = new VampireWeapon(20);123 Weapon ghoulBazooka = new GhoulWeapon(30);124125 Enemy vampire = new Vampire(vampireShotgun, 15);126 Enemy ghoul = new Ghoul(ghoulBazooka, 5);127128 vampire.attack();129 ghoul.defend();130 vampire.defend();131 ghoul.attack();132 ghoul.attack();133 }134 }135