×
¥
查看详情
🔥 会员专享 文生代码 其它

设计模式解析

👁️ 509 次查看
📅 Nov 24, 2025
💡 核心价值: 本提示词可针对指定设计模式,详细解析其结构、关键角色、使用场景和优缺点,并结合具体编程语言提供清晰代码示例,帮助开发者快速理解并在组件设计中应用。

🎯 可自定义参数(5个)

设计模式名称
需要讲解的设计模式名称
编程语言
代码示例使用的编程语言
示例复杂度
代码示例的复杂程度
应用目标场景
示例应用在实际组件或系统中的作用
示例类型偏好
代码示例的类型偏好

🎨 效果示例

以下内容围绕策略模式在 Python 的作用、结构与实践展开,并包含一个面向结算域(Checkout)的进阶 FastAPI 服务端示例,通过策略模式将促销与税费规则解耦,支持渠道与会员等级组合、运行时策略链选择、A/B 测试与灰度发布。

一、策略模式在 Python 的作用

  • 目标:将算法(业务规则)封装为可互换的策略对象,通过统一接口在运行时选择和组合,避免大量 if-else/switch。
  • 价值:
    • 解耦:调用方不依赖具体促销/税费规则,实现模块化和可插拔。
    • 扩展:新增策略不影响既有代码,开放封闭原则(OCP)。
    • 测试友好:策略是纯粹的小单元,可独立编写和测试。
    • 部署灵活:可根据配置中心策略键动态启用/禁用与组合,适配 A/B 和灰度。

二、结构(关键组件与角色)

  • Strategy 接口:定义统一行为(如 apply(context)),可包含适用性判断 is_applicable(context)。
  • ConcreteStrategy:具体策略实现(如百分比折扣、满减、阶梯价、包邮、税费)。
  • Context(业务上下文):承载结算输入与中间结果(价格、税费、运费、用户标签、渠道、站点等)。
  • StrategyRegistry:策略注册表,按“类型”生成策略实例,支持根据配置构建策略链。
  • ConfigService(外部配置中心代理):从配置下发策略键与参数,支持 A/B/灰度。
  • CheckoutService(应用上下文):根据请求信息和配置构造策略链并执行,返回价格拆解。

三、常见使用场景

  • 电商结算:折扣、满减、阶梯价、包邮、税费、多币种换算、复杂排他/叠加规则。
  • 订阅/会员:分层价、按渠道/地域差异定价、合规税费。
  • 营销试验:A/B 测试、策略灰度发布、按用户标签/站点切换。

四、优缺点

  • 优点:
    • 高内聚低耦合:策略可被独立替换与组合。
    • 运行时可配置:面向策略键的动态加载。
    • 易测试:每个策略单测简单明确。
  • 缺点:
    • 策略爆炸:策略数量多时需要良好治理(命名、注册、可见性)。
    • 顺序与相互影响需定义:促销、税、运费的执行顺序影响结果,需在架构层明确排序与约束。
    • 配置复杂度:配置中心需要良好的参数校验与回滚机制。

五、Python FastAPI 服务端示例(结算域:促销与税费解耦、策略链组合) 说明:

  • 设计 PriceStrategy 接口,具体实现包含:百分比折扣、满减、阶梯价、包邮;附加 VAT 税费策略展示税费解耦。
  • 支持按渠道与会员等级组合。
  • CheckoutService 根据站点、用户标签与配置中心的策略键,在运行时选择并组合策略链。
  • 结构单元测试友好,含策略扩展模板。

代码示例(可直接运行的结构化示例,省略持久化与真实配置中心接入,以内存模拟配置下发):

from __future__ import annotations
from typing import List, Dict, Any, Optional, Callable
from decimal import Decimal, ROUND_HALF_UP, getcontext
from dataclasses import dataclass, replace
from enum import Enum
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel, Field, condecimal, conint, validator

# -----------------------------
# Money & enums
# -----------------------------
getcontext().prec = 28  # sufficient precision

def money(v: Decimal) -> Decimal:
    return v.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

class Channel(str, Enum):
    web = "web"
    mobile = "mobile"
    partner = "partner"

class MemberLevel(str, Enum):
    standard = "standard"
    silver = "silver"
    gold = "gold"

# -----------------------------
# Pydantic models (request/response)
# -----------------------------
class LineItem(BaseModel):
    sku: str = Field(..., min_length=1)
    qty: conint(gt=0) = 1
    unit_price: condecimal(gt=0, max_digits=12, decimal_places=2)

class CheckoutRequest(BaseModel):
    site: str = Field(..., min_length=1)
    channel: Channel
    member_level: MemberLevel
    user_tags: List[str] = []
    currency: str = "CNY"
    items: List[LineItem]
    base_shipping_fee: condecimal(ge=0, max_digits=12, decimal_places=2) = Decimal("0.00")
    ab_bucket: Optional[str] = None

    @validator("items")
    def non_empty_items(cls, v):
        if not v:
            raise ValueError("items cannot be empty")
        return v

class StrategyApplication(BaseModel):
    key: str
    description: str
    delta: condecimal(max_digits=12, decimal_places=2)
    meta: Dict[str, Any] = {}

class CheckoutResponse(BaseModel):
    currency: str
    subtotal: condecimal(max_digits=12, decimal_places=2)
    discount_total: condecimal(max_digits=12, decimal_places=2)
    shipping_fee: condecimal(max_digits=12, decimal_places=2)
    tax_total: condecimal(max_digits=12, decimal_places=2)
    final_total: condecimal(max_digits=12, decimal_places=2)
    applied: List[StrategyApplication]

# -----------------------------
# Runtime PricingContext
# -----------------------------
@dataclass(frozen=True)
class PricingContext:
    site: str
    channel: Channel
    member_level: MemberLevel
    user_tags: List[str]
    currency: str

    subtotal: Decimal
    total_qty: int

    discount_total: Decimal
    shipping_fee: Decimal
    tax_total: Decimal
    final_total: Decimal

    applied: List[StrategyApplication]

    def with_updates(
        self,
        *,
        discount_delta: Decimal = Decimal("0.00"),
        shipping_delta: Decimal = Decimal("0.00"),
        tax_delta: Decimal = Decimal("0.00"),
        application: Optional[StrategyApplication] = None,
    ) -> "PricingContext":
        new_discount = money(self.discount_total + discount_delta)
        new_shipping = money(self.shipping_fee + shipping_delta)
        new_tax = money(self.tax_total + tax_delta)
        new_final = money(self.subtotal - new_discount + new_shipping + new_tax)
        new_applied = list(self.applied)
        if application:
            new_applied.append(application)
        return replace(
            self,
            discount_total=new_discount,
            shipping_fee=new_shipping,
            tax_total=new_tax,
            final_total=new_final,
            applied=new_applied,
        )

# -----------------------------
# Strategy interface & registry
# -----------------------------
class PriceStrategy:
    key: str
    description: str
    order: int  # lower first; promotions < shipping < tax

    def is_applicable(self, ctx: PricingContext) -> bool:
        return True

    def apply(self, ctx: PricingContext) -> PricingContext:
        raise NotImplementedError

