3

How do I design code that has two qualities:

  • uses similar but different subtypes of an object
  • uses similar but different types of computation on the above objects

Example

I am refactoring this from a legacy code that had if/then/else statements for different object types in one file, and if/then/else statements for doing various computing tasks with the objects in another file.

Using OOP I can rewrite it somewhat like this:

class Pricing {
    public $base;
}

class PumpPricing extends Pricing {
    public $pumpCost;
    public $pumpOption1;
};
class MotorPricing extends Pricing {
    public $motorCost;
    public $motorOption1;
};
class PartPricing extends Pricing {
    public $partCost;
};

Later . . .

//I can put proper object creation into Factory:
$pricing = (new PriceFactory())->getPriceObject("motor");

//but how do I compartmentalize computations?
//i.e. this:

if  (get_class($pricing) == "PumpPricing") {
    $json = array( //future json
        'total' => $pricing->pumpCost + $pricing->base + $pricing->pumpOption1,
        'pump_option_1' => $pricing->pumpOption1
    );
}
else if (get_class($pricing) == "MotorPricing") {
    $json = array(
        'total' => $pricing->motorCost + $pricing->base + $pricing->motorOption1,
        'motor_option_1' => $pricing->motorOption1
    );
}

Question

Considering my goal to get rid of if/then/else statements in the mainstream code and to encapsulate the computations, where and how do I do that?

For one, I can group the above formulas into a single computational getter, and put it into the Pricing object. i.e. to mimic line from above:

'total' => $pricing->getTotal(),
'motor_option_1' => $pricing->getOption1()

But, I am still dealing with different options that I want to group into an array, i.e. names and quantity of the options are different.

3
  • Whenever I hear that a bunch of if statements are peppered all over code doing calculations, I automatically think of the Strategy Pattern: sourcemaking.com/design_patterns/strategy. Can you post more detail about how these pricing objects get used? Maybe a real world example with more context might help. Nov 18, 2015 at 2:10
  • Greg please see codereview.stackexchange.com/questions/111142/…
    – Dennis
    Nov 18, 2015 at 20:26
  • I see your code review. Could you change the title so it summarizes what the code does? It would make it easier to review that way. Nov 18, 2015 at 20:39

3 Answers 3

1

1) You can just add a serialize method to each class. So your base class Pricing would define an abstract method like:

abstract function getPriceObject();

Then each class would define a unique getPriceObject.

http://php.net/manual/en/language.oop5.abstract.php

2) That said this also looks like a problem suited for the visitor pattern. If someone else wants to write a verbose explanation of this it might be useful, but searching online gives a lot of PHP examples.

1

As a first step, you can put that behaviour into methods of your Price hierarchy objects. Then:

class PumpPricing extends Pricing {
  ...
  function serializableRepresentation() {
    return array(
      'total' => $this->pumpCost + $this->base + $this->pumpOption1,
      'pump_option_1' => $this->pumpOption1
    );
  }
}

This simplifies your main code to:

$json = $pricing->serializationRepresentation();

Unfortunately, this likely violates the Single Responsibility Principle for more complex behaviours. In this case, we can use the Visitor Pattern to allow us to define extension methods on your types. The only thing that has to be modified is adding an acceptVisitor method to each Pricing type:

class PumpPricing extends Pricing {
  ...
  function acceptVisitor($v) {
    return $v->visitPumpPricing($this);
  }
}

We can now define operations in external classes:

class SerializationRepresentationVisitor {
  ...
  function visitPumpPricing($pricing) {
    return array(
      'total' => $pricing->pumpCost + $pricing->base + $pricing->pumpOption1,
      'pump_option_1' => $pricing->pumpOption1
    );
  }
}

function serializationRepresentation($pricing) {
  $visitor = new SerializationRepresentationVisitor();
  return $pricing->acceptVisitor($visitor);
}

This changes your main code to:

$json = serializationRepresentation($pricing);

I believe using a visitor like this to enable the easy addition of new functionality strikes the best balance between separating responsibilities, and making for readable code. This is particularly advisable when operating on a hierarchy of dumb types that do not have much behaviour of their own, but a lot of operations that act on the whole hierarchy. Your Pricing hierarchy is one example, as would be abstract syntax trees in a compiler.

5
  • I gotta say this broke my mind. ... will be looking at it long time :) (your explanation was clear but need to wrap my mind around it more)
    – Dennis
    Nov 17, 2015 at 21:20
  • I am thinking I will try the first step without the visitor pattern. And possibly refactor from there. I don't yet fully see (other than theoretically) the violation of SRP. I suppose if I just do serialization, I will have subclasses that carry two responsibilities: computing price + providing serialization. Do I get that right?
    – Dennis
    Nov 17, 2015 at 21:24
  • @Dennis In this case it's probably fine, but if you have to add another and yet another method to all classes in that hierarchy, something is wrong. For a class that represents a pricing strategy, adding serialization-related methods is fairly reasonable since that potentially has to touch on internal details. This stops being OK if you have multiple serialization representations, or add a pretty-printing method or a getTotalWithDiscount() method and so on.
    – amon
    Nov 17, 2015 at 21:32
  • Thanks. In my case, my serialization is pretty much tied to the internals of each subclass, so what I did now just add getSerialization method inside each subclass where each contains its own individual serialization (different number of different key/value pairs). I suspect that if I do the visitor pattern, I will essentially need to code the algo for each subclass anyway...
    – Dennis
    Nov 18, 2015 at 16:34
  • While I see Visitor as more flexible I don't yet see the justification for the complexity. If I do need a different representation, I will need to code up each representation and then add (factory?) code to be able to create/select it as well.. So more complexity as well. Perhaps I will cross that bridge when I get there . ..
    – Dennis
    Nov 18, 2015 at 16:35
1

You have two problems here:

  1. 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)

  2. 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 ifs, switchs, ands (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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.