Switch Statements
By this logic a method can never have if-else statement - they also do N things.
There was a whole movement of anti-if programming. I'm not quite sure if it's a joke or not
Ok, back to Martin:
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
Whether this violates the "one thing" rule depends entirely on perspective:
- Low-level view: Four branches doing four different things
- High-level view: One thing - calculating employee pay
Switch statements have bad rep among Java developers:
- Doesn't look like OOP
- Leads to repetition
- Lead to bugs when copies get out of sync
Starting from Java 13 switch expressions introduced exhaustive matching, addressing one of these issues.
public Money calculatePay(Employee e) throws InvalidEmployeeType {
return switch (e.type) {
case COMMISSIONED -> yield calculateCommissionedPay(e);
case HOURLY -> yield calculateHourlyPay(e);
case SALARIED -> yield calculateSalariedPay(e);
}
}
Forgetting to handle a case now causes compile-time errors that's improssible to miss. No extra abstractions needed.
Martin suggests hiding the switch statement behind polymorphism:
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
This common approach has serious problems, as Martin's own example shows.
His Employee interface becomes a god object mixing unrelated concerns:
Employee.isPayday()
- couples Employee with payments, agreements, calendars and datesEmployee.calculatePay()
- couples Employee with financial calculationsEmployee.deliverPay()
- couples Employee with transaction processing and persistence
As more features will be added, the Employee interface will inevitably grow into an unmanageable, bloated entity
Ironically, Chapter 10 talks about cohesion and single responsibility principle. Yet here, to avoid a switch, he violates these core OOP principles.
By declaring switch
fundamentally bad, Martin loses this balance. His solution increases complexity and couples unrelated concerns, trading one set of problems for another.
To properly separate concerns, we'd need to split Employee
into focused interfaces like Payable
, Schedulable
, and Transactionable
.
But to maintain polymorphism, we'd then need either three factories (repetition) or a factory of factories (over-engineering).