When it comes to object-oriented programming, managing complex conditionals within your code can quickly lead to messy, hard-to-maintain logic. A powerful solution to simplify and enhance the flexibility of your code is polymorphism. Polymorphism enables objects of different classes to be treated as objects of a common superclass. This can dramatically reduce the need for conditionals like switch
or if-else
statements.
In this post, we’ll explore how to refactor a typical project pricing model using polymorphism. We’ll begin with a basic example of conditional logic and demonstrate how to refactor it step-by-step to use polymorphism, making the code more extensible and maintainable.
The Problem: Overloaded Conditionals
Consider a Project
class with a calculateRate
method that calculates a project’s rate based on its type: design
, strategy
, or development
. The current implementation uses a switch
statement to apply different logic for each type.
class Project
{
private $type;
private $baseDesignRate = 3;
private $baseStrategyRate = 2;
private $baseDevelopmentRate = 1;
public function calculateRate()
{
switch ($this->type) {
case 'design':
$rate = $this->baseDesignRate;
break;
case 'strategy':
$rate = $this->baseStrategyRate;
break;
case 'development':
$rate = $this->baseDevelopmentRate;
break;
}
return $rate;
}
}
While this approach works, every time you need to add a new type or modify the logic for an existing type, you have to update the switch
statement. This creates maintenance challenges and limits flexibility.
Refactoring to Polymorphism
By refactoring this logic, we can replace the switch
statement with polymorphism. Polymorphism allows different project types to have their own class, each implementing a method to calculate rates. Let’s go through the steps to achieve this.
Step 1: Extract the Rate Calculation Logic
We start by moving the rate calculation logic into its own class. This can be an abstract class or an interface that defines a common calculateRate
method. Each specific type (e.g., DesignRate
, StrategyRate
, DevelopmentRate
) will extend this class and implement the rate calculation.
abstract class ProjectRateType
{
abstract public function calculateRate(Project $project);
}
Step 2: Define Specific Rate Calculation Classes
Next, we create subclasses for each project type. Each subclass overrides the calculateRate
method to implement the specific rate logic for that project type.
class DesignRateType extends ProjectRateType
{
public function calculateRate(Project $project)
{
return $project->getBaseDesignRate();
}
}
class StrategyRateType extends ProjectRateType
{
public function calculateRate(Project $project)
{
return $project->getBaseStrategyRate();
}
}
class DevelopmentRateType extends ProjectRateType
{
public function calculateRate(Project $project)
{
return $project->getBaseDevelopmentRate();
}
}
Step 3: Choose the Correct Subclass Dynamically
To select the appropriate subclass dynamically, we can use either a lookup array or a factory class. The lookup array maps project types to their corresponding classes.
class Project
{
private $type;
private $baseDesignRate = 3;
private $baseStrategyRate = 2;
private $baseDevelopmentRate = 1;
public function calculateRate()
{
$lookup = [
'design' => 'DesignRateType',
'strategy' => 'StrategyRateType',
'development' => 'DevelopmentRateType'
];
if (!array_key_exists($this->type, $lookup)) {
throw new \RuntimeException('Invalid project type');
}
$className = "App\\Services\\" . $lookup[$this->type];
return (new $className)->calculateRate($this);
}
}
Step 4: Using a Factory for Dynamic Class Creation
Instead of a static lookup, we can abstract the class loading logic into a Factory class. This factory will handle the instantiation of the correct rate calculation class based on the project type.
class ProjectRateFactory
{
public function getProjectRateType($type)
{
$className = "App\\Services\\" . ucfirst($type) . "RateType";
if (!class_exists($className)) {
throw new \RuntimeException('Invalid project type');
}
return new $className;
}
}
With this, the main Project
class remains clean, and adding new types of projects becomes much easier without modifying existing code. Now, you can instantiate the ProjectRateFactory
and delegate rate calculation to the appropriate class.
$project = new Project(new ProjectRateFactory());
$project->setType('development');
echo $project->calculateRate();
Benefits of Using Polymorphism
- Extensibility: New project types can be added by simply creating new subclasses without changing the existing code.
- Maintainability: Polymorphism allows the logic for different project types to be encapsulated in their respective classes, making the code easier to maintain and understand.
- Testability: The separate classes for each type allow for more granular testing. You can easily mock or stub the classes in unit tests.
- Decoupling: The main
Project
class no longer needs to worry about the specifics of rate calculation, promoting the single responsibility principle.
Conclusion
Replacing conditionals with polymorphism is a great way to simplify your code and prepare it for future changes. By creating specific subclasses for each project type, we’ve not only removed the complex switch
statement but also made our code more flexible and maintainable. Whether you use a lookup array or a factory pattern, polymorphism offers a clear path toward cleaner, more manageable code.
Leave a Reply