# Strategy spec from ConfigService
class StrategySpec(BaseModel):
    type: str  # e.g., "percentage_discount"
    key: str
    params: Dict[str, Any] = {}
    order: int = 100  # default

StrategyFactory = Callable[[StrategySpec], PriceStrategy]

class StrategyRegistry:
    def __init__(self):
        self._builders: Dict[str, StrategyFactory] = {}

    def register(self, type_name: str, builder: StrategyFactory):
        self._builders[type_name] = builder

    def build(self, spec: StrategySpec) -> PriceStrategy:
        if spec.type not in self._builders:
            raise KeyError(f"Unknown strategy type: {spec.type}")
        strategy = self._builders[spec.type](spec)
        # allow spec to override default order
        strategy.order = spec.order
        return strategy

# -----------------------------
# Concrete strategies
# -----------------------------
class PercentageDiscountStrategy(PriceStrategy):
    def __init__(self, key: str, percent: Decimal, allowed_channels: List[Channel], allowed_levels: List[MemberLevel]):
        self.key = key
        self.description = f"{percent}% off"
        self.percent = Decimal(percent)
        self.allowed_channels = set(allowed_channels)
        self.allowed_levels = set(allowed_levels)
        self.order = 10

    def is_applicable(self, ctx: PricingContext) -> bool:
        return ctx.channel in self.allowed_channels and ctx.member_level in self.allowed_levels

    def apply(self, ctx: PricingContext) -> PricingContext:
        discount = money(ctx.subtotal * self.percent / Decimal("100"))
        if discount <= Decimal("0.00"):
            return ctx
        return ctx.with_updates(
            discount_delta=discount,
            application=StrategyApplication(
                key=self.key, description=self.description, delta=-discount, meta={"percent": str(self.percent)}
            ),
        )

class FullReductionStrategy(PriceStrategy):
    def __init__(self, key: str, rules: List[Dict[str, Any]], allowed_channels: List[Channel]):
        """
        rules: [{threshold: 300, reduce: 40}, {threshold: 600, reduce: 90}]
        pick the best rule for subtotal
        """
        self.key = key
        self.description = "Full reduction"
        self.rules = sorted(rules, key=lambda r: Decimal(str(r["threshold"])))
        self.allowed_channels = set(allowed_channels)
        self.order = 20

    def is_applicable(self, ctx: PricingContext) -> bool:
        return ctx.channel in self.allowed_channels

    def apply(self, ctx: PricingContext) -> PricingContext:
        applicable = [r for r in self.rules if ctx.subtotal >= Decimal(str(r["threshold"]))]
        if not applicable:
            return ctx
        best = max(applicable, key=lambda r: Decimal(str(r["reduce"])))
        reduce_amt = money(Decimal(str(best["reduce"])))
        return ctx.with_updates(
            discount_delta=reduce_amt,
            application=StrategyApplication(
                key=self.key, description=f"满减: 满{best['threshold']}减{best['reduce']}", delta=-reduce_amt, meta=best
            ),
        )

class TieredPricingStrategy(PriceStrategy):
    def __init__(self, key: str, tiers: List[Dict[str, Any]], allowed_levels: List[MemberLevel]):
        """
        tiers: [{min_qty: 3, percent: 5}, {min_qty: 5, percent: 10}]
        applies percent discount based on total_qty
        """
        self.key = key
        self.description = "Tiered pricing by qty"
        self.tiers = sorted(tiers, key=lambda t: int(t["min_qty"]))
        self.allowed_levels = set(allowed_levels)
        self.order = 15

    def is_applicable(self, ctx: PricingContext) -> bool:
        return ctx.member_level in self.allowed_levels

    def apply(self, ctx: PricingContext) -> PricingContext:
        applicable = [t for t in self.tiers if ctx.total_qty >= int(t["min_qty"])]
        if not applicable:
            return ctx
        best = max(applicable, key=lambda t: Decimal(str(t["percent"])))
        percent = Decimal(str(best["percent"]))
        discount = money(ctx.subtotal * percent / Decimal("100"))
        if discount <= Decimal("0.00"):
            return ctx
        return ctx.with_updates(
            discount_delta=discount,
            application=StrategyApplication(
                key=self.key, description=f"阶梯价: 满{best['min_qty']}件打{percent}%", delta=-discount, meta=best
            ),
        )

class FreeShippingStrategy(PriceStrategy):
    def __init__(self, key: str, threshold: Optional[Decimal], allowed_channels: List[Channel], allowed_levels: List[MemberLevel]):
        self.key = key
        self.description = "Free shipping"
        self.threshold = Decimal(str(threshold)) if threshold is not None else None
        self.allowed_channels = set(allowed_channels)
        self.allowed_levels = set(allowed_levels)
        self.order = 30  # after promotions but before tax

    def is_applicable(self, ctx: PricingContext) -> bool:
        if ctx.channel not in self.allowed_channels or ctx.member_level not in self.allowed_levels:
            return False
        if self.threshold is None:
            return True
        # Apply free shipping if subtotal after discounts >= threshold
        subtotal_after_discount = money(ctx.subtotal - ctx.discount_total)
        return subtotal_after_discount >= self.threshold

    def apply(self, ctx: PricingContext) -> PricingContext:
        if ctx.shipping_fee <= Decimal("0.00"):
            return ctx
        waive = ctx.shipping_fee
        return ctx.with_updates(
            shipping_delta=-waive,
            application=StrategyApplication(
                key=self.key, description="包邮", delta=-waive, meta={"threshold": str(self.threshold) if self.threshold else None}
            ),
        )

class VATTaxStrategy(PriceStrategy):
    def __init__(self, key: str, vat_percent: Decimal):
        self.key = key
        self.description = "VAT tax"
        self.vat_percent = Decimal(vat_percent)
        self.order = 40  # taxes last

    def is_applicable(self, ctx: PricingContext) -> bool:
        return True

    def apply(self, ctx: PricingContext) -> PricingContext:
        taxable_base = money(ctx.subtotal - ctx.discount_total + ctx.shipping_fee)
        tax = money(taxable_base * self.vat_percent / Decimal("100"))
        if tax <= Decimal("0.00"):
            return ctx
        return ctx.with_updates(
            tax_delta=tax,
            application=StrategyApplication(
                key=self.key, description=f"VAT {self.vat_percent}%", delta=tax, meta={"base": str(taxable_base)}
            ),
        )

