Code Smell Series: Parallel Inheritance Hierarchies
If you want to keep your code smelling fresh, beware parallel inheritance hierarchies and the problems they can cause when making additions to your code.
Join the DZone community and get the full member experience.
Join For FreeCode smells are similar in concept to development-level anti-patterns. Sometimes, in our code, we unintentionally introduce code smells and make our design fragile.
Definition of a Code Smell
Code smells, also known as a bad smell, in computer programming refer to any symptom in the source code of a program that possibly indicates a deeper problem.
Or, as Martin Fowler puts it, "a code smell is a surface indication that usually corresponds to a deeper problem in the system."
Code smells create a lot of problems when introducing new features or maintaining the codebase. Often, developers have to write repeatable code, breaking encapsulation, breaking abstraction, etc.
So, always refactor code smells while developing.
In this article, we discuss the “Parallel Inheritance Hierarchies” code smell.
Parallel Inheritance Hierarchies occur when an inheritance tree depends on another inheritance tree by composition, and they maintain a special relationship where one subclass of a dependent inheritance must depend on one a particular subclass of another Inheritance.
Think about engineers — just engineers in general. Computer engineers work on computers and deliver projects, whereas civil engineer work on structures. From a design perspective, there are two parallel hierarchies:
Engineers
Milestones
The different engineers have different milestones, and each engineer has a specified milestone (special relation).
The problem is that every time you add a new engineer in the Engineer inheritance, you have to introduce a new Milestone in Milestone hierarchy.
Causes of the Parallel Inheritance Hierarchies Smell
- Failing to understand the responsibility, often due to misunderstandings (breaking the single responsibility principle)
- Overenthusiasm to break each function down as a separate interface.
- Failure to introduce proper design patterns.
- Lots of duplicate code.
- The wrong relationship sets (client-side).
- Unmaintainable code base.
Refactor Strategy
We can do it via the “Move Method” and “Move Field” techniques.
Now, take an example where parallel inheritance hierarchies are present. We will implement the Engineer and set the Milestone for him.
package com.example.codesmell.parallelinheritence;
public interface Engineer {
String getType();
void setType(String type);
int getSalary();
void setSalary(int salary);
MileStone getMileStone();
void setMileStone(MileStone mileStone);
}
package com.example.codesmell.parallelinheritence;
public interface MileStone {
public String work();
public String target();
}
package com.example.codesmell.parallelinheritence;
public class ComputerEngineer implements Engineer {
private String type;
private int salary;
private MileStone mileStone;
public void setType(String type) {
this.type = type;
}
public void setSalary(int salary) {
this.salary = salary;
}
public void setMileStone(MileStone mileStone) {
this.mileStone = mileStone;
}
@Override
public String getType() {
// TODO Auto-generated method stub
return type;
}
@Override
public int getSalary() {
// TODO Auto-generated method stub
return salary;
}
@Override
public MileStone getMileStone() {
// TODO Auto-generated method stub
return mileStone;
}
@Override
public String toString() {
return "ComputerEngineer [type=" + type + ", salary=" + salary
+ ", mileStone=" + mileStone + "]";
}
}
package com.example.codesmell.parallelinheritence;
public class ComputerMileStone implements MileStone {
@Override
public String work() {
return"Build a Billing MicroService";
}
@Override
public String target() {
return"Has to be finshed in 14 PD";
}
@Override
public String toString() {
return "ComputerMileStone [work()=" + work() + ", target()=" + target()
+ "]";
}
}
package com.example.codesmell.parallelinheritence;
public class CivilEngineer implements Engineer {
private String type;
private int salary;
private MileStone mileStone;
public void setType(String type) {
this.type = type;
}
public void setSalary(int salary) {
this.salary = salary;
}
public void setMileStone(MileStone mileStone) {
this.mileStone = mileStone;
}
@Override
public String getType() {
// TODO Auto-generated method stub
return type;
}
@Override
public int getSalary() {
// TODO Auto-generated method stub
return salary;
}
@Override
public MileStone getMileStone() {
// TODO Auto-generated method stub
return mileStone;
}
@Override
public String toString() {
return "CivilEngineer [type=" + type + ", salary=" + salary
+ ", mileStone=" + mileStone + "]";
}
}
package com.example.codesmell.parallelinheritence;
public class CivilMileStone implements MileStone {
@Override
public String work() {
// TODO Auto-generated method stub
return "Create Twin Towers";
}
@Override
public String target() {
// TODO Auto-generated method stub
return "Has to be completed in 2 years";
}
@Override
public String toString() {
return "CivilMileStone [work()=" + work() + ", target()=" + target()
+ "]";
}
}
package com.example.codesmell.parallelinheritence;
public class Manager {
public static void main(String[] args) {
Engineer comp = new ComputerEngineer();
comp.setType("Computer Engineer");
comp.setSalary(50000);
comp.setMileStone(new ComputerMileStone());
Engineer civil = new CivilEngineer();
civil.setType("Civil Engineer");
civil.setSalary(60000);
civil.setMileStone(new CivilMileStone());
System.out.println(comp);
System.out.println("********************");
System.out.println(civil);
}
}
Output :
ComputerEngineer [type=Computer Engineer, salary=50000, mileStone=ComputerMileStone [work()=Build a Billing MicroService, target()=Has to be finshed in 14 PD]]
********************
CivilEngineer [type=Civil Engineer, salary=60000, mileStone=CivilMileStone [work()=Create Twin Towers, target()=Has to be completed in 2 years]]
We create two interfaces — Engineer and Milestone — and create subclasses for them, but the thing to notice is that every engineer has his own special milestone, so as we expose the setMileStone method to the client, it may be possible for the client to set the wrong Milestone for an Engineer.
Another thing to note is that if we want to add a new Engineer, we also need to add a new Milestone for him/her. That's a very difficult problem to fix. In fact, if we try, we might break the SRP (Single Responsibility Principle).
There are three possible way we can deal with it.
Solution 1
Keep the parallel hierarchies open and get used to it.
Pros
Better way to maintain SRP.
The code will be flexible.
Cons
To add a new feature, we have to create two classes every time.
Hierarchies are coupled. Changes in one might necessitate changes in the other.
Harder to maintain.
Solution 2
Make them into partial hierarchies so we can open provision them for parallel hierarchies.
Pros
Only maintain one hierarchy.
When you are not sure about responsibility, try to adopt it.
Provide flexibility.
Cons
May break SRP
Technique
Make a concrete class and implement both interfaces. The client got the instance of this class via the static factory method.
Let's see the solution:
package com.example.codesmell.parallelinheritence;
public class PartialComputerEngineer implements Engineer,MileStone {
private String type;
private int salary;
@Override
public String work() {
return"Build a Billing MicroService";
}
@Override
public String target() {
return"Has to be finshed in 14 PD";
}
@Override
public String getType() {
// TODO Auto-generated method stub
return type;
}
@Override
public void setType(String type) {
this.type=type;
}
@Override
public int getSalary() {
// TODO Auto-generated method stub
return salary;
}
@Override
public void setSalary(int salary) {
this.salary=salary;
}
@Override
public MileStone getMileStone() {
// TODO Auto-generated method stub
return this;
}
@Override
public void setMileStone(MileStone mileStone) {
throw new UnsupportedOperationException("Not Supported");
}
@Override
public String toString() {
return "PartialComputerEngineer [type=" + type + ", salary=" + salary
+ ", work()=" + work() + ", target()=" + target()
+ ", getType()=" + getType() + ", getSalary()=" + getSalary()
+ "]";
}
}
package com.example.codesmell.parallelinheritence;
public class PartialCivilEngineer implements Engineer,MileStone {
private String type;
private int salary;
@Override
public String work() {
return "Create Twin Towers";
}
@Override
public String target() {
return "Has to be completed in 2 years";
}
@Override
public String getType() {
return type;
}
@Override
public void setType(String type) {
this.type=type;
}
@Override
public int getSalary() {
// TODO Auto-generated method stub
return salary;
}
@Override
public void setSalary(int salary) {
this.salary=salary;
}
@Override
public MileStone getMileStone() {
// TODO Auto-generated method stub
return this;
}
@Override
public void setMileStone(MileStone mileStone) {
throw new UnsupportedOperationException("Not Supported");
}
@Override
public String toString() {
return "PartialCivilEngineer [type=" + type + ", salary=" + salary
+ ", work()=" + work() + ", target()=" + target()
+ ", getType()=" + getType() + ", getSalary()=" + getSalary()
+ "]";
}
}
package com.example.codesmell.parallelinheritence;
public class EngineerFactory {
public static Engineer getEngineer(Class clazz) throws InstantiationException, IllegalAccessException
{
return (Engineer) clazz.newInstance();
}
}
package com.example.codesmell.parallelinheritence;
public class Manager {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Engineer comp = EngineerFactory.getEngineer(PartialComputerEngineer.class);
comp.setType("Computer Engineer");
comp.setSalary(50000);
Engineer civil = EngineerFactory.getEngineer(PartialCivilEngineer.class);
civil.setType("Computer Engineer");
civil.setSalary(50000);
System.out.println(comp);
System.out.println("********************");
System.out.println(civil);
}
}
Output :
PartialComputerEngineer [type=Computer Engineer, salary=50000, work()=Build a Billing MicroService, target()=Has to be finshed in 14 PD, getType()=Computer Engineer, getSalary()=50000]
********************
PartialCivilEngineer [type=Computer Engineer, salary=50000, work()=Create Twin Towers, target()=Has to be completed in 2 years, getType()=Computer Engineer, getSalary()=50000]
Solution 3
Collapse a hierarchy.
Pros
Only maintain One hierarchy
Easy to maintain
Cons
Breaks SRP fairly often.
Technique
Make a common interface and move methods from another interface.
Let's see the solution:
package com.example.codesmell.parallelinheritence;
public interface EngineerMileStone {
String getType();
void setType(String type);
int getSalary();
void setSalary(int salary);
public String work();
public String target();
}
package com.example.codesmell.parallelinheritence;
public class RefactorComputerEngineer implements EngineerMileStone {
private String type;
private int salary;
@Override
public String getType() {
return type;
}
@Override
public void setType(String type) {
this.type=type;
}
@Override
public int getSalary() {
return salary;
}
@Override
public void setSalary(int salary) {
this.salary=salary;
}
@Override
public String work() {
return"Build a Billing MicroService";
}
@Override
public String target() {
return"Has to be finshed in 14 PD";
}
@Override
public String toString() {
return "RefactorComputerEngineer [type=" + type + ", salary=" + salary
+ ", getType()=" + getType() + ", getSalary()=" + getSalary()
+ ", work()=" + work() + ", target()=" + target() + "]";
}
}
package com.example.codesmell.parallelinheritence;
public class ReFactorCivilEngineer implements EngineerMileStone {
private String type;
private int salary;
@Override
public String getType() {
return type;
}
@Override
public void setType(String type) {
this.type=type;
}
@Override
public int getSalary() {
return salary;
}
@Override
public void setSalary(int salary) {
this.salary=salary;
}
@Override
public String work() {
return "Create Twin Towers";
}
@Override
public String target() {
return "Has to be completed in 2 years";
}
@Override
public String toString() {
return "ReFactorCivilEngineer [type=" + type + ", salary=" + salary
+ ", getType()=" + getType() + ", getSalary()=" + getSalary()
+ ", work()=" + work() + ", target()=" + target() + "]";
}
}
package com.example.codesmell.parallelinheritence;
public class Manager {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
EngineerMileStone comp = new RefactorComputerEngineer();
comp.setType("Computer Engineer");
comp.setSalary(50000);
EngineerMileStone civil = new ReFactorCivilEngineer();
civil.setType("Civil Engineer");
civil.setSalary(60000);
System.out.println(comp);
System.out.println("********************");
System.out.println(civil);
}
}
Output
RefactorComputerEngineer [type=Computer Engineer, salary=50000, getType()=Computer Engineer, getSalary()=50000, work()=Build a Billing MicroService, target()=Has to be finshed in 14 PD]
********************
ReFactorCivilEngineer [type=Civil Engineer, salary=60000, getType()=Civil Engineer, getSalary()=60000, work()=Create Twin Towers, target()=Has to be completed in 2 years]
Opinions expressed by DZone contributors are their own.
Comments