4

In my company we have a very specific pricing strategy:

  • Every Product in our catalog has a baseUsdPrice, for example product Foo has base USD price of 9.99$.
  • That does not necessary mean that will be your you'll be paying 9.99$. We check your country for price exceptions - for example in GB Foo costs 8.99$
  • Further more you can choose a currency you want to pay - if you're in GB you can pay in either USD (mentioned 8.99$) or your local currency (in this case GBP).
  • If you choose to pay by your local currency we calculate an equivalent of 8.99$ to british pounds based on fixed price matrix (for example here that'll be 3.99£)
  • This the price you pay.

How should I design my Product aggregate root in DDD manner to be clean, cohesive, decoupled and easy to change?

  • Should paymentPrice be calculated by domain service and its result put as a part of Product aggregate? If so that means my ProductRepository will have methods like product(productId, countryId, currency)
  • Should I put all calculations in domain service class PaymentPriceCalculator and use visitor pattern like getPaymentPrice(Country, Currency)? What if I need to use paymentPrice from my entity to perform some business rule checks?

I'm trying to wrap my head around it and I think I'm overthinking it and it hurts. ;)

acid
  • 2,099
  • 4
  • 28
  • 41
  • An idea would be the `Decorator` pattern to wrap your objects (users) with your functionality (methods) and use a `Factory Pattern` to roll-out the different types of objects (users). – Drew Kennedy Dec 16 '14 at 19:57

4 Answers4

2

I would lean towards your second option of having a PaymentPriceCalculator. This way you would be able to have different calculators if you ever decided to change the algorithm or use multiple algorithms.

  • I would not put it in the Product aggregate since the price varies by country. It would also make your Product class more complicated. From a domain point of view, aren't 2 otherwise identical products "equal" even if they are purchased in different countries?

  • Visitor pattern doesn't really fit here.

I might also have a 2nd service that converts $ into whatever currency is needed. This should be separate service since your domain appears to use dollars for everything until the very end when the user needs to actual pay for stuff. This way you can also add applicable taxes / VAT etc as separate from the logic of figuring out price exceptions by country.

dkatzel
  • 31,188
  • 3
  • 63
  • 67
  • "aren't 2 otherwise identical products "equal" even if they are purchased in different countries" no, they are with different prices. – Weltschmerz Dec 16 '14 at 19:58
  • 1
    @nik well it depends on the domain. They might be considered equal because they have the same base price – dkatzel Dec 16 '14 at 20:11
  • Maybe I miscommunicated it - I didn't want to put calculations in `Product` AR. I want it to delegate to some calculator but my question is - assuming that I retrieve Product from ProductRepository via `product(productId, country, currency)` should I assemble it by calling calculator and passing price via Product's constructor or should I model my Product like that: `getPaymentPrice(Country, Currency)`? – acid Dec 17 '14 at 15:55
0

I would declare an object responsible for calculating Product's price, for example:

interface ProductPriceCalculator {

    public function determineProductPrice($productId, Currency $currency = null);
}

Since each product has the basePrice and we would like to modify it can be organized like this:

class Product {

    private $id;

    /**
     * Initially base price
     * @var Money
     */
    private $price;

    public function modifyPrice(ProductPriceCalculator $calculator, Currency $currency = null) {
        $newPrice = $calculator->determineProductPrice($this->id, $currency);

        if ($newPrice !== null) {
            $this->price = $newPrice;
        }
    }

}

Now in this case you need the country to be the price modifier. The solution that makes sense to me is to reflect that in code exactly:

class Country implements ProductPriceCalculator {

    private $id;

    /**
     * @var Currency
     */
    private $currency;

    /**
     * Hashmap<String, Money> where product id evaluates to price in $this country
     * @var array
     */
    private $productPrices = array();

    /**
     * @param string $productId
     * @param Currency $currency
     * @return Money
     */
    public function determineProductPrice($productId, Currency $currency = null) {
        if (array_key_exists($productId, $this->productPrices)) {
            $productPrice = clone $this->productPrices[$productId];

            if ($currency !== null) {
                $currency = $this->currency;
            }

            return $productPrice->convertTo($currency);
        }
    }

}

Now to support money and currency logic:

class Money {

    private $value;
    private $currency;

    public function __construct($amount, Currency $currency) {
        $this->value = $amount;
        $this->currency = $currency;
    }

    public function convertTo(Currency $newCurrency) {
        if (!$this->currency->equalTo($newCurrency)) {
            $this->value *= $newCurrency->ratioTo($this->currency);
            $this->currency = $newCurrency;
        }
    }

}

class Currency {

    private $code;
    private static $conversionTable = array();

    public function equalTo(Currency $currency) {
        return $this->code == $currency->code;
    }

    public function ratioTo(Currency $currency) {
        return self::$conversionTable[$this->code . '-' . $currency->code];
    }

}

And finally the client would look something like this:

class SomeClient {

    public function someAction() {
        //initialize $product (it has the base price)
        //initialize $selectedCountry with prices for this product
        //initialize $selectedCurrency

        $product->modifyPrice($selectedCountry, $selectedCurrency);

        //here $product contains the price for that country
    }

}
Weltschmerz
  • 2,166
  • 1
  • 15
  • 18
0

Agree with @dkatzel, aggregate is not a good place to hold calculation.

Assume that the product hold the calculation:

product.paymentPrice(countryId, currency, fixedPriceMatrix)

To test this, you need to build a product in each test case although some cases focus on currency choosing only.

countryId = ...
currency = ...
fixedPriceMatrix = ...
basePrice = ...
countryPrice = ...
product = new Product(id, basePrice, countryPrice...)
paymentPrice = product.paymentPrice(countryId, currency, fixedPriceMatrix)

In real projects, aggregates holds many information (for different purposes) which makes them relatively more difficult to setup in tests.

The easies way to test the current calculation is using a value object PaymentPrice

//in unit test
paymentPrice = new PaymentPrice(basePriceForYourCountry, currency, fixedPriceMatrix)
value = paymentPrice.value()

//the product now holds countryPrice calculation
countryPrice = new Product(id, basePrice).countryPrice(countryId);

You can use PaymentPriceCalculator as a stateless domain service as the factory:

class PaymentPriceCalculator {
    PaymentPrice paymentPrice(product, countryId, currency) {
        fixedPriceMatrix = fixedPriceMatrixStore.get()
        return new PaymentPrice(product.countryPrice(countryId), currency, fixedPriceMatrix())
    }
}

The potential change could be:

  1. algorithm change(you can extract PaymentPrice as superclass and introduce various subclass for different algorithms)

  2. more options for user. This may break existing method signature to add more parameters. You can introduce a parameter object to hold countryId, currency and others.

Yugang Zhou
  • 7,123
  • 6
  • 32
  • 60
0

I think the most appropriate design pattern for this problem is Strategy Design Pattern.

Please refer below links to understand how it can be applied in this scenario.

Strategy Design Pattern - Payment Strategy

Strategy Design Pattern - Overview

Please note you can combine Strategy Pattern with other patterns like Factory and Composite if you need more than one strategy to get the final price.

Atul
  • 1,694
  • 4
  • 21
  • 30