# -----------------------------
# Config service (simulated)
# -----------------------------
class ConfigService:
    """
    Simulates strategy specs from config center.
    In real system, this could read from a KV store / feature flag service.
    """

    def get_strategy_specs(self, site: str, channel: Channel, member_level: MemberLevel, user_tags: List[str], ab_bucket: Optional[str]) -> List[StrategySpec]:
        # Simplified rules for demo; would be data-driven in production
        specs: List[StrategySpec] = []

        # Example A/B buckets
        bucket = ab_bucket or "A"

        # Percentage discount: mobile + gold users get 10% in bucket A, 12% in bucket B
        percent = "12" if bucket == "B" else "10"
        specs.append(StrategySpec(
            type="percentage_discount",
            key=f"percent-{percent}-mobile-gold",
            params={"percent": percent, "allowed_channels": ["mobile"], "allowed_levels": ["gold"]},
            order=10,
        ))

        # Tiered pricing for silver/gold
        specs.append(StrategySpec(
            type="tiered_pricing",
            key="tier-qty",
            params={"tiers": [{"min_qty": 3, "percent": 5}, {"min_qty": 5, "percent": 10}], "allowed_levels": ["silver", "gold"]},
            order=15,
        ))

        # Full reduction on web + mobile
        specs.append(StrategySpec(
            type="full_reduction",
            key="full-300-40",
            params={"rules": [{"threshold": 300, "reduce": 40}, {"threshold": 600, "reduce": 90}], "allowed_channels": ["web", "mobile"]},
            order=20,
        ))

        # Free shipping over 200 for web/mobile, gold only
        specs.append(StrategySpec(
            type="free_shipping",
            key="free-ship-200",
            params={"threshold": "200", "allowed_channels": ["web", "mobile"], "allowed_levels": ["gold"]},
            order=30,
        ))

        # VAT tax 7%
        specs.append(StrategySpec(
            type="vat_tax",
            key="vat-7",
            params={"vat_percent": "7"},
            order=40,
        ))
        return specs

# -----------------------------
# Registry DI setup
# -----------------------------
def create_registry() -> StrategyRegistry:
    reg = StrategyRegistry()

    reg.register("percentage_discount", lambda spec: PercentageDiscountStrategy(
        key=spec.key,
        percent=Decimal(str(spec.params["percent"])),
        allowed_channels=[Channel(c) for c in spec.params.get("allowed_channels", [])],
        allowed_levels=[MemberLevel(l) for l in spec.params.get("allowed_levels", [])],
    ))

    reg.register("full_reduction", lambda spec: FullReductionStrategy(
        key=spec.key,
        rules=spec.params["rules"],
        allowed_channels=[Channel(c) for c in spec.params.get("allowed_channels", [])],
    ))

    reg.register("tiered_pricing", lambda spec: TieredPricingStrategy(
        key=spec.key,
        tiers=spec.params["tiers"],
        allowed_levels=[MemberLevel(l) for l in spec.params.get("allowed_levels", [])],
    ))

    reg.register("free_shipping", lambda spec: FreeShippingStrategy(
        key=spec.key,
        threshold=Decimal(str(spec.params.get("threshold"))) if "threshold" in spec.params else None,
        allowed_channels=[Channel(c) for c in spec.params.get("allowed_channels", [])],
        allowed_levels=[MemberLevel(l) for l in spec.params.get("allowed_levels", [])],
    ))

    reg.register("vat_tax", lambda spec: VATTaxStrategy(
        key=spec.key,
        vat_percent=Decimal(str(spec.params["vat_percent"])),
    ))
    return reg

# DI providers for FastAPI
def get_config_service() -> ConfigService:
    return ConfigService()

def get_strategy_registry() -> StrategyRegistry:
    return create_registry()

# -----------------------------
# Pipeline executor (test-friendly)
# -----------------------------
def execute_pricing_pipeline(
    ctx: PricingContext,
    specs: List[StrategySpec],
    registry: StrategyRegistry,
) -> PricingContext:
    strategies: List[PriceStrategy] = []
    for spec in specs:
        st = registry.build(spec)
        strategies.append(st)

    # sort by order; if same order, keep config order
    strategies.sort(key=lambda s: s.order)

    current = ctx
    for s in strategies:
        if s.is_applicable(current):
            current = s.apply(current)
    return current

# -----------------------------
# FastAPI app & handler
# -----------------------------
app = FastAPI(title="Checkout Pricing Service")

@app.post("/checkout/price", response_model=CheckoutResponse)
def checkout_price(
    req: CheckoutRequest,
    cfg: ConfigService = Depends(get_config_service),
    reg: StrategyRegistry = Depends(get_strategy_registry),
):
    subtotal = sum((Decimal(item.unit_price) * item.qty for item in req.items), start=Decimal("0.00"))
    subtotal = money(subtotal)
    total_qty = sum(item.qty for item in req.items)

    initial = PricingContext(
        site=req.site,
        channel=req.channel,
        member_level=req.member_level,
        user_tags=req.user_tags,
        currency=req.currency,
        subtotal=subtotal,
        total_qty=total_qty,
        discount_total=Decimal("0.00"),
        shipping_fee=money(Decimal(req.base_shipping_fee)),
        tax_total=Decimal("0.00"),
        final_total=money(subtotal + Decimal(req.base_shipping_fee)),
        applied=[],
    )

    try:
        specs = cfg.get_strategy_specs(req.site, req.channel, req.member_level, req.user_tags, req.ab_bucket)
        final_ctx = execute_pricing_pipeline(initial, specs, reg)
    except KeyError as e:
        raise HTTPException(status_code=400, detail=str(e))

    return CheckoutResponse(
        currency=final_ctx.currency,
        subtotal=money(final_ctx.subtotal),
        discount_total=money(final_ctx.discount_total),
        shipping_fee=money(final_ctx.shipping_fee),
        tax_total=money(final_ctx.tax_total),
        final_total=money(final_ctx.final_total),
        applied=final_ctx.applied,
    )

# -----------------------------
# Unit test hints (not executed here)
# -----------------------------
"""
Example test for strategies:

def test_full_reduction():
    ctx = PricingContext(
        site="cn", channel=Channel.web, member_level=MemberLevel.standard, user_tags=[],
        currency="CNY", subtotal=Decimal("650.00"), total_qty=2,
        discount_total=Decimal("0.00"), shipping_fee=Decimal("20.00"),
        tax_total=Decimal("0.00"), final_total=Decimal("670.00"), applied=[]
    )
    spec = StrategySpec(
        type="full_reduction", key="full-300-40",
        params={"rules":[{"threshold":300,"reduce":40},{"threshold":600,"reduce":90}], "allowed_channels":["web"]},
        order=20
    )
    reg = create_registry()
    st = reg.build(spec)
    assert st.is_applicable(ctx)
    new_ctx = st.apply(ctx)
    assert new_ctx.discount_total == Decimal("90.00")
    assert any(a.key=="full-300-40" for a in new_ctx.applied)

# Integration test:
def test_pipeline_order():
    reg = create_registry()
    cfg = ConfigService()
    # Build context and pipeline as in endpoint, assert final_total and applied order.
"""

# -----------------------------
# Strategy extension template
# -----------------------------
class NewPriceStrategyTemplate(PriceStrategy):
    """
    Template for introducing a new strategy.
    1) Define key, description, order.
    2) Implement is_applicable(ctx) with channel/level/tags/geo rules if needed.
    3) Implement apply(ctx) returning a NEW context via ctx.with_updates(...)
    4) Register builder in StrategyRegistry.
    """
    def __init__(self, key: str, params: Dict[str, Any]):
        self.key = key
        self.description = "New Strategy"
        self.order = params.get("order", 25)
        self.params = params

    def is_applicable(self, ctx: PricingContext) -> bool:
        # Example: only on partner channel and silver+
        allowed_channels = set([Channel.partner])
        return ctx.channel in allowed_channels and ctx.member_level in {MemberLevel.silver, MemberLevel.gold}

    def apply(self, ctx: PricingContext) -> PricingContext:
        # Example: fixed discount of 15
        discount = money(Decimal("15.00"))
        return ctx.with_updates(
            discount_delta=discount,
            application=StrategyApplication(key=self.key, description=self.description, delta=-discount, meta=self.params),
        )

