在我的公司,我们有一个非常具体的定价策略:
Product
都有一个baseUsdPrice
,例如产品 Foo 的基本美元价格为9.99美元。 我应该如何以DDD方式设计我的Product
聚合根,使其干净,有凝聚力,解耦并且易于更改?
paymentPrice
,其结果是Product
汇总的一部分吗?如果是这样,那意味着我的ProductRepository
将使用product(productId, countryId, currency)
PaymentPriceCalculator
中并使用getPaymentPrice(Country, Currency)
之类的访问者模式吗?如果我需要使用实体中的paymentPrice
来执行某些业务规则检查,该怎么办?我正试着把头包裹起来,我想我正在过度思考而且很伤心。 ;)
答案 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())
}
}
潜在的变化可能是:
算法更改(您可以将PaymentPrice作为超类提取并为不同的算法引入各种子类)
更多用户选项。这可能会破坏现有方法签名以添加更多参数。您可以引入参数对象来保存countryId,货币等。
答案 3 :(得分:0)
我认为这个问题最合适的设计模式是策略设计模式。
请参阅以下链接,了解如何在此方案中应用它。
Strategy Design Pattern - Payment Strategy
Strategy Design Pattern - Overview
请注意,如果您需要多个策略来获得最终价格,您可以将策略模式与其他模式(如工厂和复合模式)结合使用。