四、业务建模的最佳实践
4.1 建模原则
- 单一职责原则(SRP)
一个聚合根只负责一个明确的核心业务。不要把订单的支付、库存、物流全部塞进订单聚合。
# 错误:订单聚合做了太多事
class BadOrder:
def pay(self): pass
def check_inventory(self): pass
def calculate_shipping(self): pass
def send_notification(self): pass
# 正确:分离关注点
class Order:
def pay(self, payment_service): pass
class InventoryService:
def reserve(self, order): pass
class ShippingService:
def calculate_fee(self, order): pass
class NotificationService:
def send_order_confirmation(self, order): pass
2.不变性保护
业务规则需要被严格保护,不要让外部直接修改核心状态。
class Order:
def __init__(self):
self._status = OrderStatus.PENDING
self._items = []
@property
def status(self):
return self._status
@property
def items(self):
# 返回副本,防止外部直接修改
return list(self._items)
def add_item(self, product, quantity):
# 业务规则:已支付的订单不能添加商品
if self._status != OrderStatus.PENDING:
raise OrderAlreadyPaidError("订单已支付,无法修改")
# 业务规则:单个商品数量不能超过10
if quantity > 10:
raise QuantityLimitError("单个商品购买数量不能超过10")
self._items.append(OrderItem(product, quantity))
3.显式建模隐式概念
很多业务规则是隐性的,需要显式地用代码表达。
# 隐式:直接用字符串
order.status = "paid"
# 显式:用枚举
class OrderStatus(Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
# 更隐式:用数值比较
if amount > 100:
discount = 10
# 更显式:用命名常量或规格
class DiscountRule:
MIN_AMOUNT_FOR_DISCOUNT = Decimal('100')
DISCOUNT_AMOUNT = Decimal('10')
if amount >= DiscountRule.MIN_AMOUNT_FOR_DISCOUNT:
discount = DiscountRule.DISCOUNT_AMOUNT
# 或者用规格模式
class EligibleForDiscountSpec(Specification):
def is_satisfied_by(self, order):
return order.amount >= DiscountRule.MIN_AMOUNT_FOR_DISCOUNT
4.2 避免的陷阱
陷阱1:过度设计
不要一开始就做完美的模型。从最简单的模型开始,随着业务理解加深逐步演进。
陷阱2:贫血模型
只包含数据没有行为的模型。
# 贫血模型(坏)
class OrderData:
def __init__(self):
self.id = None
self.status = None
self.items = []
# 业务逻辑散落在Service层
class OrderService:
def pay(self, order_data, payment_info):
if order_data.status == "pending":
# 调用支付接口
order_data.status = "paid"
陷阱3:技术驱动而非业务驱动
为了用某个设计模式而用,而不是为了解决业务问题。
4.3 持续精炼模型
模型不是一次建成的,需要持续迭代:
与业务方定期沟通:验证模型是否符合预期
代码即文档:好的模型代码本身就是文档
重构不设限:发现模型缺陷及时重构
事件追溯:通过领域事件理解业务变迁
五、实战案例:电商促销系统建模
5.1 需求描述
设计一个促销系统,支持:
满减券:满100减10
折扣券:8折券,最高优惠50元
单品券:指定商品立减
叠加规则:部分券可叠加,部分不可
使用条件:订单金额、用户等级、商品范围等限制
5.2 建模过程
第一步:识别核心概念
优惠券(Coupon):包含类型、面额、使用条件
用户(User):领券、用券
订单(Order):使用优惠券
促销规则(PromotionRule):判断是否可用
优惠计算(DiscountCalculation):计算结果
第二步:建立模型
from abc import ABC, abstractmethod
from typing import Optional, List
from decimal import Decimal
from enum import Enum
from dataclasses import dataclass
from datetime import datetime
class CouponType(Enum):
CASH_DEDUCTION = "cash_deduction" # 满减券
PERCENTAGE_DISCOUNT = "percentage_discount" # 折扣券
FIXED_PRICE = "fixed_price" # 单品立减券
@dataclass
class UsageCondition:
"""使用条件值对象"""
min_order_amount: Optional[Decimal] = None # 最低订单金额
applicable_user_levels: List[str] = None # 适用用户等级
applicable_product_ids: List[str] = None # 适用商品ID
applicable_categories: List[str] = None # 适用商品分类
exclude_product_ids: List[str] = None # 排除商品ID
def is_satisfied_by(self, order: 'Order', user: 'User') -> bool:
# 检查订单金额
if self.min_order_amount and order.subtotal < self.min_order_amount:
return False
# 检查用户等级
if self.applicable_user_levels and user.level not in self.applicable_user_levels:
return False
# 检查商品范围
if self.applicable_product_ids:
order_product_ids = {item.product_id for item in order.items}
if not order_product_ids.issubset(set(self.applicable_product_ids)):
return False
# 检查排除商品
if self.exclude_product_ids:
order_product_ids = {item.product_id for item in order.items}
if order_product_ids.intersection(set(self.exclude_product_ids)):
return False
return True
class Coupon(ABC):
"""优惠券抽象基类"""
def __init__(self, coupon_id: str, name: str, condition: UsageCondition):
self.coupon_id = coupon_id
self.name = name
self.condition = condition
self.created_at = datetime.now()
self.used = False
@abstractmethod
def calculate_discount(self, order: 'Order', user: 'User') -> Decimal:
"""计算优惠金额"""
pass
def can_apply(self, order: 'Order', user: 'User') -> bool:
"""检查优惠券是否可用"""
if self.used:
return False
return self.condition.is_satisfied_by(order, user)
class CashDeductionCoupon(Coupon):
"""满减券:满X减Y"""
def __init__(self, coupon_id: str, name: str, condition: UsageCondition,
threshold: Decimal, deduction: Decimal):
super().__init__(coupon_id, name, condition)
self.threshold = threshold # 满多少
self.deduction = deduction # 减多少
# 自动设置最低金额条件
if not condition.min_order_amount or condition.min_order_amount < threshold:
condition.min_order_amount = threshold
def calculate_discount(self, order: 'Order', user: 'User') -> Decimal:
if not self.can_apply(order, user):
return Decimal('0')
if order.subtotal >= self.threshold:
return min(self.deduction, order.subtotal)
return Decimal('0')
class PercentageDiscountCoupon(Coupon):
"""折扣券:打X折,最高优惠Y元"""
def __init__(self, coupon_id: str, name: str, condition: UsageCondition,
discount_rate: Decimal, max_discount: Optional[Decimal] = None):
super().__init__(coupon_id, name, condition)
# discount_rate: 0.8 表示8折,0.9表示9折
self.discount_rate = discount_rate
self.max_discount = max_discount or Decimal('inf')
def calculate_discount(self, order: 'Order', user: 'User') -> Decimal:
if not self.can_apply(order, user):
return Decimal('0')
# 计算折扣金额
discount = order.subtotal * (Decimal('1') - self.discount_rate)
# 应用最高优惠限制
final_discount = min(discount, self.max_discount)
# 折扣不能超过订单金额
return min(final_discount, order.subtotal)
class FixedPriceCoupon(Coupon):
"""单品立减券:指定商品减指定金额"""
def __init__(self, coupon_id: str, name: str, condition: UsageCondition,
product_id: str, deduction: Decimal):
super().__init__(coupon_id, name, condition)
self.product_id = product_id
self.deduction = deduction
# 设置适用商品
if not condition.applicable_product_ids:
condition.applicable_product_ids = [product_id]
def calculate_discount(self, order: 'Order', user: 'User') -> Decimal:
if not self.can_apply(order, user):
return Decimal('0')
# 找到指定商品
for item in order.items:
if item.product_id == self.product_id:
# 不能超过商品价格
return min(self.deduction, item.price * item.quantity)
return Decimal('0')
class StackingRule:
"""优惠券叠加规则"""
def __init__(self):
self.allowed_combinations = {} # 类型 -> 可叠加的类型列表
self._init_default_rules()
def _init_default_rules(self):
# 默认规则:同类型优惠券不能叠加
for coupon_type in CouponType:
self.allowed_combinations[coupon_type] = [coupon_type]
def can_stack(self, coupon1: Coupon, coupon2: Coupon) -> bool:
"""判断两个优惠券能否叠加"""
type1 = self._get_coupon_type(coupon1)
type2 = self._get_coupon_type(coupon2)
# 检查类型1是否允许类型2叠加
if type2 not in self.allowed_combinations.get(type1, []):
return False
# 检查类型2是否允许类型1叠加
if type1 not in self.allowed_combinations.get(type2, []):
return False
# 检查是否有互斥的商品
return self._check_no_conflict(coupon1, coupon2)
def _get_coupon_type(self, coupon: Coupon) -> CouponType:
if isinstance(coupon, CashDeductionCoupon):
return CouponType.CASH_DEDUCTION
elif isinstance(coupon, PercentageDiscountCoupon):
return CouponType.PERCENTAGE_DISCOUNT
elif isinstance(coupon, FixedPriceCoupon):
return CouponType.FIXED_PRICE
raise ValueError("Unknown coupon type")
def _check_no_conflict(self, coupon1: Coupon, coupon2: Coupon) -> bool:
# 检查是否有商品同时被两个单品券锁定
fixed_coupons = []
for c in [coupon1, coupon2]:
if isinstance(c, FixedPriceCoupon):
fixed_coupons.append(c)
if len(fixed_coupons) > 1 and fixed_coupons[0].product_id != fixed_coupons[1].product_id:
# 两个不同的单品券作用于不同商品,可以叠加
return True
elif len(fixed_coupons) > 1:
# 同一个商品用了两张单品券,取最优
return False
return True
class ShoppingCartWithPromotion:
"""支持促销的购物车"""
def __init__(self, user: 'User', stacking_rule: StackingRule = None):
self.user = user
self._items = []
self.selected_coupons = []
self.stacking_rule = stacking_rule or StackingRule()
def add_item(self, product, quantity: int):
self._items.append(OrderItem(product, quantity))
def select_coupons(self, coupons: List[Coupon]):
"""选择要使用的优惠券组合"""
# 验证是否可以叠加
for i in range(len(coupons)):
for j in range(i + 1, len(coupons)):
if not self.stacking_rule.can_stack(coupons[i], coupons[j]):
raise CouponStackError(f"优惠券 {coupons[i].name} 和 {coupons[j].name} 不能叠加使用")
self.selected_coupons = coupons
@property
def items(self):
return list(self._items)
@property
def subtotal(self) -> Decimal:
return sum(item.subtotal() for item in self._items)
def calculate_final_price(self) -> dict:
"""计算最终价格和优惠明细"""
original_total = self.subtotal
remaining_amount = original_total
discount_details = []
# 计算优惠券的优先级:单品券 > 满减券 > 折扣券
sorted_coupons = sorted(self.selected_coupons,
key=lambda c: self._coupon_priority(c))
for coupon in sorted_coupons:
if remaining_amount <= 0:
break
discount = coupon.calculate_discount(self._get_temp_order(remaining_amount), self.user)
if discount > 0:
discount_details.append({
'coupon_name': coupon.name,
'discount': discount
})
remaining_amount -= discount
final_total = max(remaining_amount, Decimal('0'))
return {
'original_total': original_total,
'total_discount': original_total - final_total,
'final_total': final_total,
'discount_details': discount_details
}
def _coupon_priority(self, coupon: Coupon) -> int:
"""优惠券优先级:数字越小优先级越高"""
if isinstance(coupon, FixedPriceCoupon):
return 1
elif isinstance(coupon, CashDeductionCoupon):
return 2
elif isinstance(coupon, PercentageDiscountCoupon):
return 3
return 4
def _get_temp_order(self, remaining_amount):
"""创建临时订单用于计算(模拟已有优惠后的订单)"""
class TempOrder:
def __init__(self, items, amount):
self.items = items
self.subtotal = amount
return TempOrder(self.items, remaining_amount)
# 使用示例
def main():
# 创建用户
user = User("user_001", level="vip")
# 创建商品
laptop = Product("p001", "笔记本电脑", Decimal('5999'))
mouse = Product("p002", "鼠标", Decimal('99'))
# 创建购物车
cart = ShoppingCartWithPromotion(user)
cart.add_item(laptop, 1)
cart.add_item(mouse, 2)
print(f"原始总价:¥{cart.subtotal}")
# 原始总价:¥6197 (5999 + 99*2)
# 创建优惠券
# 1. 满5000减500的满减券
condition1 = UsageCondition(min_order_amount=Decimal('5000'))
cash_coupon = CashDeductionCoupon(
"c001", "618大促满减券", condition1,
threshold=Decimal('5000'), deduction=Decimal('500')
)
# 2. 全场9折券,最高优惠300
condition2 = UsageCondition()
discount_coupon = PercentageDiscountCoupon(
"c002", "VIP专享9折券", condition2,
discount_rate=Decimal('0.9'), max_discount=Decimal('300')
)
# 3. 鼠标单品立减20元券
condition3 = UsageCondition()
fixed_coupon = FixedPriceCoupon(
"c003", "鼠标优惠券", condition3,
product_id="p002", deduction=Decimal('20')
)
# 测试不同组合
# 组合1:只用满减券
cart.select_coupons([cash_coupon])
result = cart.calculate_final_price()
print(f"只用满减券:¥{result['final_total']} (省¥{result['total_discount']})")
# 组合2:满减+鼠标券
cart.select_coupons([cash_coupon, fixed_coupon])
result = cart.calculate_final_price()
print(f"满减+鼠标券:¥{result['final_total']} (省¥{result['total_discount']})")
for detail in result['discount_details']:
print(f" - {detail['coupon_name']}: 省¥{detail['discount']}")
# 组合3:满减+折扣券(需要检查叠加规则)
try:
cart.select_coupons([cash_coupon, discount_coupon])
result = cart.calculate_final_price()
print(f"满减+折扣券:¥{result['final_total']} (省¥{result['total_discount']})")
except CouponStackError as e:
print(f"叠加失败:{e}")
第三步:模型演进
随着业务发展,促销系统需要支持:
秒杀价
拼团优惠
会员日额外折扣
模型通过扩展而非修改的方式来应对变化(开放封闭原则)。
来源:
https://htnus.cn/