# To enable the template:
# reg.register("new_strategy_type", lambda spec: NewPriceStrategyTemplate(key=spec.key, params=spec.params))

设计要点与落地建议:

  • 排序约定:促销类(折扣/满减/阶梯价)应先执行,再处理运费(包邮等),最后计算税费。通过策略的 order 字段定义。
  • 可观察性:在 PricingContext.applied 保留策略应用日志,便于审计与问题定位。
  • 配置治理:
    • 为每类策略定义严格的参数模式(Schema)与校验,避免错误参数导致价格异常。
    • 给策略键命名规范(type-params-channel-level),支持灰度回滚。
  • 叠加与排他:
    • 若需要复杂排他(如满减与百分比折扣不可叠加),可在 StrategyRegistry 增加“冲突检测”,或在 apply 中查询已应用的策略并决定跳过。
  • 多租户与渠道隔离:
    • 根据 site/channel/member_level/user_tags 过滤 is_applicable,避免策略越权。
  • 钱包精度与货币:
    • 全链路使用 Decimal 并统一量化到 0.01,必要时引入货币与税区(Geo)的抽象分层。

下面从作用、结构、常见场景、优缺点四个方面讲解观察者模式,并给出一个原生 JavaScript 的前端组件示例,展示轻量状态中心的 subscribe/notify 机制,含取消订阅、防抖更新、一次性订阅与内存泄漏防护,以及简单的对接和测试用例。

一、作用

  • 观察者模式用于实现一对多的发布/订阅。主题(Subject)维护观察者(Observer)列表,当有状态变更或事件产生时,通知所有订阅者。
  • 在前端中,它能解耦“状态来源”和“呈现组件”,避免组件之间直接相互依赖,降低耦合度,提升可维护性。
  • 适用于跨组件通信、实时数据推送、主题/语言切换等事件驱动场景。

二、结构(关键组件与角色)

  • Subject(主题/发布者):维护订阅者列表,提供 subscribe(topic, handler, options)、notify(topic, payload) 等方法。
  • Observer(观察者/订阅者):通过 subscribe 注册回调以接收通知。
  • Event/Topic(事件类型或主题):如 theme、fxRate、alertLevel、locale 等具体业务事件。
  • Subscription(订阅实体):用于管理一次性订阅、取消订阅、防抖、生命周期绑定(内存泄漏防护)等。

三、常见使用场景

  • UI 仪表盘:多个小部件响应主题切换、实时数据(如汇率)推送、国际化语言变化。
  • 全局事件总线:跨页面、跨组件的消息广播。
  • 数据流/日志流:后端推送或 WebSocket 流数据分发。
  • 插件机制:插件订阅宿主应用的事件扩展自身行为。

四、优缺点

  • 优点
    • 解耦:主题与观察者只通过事件契约交互。
    • 扩展性好:可动态增删订阅者,支持多种事件类型。
    • 适合实时/异步场景:广播机制、响应式更新。
  • 缺点
    • 调试复杂:订阅链条长、事件源头难以追踪。
    • 内存泄漏风险:忘记取消订阅或长时间持有回调。
    • 时序不确定:订阅者执行顺序不可控,可能引入竞态。
    • 类型约束弱(在 JS 中):事件载荷结构约定需自律。
  • 缓解策略
    • 绑定生命周期(如 AbortController/信号)自动清理。
    • 仪表化与日志:对 notify 进行埋点。
    • 主题常量化与文档化载荷结构。
    • 在高频事件使用防抖/节流。

