You have two problems here:
You need to serialize an object model with differences in the names of the keys, but containing the same basic data (a price and an option)
You need to perform some specific calculations in a generic manner
This requires two solutions.
First, let's tackle performing calculations, because this solution will set us up to solve our data serialization issue. What you really want is the Strategy Pattern:
In computer programming, the strategy pattern (also known as the policy pattern) is a software design pattern that enables an algorithm's behavior to be selected at runtime. The strategy pattern
- defines a family of algorithms,
- encapsulates each algorithm, and
- makes the algorithms interchangeable within that family.
This is precisely what you are trying to do. First, you want to define a public interface for your strategy objects that:
- Calculates the price
- Returns some sort of "name" for the strategy (useful for building the JSON array)
- Returns the "option" price (again, useful for building the JSON array)
interface PricingStrategy
{
public function calculatePrice();
public function getName();
public function getOption();
}
Since the algorithm for the pricing strategy is the same in your code example, and only a few values change, we can implement an abstract base class, and then extend it to provide specific values:
abstract class ProductPricingStrategy implements PricingStrategy
{
private $name;
private $basePrice;
private $cost;
private $option;
protected function __construct($name, $basePrice, $cost, $option) {
$this->name = $name;
$this->basePrice = $basePrice;
$this->cost = $cost;
$this->option = $option;
}
protected function getBasePrice() {
return $this->basePrice;
}
protected function getCost() {
return $this->cost;
}
public function getOption() {
return $this->option;
}
public function calculatePrice() {
return $this->basePrice + $this->cost + $this->option;
}
public function getName() {
return $this->name;
}
}
This gives you the basic framework for easily creating specific pricing strategies:
class PumpPricingStrategy extends ProductPricingStrategy
{
public function __construct() {
parent::__construct('pump', 1000, 250, 49.99);
}
}
class MotorPricingStrategy extends ProductPricingStrategy
{
public function __construct() {
parent::__construct('motor', 5000, 1500, 350);
}
}
class PartPricingStrategy extends ProductPricingStrategy
{
public function __construct() {
parent::__construct('part', 50, 20, 4.99);
}
}
Each concrete class calls the protected
parent constructor, and provides all the values necessary for the strategy. Now, we just need a "factory" object to provide easy, parameterized access to these strategies:
class PricingStrategyFactory
{
private $strategies;
public function __construct() {
$this->strategies = array(
new PumpPricingStrategy(),
new MotorPricingStrategy(),
new PartPricingStrategy()
);
}
public function find($name) {
foreach ($this->strategies as $strategy) {
if ($strategy->getName() == $name) {
return $strategy;
}
}
throw new Exception("Pricing Strategy '$name' not found");
}
}
Lastly, we can refactor the code in question and reduce it to just 6 lines of code that don't require any if
s, switch
s, and
s (or buts):
$factory = new PricingStrategyFactory();
$pricing = $factory->find('motor');
$json = array(
'total' => $pricing->calculatePrice(),
"{$pricing->getName()}_option_1" => $pricing->getOption()
);
The getName
method is used to generate the unique JSON array key that your response is expecting. The calculatePrice
method becomes a black box that encapsulates the algorithm, so the rest of your code base doesn't need to know it.
You'll also notice that the PricingStrategy
interface isn't referenced any place else but the definition of the interface, and the ProductPricingStrategy
class. In the short code example above you won't see the benefit of the interface. When you need to pass this pricing strategy object around, you'll see the benefit.
First, let's pretend we have a fictional MVC framework, and we've created a "controller" class called "ProductPricesController". The MVC framework routes GET requests for /product_prices/calculate?type=ABC
to the ProductPricesController#calculate
method:
class ProductPricesController extends Controller
{
// GET: /product_prices/calculate?type=motor
public function calculate() {
$type = $_GET['type'];
$factory = new PricingStrategyFactory();
$pricing = $factory->find($type);
$json = $this->getPriceJson($pricing);
echo json_encode($json);
}
private function getPriceJson(PricingStrategy $pricing) {
return array(
'total' => $pricing->calculatePrice(),
"{$pricing->getName()}_option_1" => $pricing->getOption()
);
}
}
The getPriceJson
method has a PHP Type Hint: PricingStrategy
, which is the name of our interface. This helps the developer because it gives them information about what this method expects, and it helps the application because the interface becomes a "contract" between the controller and the pricing strategy so the two can interact in a predictable and modular manner.