How to remove switch statements with polymorphism
In this post we will use polymorphism to remove switch statements. Inappropriately using switch statements is a sign of code smell.
Remove switch statements using polymorphism
In this post we will use polymorphism to remove switch statements. Inappropriately using switch statements is a sign of code smell. Robert Martin in his book Clean Code says that he limits himself to one switch statement per object type.
We will use example of a Customer
class. Initially we will use switch statements to implement the methods of the class. Then we will use polymorphism to refactor it and improve the code structure.
public class Customer {
public enum Membership {
Bronze, Silver, Gold
}
public Membership membership;
public Customer(Membership membership) {
this.membership = membership;
}
public void setMembership(Membership membership) {
this.membership = membership;
}
public Membership getMembership() {
return membership;
}
public double getMonthlyPrice() {
switch(membership) {
case Bronze:
return 10;
case Silver:
return 20;
case Gold:
return 30;
default:
throw new IllegalArgumentException("Invalid membership!!");
}
}
}
Each Customer
has a membership type. getMonthlyPrice
method calculates the monthly membership cost of Customer
based on their membership level. So far its all good. However we have been asked to add functionality to calculate points earned by the customer. Customers earn points based on their membership level and the amount they spent.
public double getPointsEarned(double amountSpent) {
switch(membership) {
case Bronze:
return amountSpent * 1;
case Silver:
return amountSpent * 2;
case Gold:
return amountSpent * 3;
default:
throw new IllegalArgumentException("Invalid membership!!");
}
}
Now we can clearly see whats wrong with our approach. We have duplicated switch statements. Our example is really simple, but the real world application will have complex logic for these calculations.
The first thought to solve this would be to use inheritance and introduce BronzeCustomer
, SilverCustomer
and GoldCustomer
which will extend abstract Customer
. Each of these classes will implement their version of getMonthlyPrice
and getPointsEarned
. But inheritance will not work in our case as the membership level might change in runtime after the object creation. What I mean by this is a Customer
can be upgraded or downgraded to other membership level anytime.
We can solve this problem by using State Pattern. We will begin refactoring by introducing Member
interface and delegate the functionality of price and membership points calculation to it. We will then create BronzeMember
, SilverMember
and GoldMember
which will implement Member
interface.
public interface Member {
double getMonthlyPrice();
double getPointsEarned(double amountSpent);
}
class BronzeMember implements Member {
public double getMonthlyPrice() {
return 10;
}
public double getPointsEarned(double amountSpent) {
return amountSpent * 1;
}
}
class SilverMember implements Member {
public double getMonthlyPrice() {
return 20;
}
public double getPointsEarned(double amountSpent) {
return amountSpent * 2;
}
}
class GoldMember implements Member {
public double getMonthlyPrice() {
return 30;
}
public double getPointsEarned(double amountSpent) {
return amountSpent * 3;
}
}
Next we will update Customer
class to use Member
interface. We have introduced a private property member
. Customer
delegates the work of calculation to its member
instance. When a membership is set, the member
instance is also updated based on the membership type. The constructor is updated to use the setMembership
method. This is how our final Customer
class looks like.
public class Customer {
public enum Membership {
Bronze, Silver, Gold
}
private Membership membership;
private Member member;
public Customer(Membership membership) {
this.setMembership(membership);
}
public void setMembership(Membership membership) {
this.membership = membership;
switch(membership) {
case Bronze:
this.member = new BronzeMember();
break;
case Silver:
this.member = new SilverMember();
break;
case Gold:
this.member = new GoldMember();
break;
default:
throw new IllegalArgumentException("Invalid membership!!");
}
}
public Membership getMembership() {
return membership;
}
public double getMonthlyPrice() {
return this.member.getMonthlyPrice();
}
public double getPointsEarned(double amountSpent) {
return this.member.getPointsEarned(amountSpent);
}
}
We have successfully used polymorphism to refactor the switch statements in the Customer
class. All the complex logic of calculating membership cost and points is delegated to respective classes. When a new membership level is added, we can simply update setMembership
method to support it. Above all, none of the client classes using Customer
will notice that the class has been changed and continue to work as before.