五、代码示例(原生 JavaScript,Web Components,含取消订阅、防抖、一次性订阅、内存泄漏防护) 功能目标:在前端仪表盘中,多个小部件订阅主题(theme)、汇率(fxRate)、告警等级(alertLevel)、语言(locale)变化,通过观察者模式的轻量状态中心实现解耦更新。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Observer Pattern Demo</title>
  <style>
    body { font-family: system-ui, Arial, sans-serif; }
    .panel { border: 1px solid #ccc; padding: 8px; margin: 8px; border-radius: 6px; }
    .dark { background: #222; color: #eee; }
    .light { background: #fff; color: #222; }
  </style>
</head>
<body>
  <theme-widget class="panel"></theme-widget>
  <fx-widget class="panel"></fx-widget>
  <alert-widget class="panel"></alert-widget>

  <script>
    // ---------- 工具:防抖 ----------
    function debounce(fn, wait) {
      let t = null, lastArgs = null;
      const wrapped = function(...args) {
        lastArgs = args;
        if (t) clearTimeout(t);
        t = setTimeout(() => {
          t = null;
          try { fn(...lastArgs); } catch (e) { console.error('[debounced handler error]', e); }
        }, wait);
      };
      wrapped.cancel = () => { if (t) { clearTimeout(t); t = null; } };
      return wrapped;
    }

    // ---------- 观察者核心:发布/订阅中心 ----------
    class ObserverCenter {
      constructor() {
        this._subs = new Map(); // topic => Set<entry>
      }
      subscribe(topic, handler, options = {}) {
        const { once = false, debounceMs = 0, signal } = options;
        let fn = handler;
        if (debounceMs > 0) fn = debounce(handler, debounceMs);

        const entry = { handler, fn, once, signal };
        let set = this._subs.get(topic);
        if (!set) { set = new Set(); this._subs.set(topic, set); }
        set.add(entry);

        const unsubscribe = () => {
          const s = this._subs.get(topic);
          if (!s || !s.has(entry)) return;
          s.delete(entry);
          if (entry.fn && entry.fn.cancel) entry.fn.cancel(); // 清理定时器,避免内存泄漏
        };

        // 生命周期绑定:AbortSignal 触发自动取消订阅(内存泄漏防护)
        if (signal) {
          if (signal.aborted) unsubscribe();
          else signal.addEventListener('abort', unsubscribe, { once: true });
        }

        return unsubscribe;
      }
      notify(topic, payload) {
        const set = this._subs.get(topic);
        if (!set || set.size === 0) return;
        // 复制集合避免回调内修改集合带来的遍历问题
        for (const entry of Array.from(set)) {
          try { entry.fn(payload); } catch (e) { console.error('[notify handler error]', e); }
          if (entry.once) {
            set.delete(entry);
            if (entry.fn && entry.fn.cancel) entry.fn.cancel();
          }
        }
      }
      subscriberCount(topic) {
        const set = this._subs.get(topic);
        return set ? set.size : 0;
      }
    }

    // ---------- 业务状态中心 ----------
    const Topics = {
      theme: 'theme',
      fxRate: 'fxRate',
      alertLevel: 'alertLevel',
      locale: 'locale',
    };

    class DashboardStore extends ObserverCenter {
      constructor() {
        super();
        this.state = {
          theme: 'light',
          fxRate: 7.20,
          alertLevel: 'info',
          locale: 'en-US',
        };
      }
      setTheme(theme) { this.state.theme = theme; this.notify(Topics.theme, theme); }
      pushFxRate(rate) { this.state.fxRate = rate; this.notify(Topics.fxRate, rate); }
      setAlertLevel(level) { this.state.alertLevel = level; this.notify(Topics.alertLevel, level); }
      setLocale(locale) { this.state.locale = locale; this.notify(Topics.locale, locale); }
      getState() { return { ...this.state }; }
    }

    const store = new DashboardStore();

    // ---------- Web Components ----------
    class ThemeWidget extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this._abort = new AbortController(); // 生命周期绑定
      }
      connectedCallback() {
        const s = store.getState();
        this.render(s.theme, s.locale);

        // 订阅主题变化
        store.subscribe(Topics.theme, (theme) => {
          this.applyTheme(theme);
          this.shadowRoot.getElementById('theme-text').textContent = `Theme: ${theme}`;
        }, { signal: this._abort.signal });

        // 一次性订阅:仅首次语言变化时,更新欢迎文案
        store.subscribe(Topics.locale, (locale) => {
          this.shadowRoot.getElementById('welcome').textContent = this.getWelcome(locale);
        }, { once: true, signal: this._abort.signal });

        // 手动取消订阅示例(非必须):在2秒后不再关注主题(演示取消机制)
        this._manualUnsub = store.subscribe(Topics.theme, (theme) => {
          console.log('[ThemeWidget second handler]', theme);
        }, { signal: this._abort.signal });
        setTimeout(() => {
          this._manualUnsub(); // 取消订阅
          console.log('[ThemeWidget] manually unsubscribed second handler');
        }, 2000);
      }
      disconnectedCallback() {
        // 自动清理所有绑定了 this._abort.signal 的订阅
        this._abort.abort();
      }
      applyTheme(theme) {
        const host = this.shadowRoot.host;
        host.classList.remove('light', 'dark');
        host.classList.add(theme);
      }
      getWelcome(locale) {
        return locale === 'zh-CN' ? '欢迎使用仪表盘' : 'Welcome to the dashboard';
      }
      render(theme = 'light', locale = 'en-US') {
        this.shadowRoot.innerHTML = `
          <div>
            <div id="welcome">${this.getWelcome(locale)}</div>
            <div id="theme-text">Theme: ${theme}</div>
            <button id="toggle">Toggle Theme</button>
          </div>
        `;
        this.applyTheme(theme);
        this.shadowRoot.getElementById('toggle').onclick = () => {
          const next = store.getState().theme === 'light' ? 'dark' : 'light';
          store.setTheme(next);
        };
      }
    }
    customElements.define('theme-widget', ThemeWidget);

    class FxWidget extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this._abort = new AbortController();
      }
      connectedCallback() {
        const s = store.getState();
        this.shadowRoot.innerHTML = `
          <div>
            <div>FX Rate: <span id="rate">${s.fxRate.toFixed(2)}</span></div>
            <button id="burst">Burst Updates</button>
          </div>
        `;
        // 高频汇率更新:使用防抖减少 DOM 更新频率
        store.subscribe(Topics.fxRate, (rate) => {
          this.shadowRoot.getElementById('rate').textContent = rate.toFixed(2);
        }, { debounceMs: 150, signal: this._abort.signal });

        // 一次性订阅:仅首次汇率推送时校准一次
        store.subscribe(Topics.fxRate, (rate) => {
          console.log('[FxWidget] first tick calibrated:', rate);
        }, { once: true, signal: this._abort.signal });

        this.shadowRoot.getElementById('burst').onclick = () => {
          // 模拟快速推送:仅触发少量防抖后的 DOM 更新
          const base = store.getState().fxRate;
          for (let i = 1; i <= 10; i++) {
            setTimeout(() => store.pushFxRate(base + i * 0.01), i * 20);
          }
        };
      }
      disconnectedCallback() { this._abort.abort(); }
    }
    customElements.define('fx-widget', FxWidget);

    class AlertWidget extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this._abort = new AbortController();
      }
      connectedCallback() {
        const s = store.getState();
        this.shadowRoot.innerHTML = `
          <div>
            <div>Alert: <span id="level">${s.alertLevel}</span></div>
            <button id="warn">Warn</button>
            <button id="crit">Critical</button>
          </div>
        `;
        store.subscribe(Topics.alertLevel, (level) => {
          const el = this.shadowRoot.getElementById('level');
          el.textContent = level;
          el.style.color = level === 'critical' ? 'red' : (level === 'warning' ? 'orange' : '');
        }, { signal: this._abort.signal });

        // 一次性订阅:首次出现 critical 级别时提示一次
        store.subscribe(Topics.alertLevel, (level) => {
          console.log('[AlertWidget] first alert event:', level);
        }, { once: true, signal: this._abort.signal });

        this.shadowRoot.getElementById('warn').onclick = () => store.setAlertLevel('warning');
        this.shadowRoot.getElementById('crit').onclick = () => store.setAlertLevel('critical');
      }
      disconnectedCallback() { this._abort.abort(); }
    }
    customElements.define('alert-widget', AlertWidget);

    // ---------- 演示:主题/语言初始推送 ----------
    setTimeout(() => store.setLocale('zh-CN'), 500);  // 仅 ThemeWidget 第一次语言订阅会生效
    setTimeout(() => store.setTheme('dark'), 800);
    setTimeout(() => store.setAlertLevel('info'), 1000);

    // ---------- 简单测试用例(控制台断言/检查) ----------
    (function tests() {
      console.log('--- Tests start ---');
      // 1) 订阅数量检查
      console.log('theme subscribers:', store.subscriberCount(Topics.theme)); // >= 1
      // 2) 一次性订阅在首次触发后应被移除
      const onceCheck = [];
      const unsub = store.subscribe(Topics.theme, (t) => onceCheck.push(t), { once: true });
      store.setTheme('light'); // 触发一次
      store.setTheme('dark');  // 不应再记录
      console.log('onceCheck:', onceCheck); // 期望 ['light']
      // 3) 防抖:多次快速推送仅触发少量更新
      let count = 0;
      const unsubFx = store.subscribe(Topics.fxRate, () => count++, { debounceMs: 100 });
      for (let i = 0; i < 10; i++) store.pushFxRate(7.5 + i * 0.01);
      setTimeout(() => {
        unsubFx();
        console.log('debounced updates count ~', count, '(should be << 10)');
      }, 500);
      // 4) 内存泄漏防护:绑定 AbortSignal 的订阅在组件卸载后自动清理
      const temp = new AbortController();
      store.subscribe(Topics.theme, () => {}, { signal: temp.signal });
      console.log('temp theme subscribers before abort:', store.subscriberCount(Topics.theme));
      temp.abort();
      console.log('temp theme subscribers after abort:', store.subscriberCount(Topics.theme));
      console.log('--- Tests end ---');
    })();
  </script>
