【问题标题】:Product class responsibilities产品类别职责
【发布时间】:2014-12-16 18:09:16
【问题描述】:

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

  • 我们目录中的每个 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 来执行一些业务规则检查怎么办?

我正试图绕开它,但我想我想太多了,这很痛苦。 ;)

【问题讨论】:

  • 一个想法是Decorator 模式用您的功能(方法)包装您的对象(用户),并使用Factory Pattern 推出不同类型的对象(用户)。

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


【解决方案1】:

我倾向于您的第二个选项,即PaymentPriceCalculator。这样,如果您决定更改算法或使用多种算法,您就可以拥有不同的计算器。

  • 我不会把它放在Product 聚合中,因为价格因国家/地区而异。它还会使您的 Product 类更加复杂。从领域的角度来看,即使是在不同国家/地区购买的 2 件在其他方面相同的产品,难道不是“相等”吗?

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

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

【讨论】:

  • “即使是在不同国家购买的 2 件其他相同的产品也不是“相等”的”不,它们的价格不同。
  • @nik 这取决于域。它们可能被认为是平等的,因为它们具有相同的底价
  • 也许我误解了它 - 我不想在 Product AR 中进行计算。我希望它委托给一些计算器,但我的问题是 - 假设我通过 product(productId, country, currency) 从 ProductRepository 检索 Product 我应该通过调用计算器并通过 Product 的构造函数传递价格来组装它,还是应该像这样对我的产品建模:getPaymentPrice(Country, Currency)
【解决方案2】:

同意@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、currency等。

【讨论】:

    【解决方案3】:

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

    请参阅下面的链接以了解如何在这种情况下应用它。

    Strategy Design Pattern - Payment Strategy

    Strategy Design Pattern - Overview

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

      • 恐怕修改产品汇总中的价格不是一个好主意,因为价格字段现在不明确。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-11
      • 2021-12-31
      • 1970-01-01
      • 1970-01-01
      • 2016-10-21
      相关资源
      最近更新 更多