产品类职责

时间:2014-12-16 18:09:16

标签: oop design-patterns domain-driven-design

在我的公司,我们有一个非常具体的定价策略:

  • 我们目录中的每个Product都有一个baseUsdPrice,例如产品 Foo 的基本美元价格为9.99美元。
  • 这并不意味着你将支付9.99美元。我们会检查您所在的国家/地区是否存在价格异常 - 例如GB Foo 费用8.99 $
  • 您还可以选择要支付的货币 - 如果您使用的是GB,则可以支付美元(提到8.99美元)或当地货币(在这种情况下为英镑)。
  • 如果您选择以当地货币付款,我们会根据固定价格矩阵计算相当于8.99美元到英镑的价格(例如此处为3.99英镑)
  • 这是你支付的价格。

我应该如何以DDD方式设计我的Product聚合根,使其干净,有凝聚力,解耦并且易于更改?

  • 域名服务应计算paymentPrice,其结果是Product汇总的一部分吗?如果是这样,那意味着我的ProductRepository将使用product(productId, countryId, currency)
  • 等方法
  • 我应该将所有计算放在域服务类PaymentPriceCalculator中并使用getPaymentPrice(Country, Currency)之类的访问者模式吗?如果我需要使用实体中的paymentPrice来执行某些业务规则检查,该怎么办?

我正试着把头包裹起来,我想我正在过度思考而且很伤心。 ;)

4 个答案:

答案 0 :(得分:2)

我倾向于你的第二个选择PaymentPriceCalculator。这样,如果您决定更改算法或使用多种算法,您将能够使用不同的计算器。

  • 我不会将其放在Product汇总中,因为价格因国家/地区而异。它还会使您的Product类更复杂。从域名的角度来看,即使它们是在不同的国家购买,也不是2个相同的产品“相等”吗?

  • 访客模式并不适合这里。

我可能还有第二项服务可以将$转换为所需的任何货币。这应该是单独的服务,因为您的域似乎使用美元用于所有内容,直到最后用户需要实际支付的东西。通过这种方式,您还可以添加适用的税/增值税等,与按国家/地区计算价格例外的逻辑分开。

答案 1 :(得分:0)

我会声明一个负责计算产品价格的对象,例如:

interface ProductPriceCalculator {

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

由于每个产品都有basePrice,我们想要修改它,可以这样组织:

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;
        }
    }

}

现在,在这种情况下,您需要将国家/地区作为价格修饰符。对我来说有意义的解决方案是在代码中完全反映出来:

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);
        }
    }

}

现在支持货币和货币逻辑:

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];
    }

}

最后客户端看起来像这样:

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
    }

}

答案 2 :(得分:0)

同意@dkatzel,聚合不是举行计算的好地方。

假设产品保留计算:

product.paymentPrice(countryId, currency, fixedPriceMatrix)

要测试这一点,您需要在每个测试用例中构建一个产品,尽管有些情况只关注货币选择。

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

在实际项目中,聚合包含许多信息(用于不同目的),这使得它们在测试中相对更难设置。

测试当前计算的简便方法是使用值对象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);

您可以将PaymentPriceCalculator用作工厂的无状态域服务:

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

潜在的变化可能是:

  1. 算法更改(您可以将PaymentPrice作为超类提取并为不同的算法引入各种子类)

  2. 更多用户选项。这可能会破坏现有方法签名以添加更多参数。您可以引入参数对象来保存countryId,货币等。

答案 3 :(得分:0)

我认为这个问题最合适的设计模式是策略设计模式。

请参阅以下链接,了解如何在此方案中应用它。

Strategy Design Pattern - Payment Strategy

Strategy Design Pattern - Overview

请注意,如果您需要多个策略来获得最终价格,您可以将策略模式与其他模式(如工厂和复合模式)结合使用。