</body>
</html>

说明与要点

  • 轻量状态中心(DashboardStore)继承 ObserverCenter,面向事件主题提供 notify;同时提供领域方法 setTheme、pushFxRate、setAlertLevel、setLocale,隔离业务入口。
  • subscribe 支持:
    • once:一次性订阅,首次通知后自动取消。
    • debounceMs:防抖,减少高频事件对 UI 的影响(如 fxRate)。
    • signal:AbortController 绑定组件生命周期,disconnectedCallback 中 abort 自动清理,防止内存泄漏。
  • 返回值为 unsubscribe 函数,便于手动取消订阅(示例中 ThemeWidget 手动取消一个额外订阅)。
  • Web Components 示例展示在框架无关环境(原生 DOM)下的对接方式,适合实际仪表盘场景。
  • 简单测试用例通过控制台检查订阅数量、一次性订阅行为、防抖效果和生命周期自动清理。

扩展建议

  • 在大型项目中为每个事件定义稳定的载荷结构与类型(在 TS 中定义接口),并对 notify 做日志打点。
  • 对高风险主题增加节流(throttle)或批处理(batch)策略。
  • 若需跨页面或持久化,可将 store 封装为单例并结合 BroadcastChannel/WebSocket 等。

下面以资深架构师视角系统讲解 C# 中的装饰器模式,并给出一个面向服务端的完整示例(订单查询微服务),演示如何用装饰器为仓储接口分层增加 Logging、Caching、Retry 三个能力,避免污染核心查询逻辑,同时展示 IoC 注册、装饰器链顺序控制、缓存键规范与滑动过期策略,以及在 ASP.NET Minimal API 中的接入与单元测试验证。

一、概念与作用

  • 定义:装饰器模式是一种结构型模式,在不修改被装饰对象(组件)代码的前提下,通过包裹(wrap)对象的方式按层次叠加行为。对外仍暴露同样的接口。
  • 作用:
    • 将核心业务(如仓储查询)与横切关注点(日志、缓存、重试、度量、鉴权等)解耦
    • 动态组合能力(按需选择与排序),避免“上帝类”或散落的重复代码
    • 满足开闭原则:对扩展开放,对修改关闭

二、结构(关键组件与角色)

  • Component(组件接口):抽象接口,定义对外能力。例如 IOrderRepository。
  • ConcreteComponent(具体组件):核心实现,专注业务(如真正访问数据库/下游服务)。
  • Decorator(装饰器基类或模式):持有一个 Component 引用,负责把调用转发到内部组件,同时在前后附加额外行为。
  • ConcreteDecorators(具体装饰器):独立实现各能力,如 LoggingDecorator、CachingDecorator、RetryDecorator。

三、常见使用场景

  • 应用服务/仓储层的横切增强:日志、审计、缓存、重试/退避、熔断/限速、度量埋点、参数验证、鉴权
  • I/O 流(Stream)的功能叠加:压缩、加密、缓冲
  • UI 组件动态装饰(WinForms/WPF)
  • 中间件式管线(与职责链、代理模式有相似点)

四、优缺点

  • 优点:
    • 将横切关注点与核心逻辑彻底分离,组合灵活
    • 更易测试与复用,每个装饰器职责单一
    • 满足开闭原则,减少修改核心类的需求
  • 缺点:
    • 调用链更深,排查问题时需要关注装饰器顺序与交互
    • 过多装饰器可能带来复杂度与轻微性能开销
    • IoC 注册与顺序控制需要规范(否则行为不符合预期)
    • 状态与缓存失效策略需要统一约束(Key 规范、并发、负缓存策略等)

五、示例:订单查询微服务(ASP.NET Minimal API) 目标:

  • IOrderRepository 提供 GetByIdAsync 查询。
  • 通过装饰器分层增加:
    • Logging:记录调用前后与耗时
    • Caching:本地内存缓存(滑动过期),标准化缓存键
    • Retry:重试与指数退避 + 抖动,处理瞬时失败
  • 展示 IoC 注册、装饰器链顺序控制(Logging 外层,Caching 中层,Retry 内层)、Minimal API 接入、控制台日志,以及单元测试验证调用次序与性能收益。

NuGet 依赖(关键包)

  • Microsoft.Extensions.Caching.Memory
  • Scrutor(用于装饰器注册与顺序控制)
  • Microsoft.AspNetCore.App(Minimal API 运行时)
  • xUnit(测试示例,可选)

Program.cs(一个文件即可运行;为简洁演示,核心仓储用内存数据并模拟瞬时故障与延迟)

using System.Collections.Concurrent;
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Scrutor;

// ---------- Domain ----------
public record Order(string Id, string CustomerId, decimal Amount);

// ---------- Abstraction ----------
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(string id, CancellationToken ct = default);
}

// ---------- Core Implementation (ConcreteComponent) ----------
public class OrderRepository : IOrderRepository
{
    private readonly ConcurrentDictionary<string, Order> _store = new();
    private readonly ConcurrentDictionary<string, int> _attemptsPerId = new();
    private readonly ILogger<OrderRepository> _logger;

    public int InvocationCount { get; private set; }

    public OrderRepository(ILogger<OrderRepository> logger)
    {
        _logger = logger;
        // seed data
        _store.TryAdd("o-001", new Order("o-001", "c-100", 199.99m));
        _store.TryAdd("o-002", new Order("o-002", "c-101", 99.5m));
    }

    public async Task<Order?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        InvocationCount++;

        // 模拟下游延迟
        await Task.Delay(TimeSpan.FromMilliseconds(80), ct);

        // 为演示 Retry,一次性制造“首次查询该 id 必失败”的瞬时错误
        int attempt = _attemptsPerId.AddOrUpdate(id, 1, (_, current) => current + 1);
        if (attempt == 1)
        {
            _logger.LogWarning("Simulated transient failure for id={Id} on first attempt.", id);
            throw new TimeoutException("Transient timeout");
        }

        _store.TryGetValue(id, out var order);
        return order;
    }
}

// ---------- Options ----------
public class OrderCacheOptions
{
    public TimeSpan SlidingExpiration { get; set; } = TimeSpan.FromMinutes(5);
    // 可扩展:Size、NegativeCaching 等
}

public class RetryOptions
{
    public int MaxAttempts { get; set; } = 3;
    public TimeSpan BaseDelay { get; set; } = TimeSpan.FromMilliseconds(100);
    public double BackoffMultiplier { get; set; } = 2.0; // 指数退避系数
    public int JitterMs { get; set; } = 50;              // 抖动范围(毫秒)
}

// ---------- Cache Key Spec ----------
public static class CacheKeys
{
    // 接口名:方法:参数 进行规范化,避免冲突
    public static string OrderById(string id) => $"IOrderRepository:GetById:{id}";
}

// ---------- Decorators ----------

// 1) Retry Decorator(最内层,确保只对真正下游调用进行重试)
public class RetryOrderRepository : IOrderRepository
{
    private readonly IOrderRepository _inner;
    private readonly ILogger<RetryOrderRepository> _logger;
    private readonly RetryOptions _options;

