Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Code Smell Series: Parallel Inheritance Hierarchies

DZone's Guide to

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.

· Java Zone
Free Resource

Try Okta to add social login, MFA, and OpenID Connect support to your Java app in minutes. Create a free developer account today and never build auth again.

Code 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]


Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
anti pattern ,java ,code smell ,inheritance

Published at DZone with permission of Shamik Mitra, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}