    public RetryOrderRepository(IOrderRepository inner,
        ILogger<RetryOrderRepository> logger,
        IOptions<RetryOptions> options)
    {
        _inner = inner;
        _logger = logger;
        _options = options.Value;
    }

    public async Task<Order?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        int attempt = 0;
        Exception? last = null;
        TimeSpan delay = _options.BaseDelay;

        while (attempt < _options.MaxAttempts)
        {
            attempt++;
            try
            {
                _logger.LogDebug("Retry decorator attempt {Attempt} for id={Id}", attempt, id);
                return await _inner.GetByIdAsync(id, ct);
            }
            catch (Exception ex) when (IsTransient(ex))
            {
                last = ex;
                if (attempt >= _options.MaxAttempts) break;

                var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, _options.JitterMs));
                var wait = delay + jitter;
                _logger.LogWarning(ex, "Transient error on attempt {Attempt} for id={Id}. Backing off {Delay} ms...", attempt, id, wait.TotalMilliseconds);
                await Task.Delay(wait, ct);
                delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * _options.BackoffMultiplier);
            }
        }

        _logger.LogError(last, "GetById({Id}) failed after {MaxAttempts} attempts.", id, _options.MaxAttempts);
        throw new Exception($"GetById({id}) failed after {_options.MaxAttempts} attempts.", last);
    }

    private static bool IsTransient(Exception ex) =>
        ex is TimeoutException || ex is HttpRequestException;
}

// 2) Caching Decorator(中层,命中缓存则不触发重试与内层调用)
public class CachingOrderRepository : IOrderRepository
{
    private readonly IOrderRepository _inner;
    private readonly IMemoryCache _cache;
    private readonly OrderCacheOptions _options;
    private readonly ILogger<CachingOrderRepository> _logger;

    public CachingOrderRepository(IOrderRepository inner,
        IMemoryCache cache,
        IOptions<OrderCacheOptions> options,
        ILogger<CachingOrderRepository> logger)
    {
        _inner = inner;
        _cache = cache;
        _options = options.Value;
        _logger = logger;
    }

    public async Task<Order?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        var key = CacheKeys.OrderById(id);

        if (_cache.TryGetValue<Order>(key, out var cached))
        {
            _logger.LogInformation("Cache hit: {Key}", key);
            return cached;
        }

        var result = await _inner.GetByIdAsync(id, ct);
        if (result != null)
        {
            // 滑动过期:每次访问会刷新过期时间
            _cache.Set(key, result, new MemoryCacheEntryOptions
            {
                SlidingExpiration = _options.SlidingExpiration
            });
            _logger.LogInformation("Cache miss: {Key}. Added with sliding expiration={ExpirationSeconds}s",
                key, _options.SlidingExpiration.TotalSeconds);
        }

        return result;
    }
}

// 3) Logging Decorator(最外层,统一记录调用前后与耗时)
public class LoggingOrderRepository : IOrderRepository
{
    private readonly IOrderRepository _inner;
    private readonly ILogger<LoggingOrderRepository> _logger;

    public LoggingOrderRepository(IOrderRepository inner, ILogger<LoggingOrderRepository> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task<Order?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("-> GetById({Id})", id);
        try
        {
            var result = await _inner.GetByIdAsync(id, ct);
            _logger.LogInformation("<- GetById({Id}) took {Elapsed} ms, found={Found}",
                id, sw.ElapsedMilliseconds, result != null);
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "<x GetById({Id}) failed after {Elapsed} ms", id, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

// ---------- ASP.NET Minimal API ----------
var builder = WebApplication.CreateBuilder(args);

// Logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();

// Memory cache
builder.Services.AddMemoryCache();

// Options
builder.Services.Configure<OrderCacheOptions>(o => o.SlidingExpiration = TimeSpan.FromSeconds(30)); // demo用短些
builder.Services.Configure<RetryOptions>(o =>
{
    o.MaxAttempts = 3;
    o.BaseDelay = TimeSpan.FromMilliseconds(100);
    o.BackoffMultiplier = 2.0;
    o.JitterMs = 50;
});

// 注册核心仓储为可供测试访问的单例,同时将 IOrderRepository 初始绑定到它
builder.Services.AddSingleton<OrderRepository>();
builder.Services.AddTransient<IOrderRepository>(sp => sp.GetRequiredService<OrderRepository>());

// 使用 Scrutor 控制装饰器链顺序:Core -> Retry -> Caching -> Logging(outermost)
builder.Services.Decorate<IOrderRepository, RetryOrderRepository>();
builder.Services.Decorate<IOrderRepository, CachingOrderRepository>();
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();

var app = builder.Build();

// Minimal API
app.MapGet("/orders/{id}", async (string id, IOrderRepository repo, CancellationToken ct) =>
{
    var order = await repo.GetByIdAsync(id, ct);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

app.MapGet("/debug/invocations", (OrderRepository core) => new { core.InvocationCount });

app.Run();

装饰器链顺序说明

  • Logging(最外层):总能记录一次调用的入口、出口、耗时,无论后续是否命中缓存或产生重试;也可看到缓存命中导致的极短耗时。
  • Caching(中层):命中缓存时直接返回,避免触发内层重试与核心查询;缓存未命中时才向内继续。
  • Retry(最内层):只对真实下游调用执行重试与退避,不影响缓存命中的快速返回。

六、调用与日志(示例)

  • 第一次 GET /orders/o-001:
    • Logging 记录开始
    • Caching 报 cache miss
    • Retry 第一次尝试触发瞬时异常,退避后第二次成功
    • Caching 将结果写入缓存(滑动过期 30s)
    • Logging 记录结束与耗时(相对较长)
  • 第二次 GET /orders/o-001(30s 内):
    • Logging 记录开始
    • Caching 命中,直接返回(不触发 Retry 与 Core)
    • Logging 记录结束与耗时(明显变短)

七、单元测试(xUnit 示例) 下面的测试构建同样的 DI 容器,验证调用次序(通过核心仓储的 InvocationCount)与性能收益(第二次调用更快)。实际项目中也可以用 InMemoryLoggerProvider 捕获日志顺序关键字进行断言。

using System.Diagnostics;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Scrutor;
using Xunit;

public class OrderRepositoryDecoratorTests
{
    private ServiceProvider BuildProvider()
    {
        var services = new ServiceCollection();
        services.AddLogging(b => b.AddDebug().AddConsole());
        services.AddMemoryCache();

        services.Configure<OrderCacheOptions>(o => o.SlidingExpiration = TimeSpan.FromSeconds(30));
        services.Configure<RetryOptions>(o =>
        {
            o.MaxAttempts = 3;
            o.BaseDelay = TimeSpan.FromMilliseconds(20); // 测试更快
            o.BackoffMultiplier = 2.0;
            o.JitterMs = 10;
        });

        services.AddSingleton<OrderRepository>();
        services.AddTransient<IOrderRepository>(sp => sp.GetRequiredService<OrderRepository>());

        services.Decorate<IOrderRepository, RetryOrderRepository>();
        services.Decorate<IOrderRepository, CachingOrderRepository>();
        services.Decorate<IOrderRepository, LoggingOrderRepository>();

        return services.BuildServiceProvider();
    }

    [Fact]
    public async Task Second_call_should_hit_cache_and_reduce_latency()
    {
        var sp = BuildProvider();
        var repo = sp.GetRequiredService<IOrderRepository>();
        var core = sp.GetRequiredService<OrderRepository>();

        var sw1 = Stopwatch.StartNew();
        var o1 = await repo.GetByIdAsync("o-001");
        sw1.Stop();

        var sw2 = Stopwatch.StartNew();
        var o2 = await repo.GetByIdAsync("o-001");
        sw2.Stop();

        Assert.NotNull(o1);
        Assert.NotNull(o2);
        Assert.Equal(o1, o2);

        // 首次会重试(Core 至少调用一次),第二次命中缓存不再调用 Core
        Assert.Equal(1, core.InvocationCount);

        // 第二次调用应显著更快(缓存返回)
        Assert.True(sw2.ElapsedMilliseconds < sw1.ElapsedMilliseconds);
    }
}

八、设计要点与实践建议

  • 缓存键规范:接口名 + 方法 + 参数;确保不冲突且可定位来源。复杂查询可做序列化/哈希。
  • 缓存策略:
    • 滑动过期适合热点查询;也可视场景改成绝对过期或多级缓存(Memory + Redis)
    • 更新场景需定义失效策略:写入/变更时发布事件清除相关键;可在命令端做失效
    • 避免缓存 null 造成误导(示例中仅缓存非 null)
  • 重试策略:
    • 仅对幂等读操作启用,避免对写操作造成副作用
    • 区分瞬时错误与永久错误;对 HTTP/网络超时等重试,对 404/验证失败不重试
    • 指数退避 + 抖动,避免击穿
  • 日志:
    • 外层统一记录入口/出口与耗时;缓存与重试各自记录命中/尝试,可通过 LogScope 关联请求 ID
  • IoC 顺序控制:
    • 明确装修顺序是行为正确与性能的关键,建议通过 Scrutor 的 Decorate 清晰声明
  • 测试:
    • 单元测试分别验证:缓存命中时核心不调用、重试次数符合预期、链路耗时与日志顺序
    • 基准测试可用 BenchmarkDotNet 验证真实收益

通过以上装饰器架构,你可以在不污染核心仓储代码的前提下,灵活地为查询能力叠加日志、缓存与重试,并在生产系统中以配置驱动各策略参数,保证可维护性与可观测性。

示例详情

该提示词已被收录:
“程序员必备:提升开发效率的专业AI提示词合集”
让 AI 成为你的第二双手,从代码生成到测试文档全部搞定,节省 80% 开发时间
√ 立即可用 · 零学习成本
√ 参数化批量生成
√ 专业提示词工程师打磨

📖 如何使用

30秒出活:复制 → 粘贴 → 搞定
与其花几十分钟和AI聊天、试错,不如直接复制这些经过千人验证的模板,修改几个 {{变量}} 就能立刻获得专业级输出。省下来的时间,足够你轻松享受两杯咖啡!
加载中...
💬 不会填参数?让 AI 反过来问你
不确定变量该填什么?一键转为对话模式,AI 会像资深顾问一样逐步引导你,问几个问题就能自动生成完美匹配你需求的定制结果。零门槛,开口就行。
转为对话模式
🚀 告别复制粘贴,Chat 里直接调用
无需切换,输入 / 唤醒 8000+ 专家级提示词。 插件将全站提示词库深度集成于 Chat 输入框。基于当前对话语境,系统智能推荐最契合的 Prompt 并自动完成参数化,让海量资源触手可及,从此彻底告别"手动搬运"。
即将推出
🔌 接口一调,提示词自己会进化
手动跑一次还行,跑一百次呢?通过 API 接口动态注入变量,接入批量评价引擎,让程序自动迭代出更高质量的提示词方案。Prompt 会自己进化,你只管收结果。
发布 API
🤖 一键变成你的专属 Agent 应用
不想每次都配参数?把这条提示词直接发布成独立 Agent,内嵌图片生成、参数优化等工具,分享链接就能用。给团队或客户一个"开箱即用"的完整方案。
创建 Agent

✅ 特性总结

系统化解析设计模式,让您轻松理解核心原理和用途,快速掌握复杂的架构设计思路。
支持多种编程语言应用,每次可定制语言环境实例,更贴近开发者实际需求。
一键生成清晰简洁的代码示例,降低学习门槛,轻松将理论转化为实践。
深入分析设计模式的组件与角色结构,帮助明确开发中的职责分工与协作逻辑。
涵盖常见应用场景,结合实际案例展示,帮助用户对号入座,找到最佳实践方式。
全面讲解优缺点,自动生成权衡建议,辅助开发者在不同项目中灵活选择实现。
模板化内容生成,高效进行知识迁移,适用于培训、文档撰写与技术知识传播。
针对用户输入的需求和行业背景,生成定制化的设计模式解读,适配场景多样性。
自动优化内容结构,逻辑清晰流畅,帮助用户快速学习,无需额外整理或加工。

🎯 解决的问题

帮助用户深入理解并掌握指定设计模式的用途、结构及实现方法,通过清晰的讲解和实例代码提升技术技能,从而解决实际开发中的架构问题,并在项目实施中充分运用到设计模式的核心思想。

🕒 版本历史

当前版本
v2.1 2024-01-15
优化输出结构,增强情节连贯性
  • ✨ 新增章节节奏控制参数
  • 🔧 优化人物关系描述逻辑
  • 📝 改进主题深化引导语
  • 🎯 增强情节转折点设计
v2.0 2023-12-20
重构提示词架构,提升生成质量
  • 🚀 全新的提示词结构设计
  • 📊 增加输出格式化选项
  • 💡 优化角色塑造引导
v1.5 2023-11-10
修复已知问题,提升稳定性
  • 🐛 修复长文本处理bug
  • ⚡ 提升响应速度
v1.0 2023-10-01
首次发布
  • 🎉 初始版本上线
COMING SOON
版本历史追踪,即将启航
记录每一次提示词的进化与升级,敬请期待。

💬 用户评价

4.8
⭐⭐⭐⭐⭐
基于 28 条评价
5星
85%
4星
12%
3星
3%
👤
电商运营 - 张先生
⭐⭐⭐⭐⭐ 2025-01-15
双十一用这个提示词生成了20多张海报,效果非常好!点击率提升了35%,节省了大量设计时间。参数调整很灵活,能快速适配不同节日。
效果好 节省时间
👤
品牌设计师 - 李女士
⭐⭐⭐⭐⭐ 2025-01-10
作为设计师,这个提示词帮我快速生成创意方向,大大提升了工作效率。生成的海报氛围感很强,稍作调整就能直接使用。
创意好 专业
COMING SOON
用户评价与反馈系统,即将上线
倾听真实反馈,在这里留下您的使用心得,敬请期待。
加载中...