英语 · 法语
@ddd-ts-kit /方面
日志记录、度量、重试、超时、断路器和缓存都不是业务逻辑,所以
它们从来没有写在里面。每一个都是一个方面:一个实现,围绕一个
适配器从外部,方式 @ddd-ts-kit/tracing 在它周围放上跨度。包装类
没有进口,并且 domain/ 和 application/ 永远不要看到这个包裹。
npm i @ddd-ts-kit/aspects
它在应用程序中的位置
调用通过端口离开用例。回答这个问题的是绑定到该端口的适配器, 由方面包裹的组成根放在它周围。用例本身也被包装起来了, 但是通过总线:它们的中间件记录和测量每个命令和查询。
与其将策略写入每个适配器,不如单独编写适配器并放置策略 :
import { compose, retry, timeout } from '@ddd-ts-kit/aspects';
interface Quote {
readonly pair: string;
readonly rate: number;
}
declare function fetchQuote(pair: string): Promise<Quote>;
export abstract class ExchangeRates {
abstract quoteOf(pair: string): Promise<Quote>;
}
// Instead of: the retry loop, its waits and its limit written inside the adapter,
// once more in every adapter, and invisible from the composition root.
export class TangledRates implements ExchangeRates {
async quoteOf(pair: string): Promise<Quote> {
for (let attempt = 1; ; attempt += 1) {
try {
return await fetchQuote(pair);
} catch (error) {
if (attempt === 3) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
}
}
}
}
// Write: the adapter does its one job...
export class HttpRates implements ExchangeRates {
async quoteOf(pair: string): Promise<Quote> {
return fetchQuote(pair);
}
}
// ...and the composition root puts the policy around it: the same object, still an HttpRates.
export const rates: ExchangeRates = compose(retry({ attempts: 3 }), timeout(2000))(new HttpRates(), { kind: 'external' });
三种形式,一种实现
同样的政策,有三种写法。它们都运行相同的代码。
1. 在组合根,在一个绑定上(首选)
import { retry, timeout } from '@ddd-ts-kit/aspects';
import { defineContextModule } from '@ddd-ts-kit/di';
interface Payment {
readonly orderId: string;
readonly amountCents: number;
}
interface Receipt {
readonly chargeId: string;
}
declare function post(url: string, body: Payment, options: { apiKey: string; signal?: AbortSignal | undefined }): Promise<Receipt>;
declare const config: { readonly stripe: { readonly url: string; readonly key: string } };
// contexts/billing/application/ports/payment-gateway.port.ts
export abstract class PaymentGateway {
abstract charge(payment: Payment, signal?: AbortSignal): Promise<Receipt>;
}
// contexts/billing/application/ports/payment-settings.port.ts
export abstract class PaymentSettings {
abstract readonly baseUrl: string;
abstract readonly apiKey: string;
}
// contexts/billing/infrastructure/external/stripe.gateway.ts
export class StripeGateway implements PaymentGateway {
constructor(private readonly settings: PaymentSettings) {}
async charge(payment: Payment, signal?: AbortSignal): Promise<Receipt> {
return post(`${this.settings.baseUrl}/charges`, payment, { apiKey: this.settings.apiKey, signal });
}
}
// contexts/billing/billing.module.ts
export const billingModule = defineContextModule({
name: 'billing',
register(builder) {
builder.bind(PaymentSettings).toValue({ baseUrl: config.stripe.url, apiKey: config.stripe.key });
builder
.bind(PaymentGateway)
.toClass(StripeGateway, [PaymentSettings])
.as('external')
.with(retry({ attempts: 3 }), timeout(2000, { signal: true }));
},
});
端口是一个抽象类,类是被绑定的对象。 StripeGateway 没有意义
从这个包里,不知道的政策。在声明的样式中,
生成的项目中,没有模块文件,并且在声明中声明了相同的策略
适配器的: @Adapter(PaymentGateway, { kind: 'external', aspects: [retry({ attempts: 3 }), timeout(2000)] }). 没有容器,方面就是一个函数:
retry({ attempts: 3 })(new StripeGateway(settings), { kind: 'external' }).
2. 在每个绑定的组合根
// main.ts
import { Cache, Clock, Logger, Metrics } from '@ddd-ts-kit/kernel';
import { kernelModule, type ContextModule } from '@ddd-ts-kit/di';
import { createApplication } from '@ddd-ts-kit/di/tsyringe';
import { StdoutLogger, aspects, forKinds, installAspectRuntime, logged, retry, timed, timeout, when, type Aspect } from '@ddd-ts-kit/aspects';
declare const tracing: Aspect; // below
declare const platformModule: ContextModule;
declare const billingModule: ContextModule;
const app = createApplication({
modules: [kernelModule({ logger: new StdoutLogger() }), platformModule, billingModule],
decorate: aspects(
tracing,
when(forKinds('repository', 'gateway', 'external'), logged(), timed()),
when(forKinds('external'), retry({ attempts: 3 }), timeout(2000)),
),
});
installAspectRuntime({
logger: app.resolve(Logger),
metrics: app.resolve(Metrics),
cache: app.resolve(Cache),
clock: app.resolve(Clock),
});
tracing 跟踪包的钩子是否写成了一个方面:
import type { Aspect } from '@ddd-ts-kit/aspects';
import { traceDependency } from '@ddd-ts-kit/tracing';
export const tracing: Aspect = (instance, info) =>
info?.kind === undefined ? instance : traceDependency(instance, { kind: info.kind, ...(info.context === undefined ? {} : { context: info.context }) });
申请网址: kernelModule({ instrumentation: traceApplication() }),这就是
CLI生成,而不是写这个 tracing 方面:内核模块跟踪绑定
本身。它张成的空间在 decorate Hook添加和添加了a的各个方面
具有约束力(见“命令”)。生成的项目不使用 decorate 勾起任何一个:
平台模块将默认策略放置在离开流程的适配器周围
builder.around(['external', 'gateway'], () => when(forKinds('external', 'gateway'), retry(...), timeout(...))),
并且一个适配器声明了它自己的策略 ownPolicy(...) 的
platform/resilience/,然后默认策略将其保留。
3. 作为适配器上的装饰器
// contexts/billing/infrastructure/external/stripe.gateway.ts
import { Retry, Timeout } from '@ddd-ts-kit/aspects/decorators';
interface Payment {
readonly orderId: string;
readonly amountCents: number;
}
interface Receipt {
readonly chargeId: string;
}
declare function post(url: string, body: Payment, options: { apiKey: string; signal?: AbortSignal | undefined }): Promise<Receipt>;
abstract class PaymentGateway {
abstract charge(payment: Payment, signal?: AbortSignal): Promise<Receipt>;
}
abstract class PaymentSettings {
abstract readonly baseUrl: string;
abstract readonly apiKey: string;
}
export class StripeGateway implements PaymentGateway {
constructor(private readonly settings: PaymentSettings) {}
@Retry({ attempts: 3 })
@Timeout(2000, { signal: true })
async charge(payment: Payment, signal?: AbortSignal): Promise<Receipt> {
return post(`${this.settings.baseUrl}/charges`, payment, { apiKey: this.settings.apiKey, signal });
}
}
@Retry, @Timeout, @Cached, @Logged, @Timed, @CircuitBreaker, @Bulkhead,
decoratorOf(adviceFactory) 项目的一个方面。一个方法上有几个嵌套
读:第一个写在最外面。类中不带日志记录器、指标或缓存
构造函数:当调用时,装饰器从方面运行时读取它们。
形成
- 选择合成根 (表格1及2)。适配器不受任何导入的影响 整个应用程序的策略都是在一个地方读取的,它在不触及类的情况下改变。
- 一个装饰师是可以接受的 在适配器上
infrastructure/或者platform/的政策 是适配器的一部分:记录其自己的重试和 超时规则。不要在两种形式中应用相同的方面:它会运行两次。 - 从来没有在
domain/或者application/,以任何形式;layer/domain-purity和layer/application-inward禁止进口。一个用例得到它的横切行为 来自总线中间件,如下所示。
方面
| 方面 | 它是做什么的 | 默认策略 | 从来没有记录 |
|---|---|---|---|
logged({ level?, fields?, logger?, clock? }) | 条目 <Class>.<method> started (调试) succeeded (level,默认为info)或 failed | 一个 DomainError 失败在 warn错误的;错误的 error;字段 class, method, kind, context, durationMs, errorType, errorCode | 参数、结果、错误消息 |
timed({ name?, metrics?, clock? }) | 柱状图 <name>_duration_ms,反 <name>_total | name 是 dependency_call;标签 class, method, kind, context, outcome, error_code | 参数、结果、错误消息、标识符 |
retry({ attempts, backoff?, retryOn?, sleep? }) | 失败后再次调用 | 从来没有。 DomainError、开路或全舱壁(isTransient); 100毫秒加倍到30秒与全抖动 | 没有:它没有记录 |
timeout(ms, { signal?, scheduler? }) | 拒绝与 TimeoutError (TIMEOUT) | 底层调用将继续运行,除非 signal: true | 该消息仅命名类和方法 |
circuitBreaker({ failureThreshold, resetAfterMs, halfOpenMax?, scope?, isFailure?, onStateChange?, clock? }) | 拒绝与 CircuitOpenError (CIRCUIT_OPEN),然后让尝试电话接通 | 每个实例和方法一个电路;一个 DomainError 不是失败;一次试用电话 | onStateChange 获取类、方法、上下文和两个状态 |
cached({ key, ttlMs, cache?, allowKinds? }) | 答案是重复阅读 Cache 港口 | 拒绝类 repository 和 use-case;键为的呼叫 undefined 未被缓存;失败不会被缓存;并发的相同调用共享一个调用;缓存失败是失败 | 该键由项目根据参数构建,并仅进入缓存 |
bulkhead({ maxConcurrent, maxQueued?, scope? }) | 边界召唤奔跑,拒绝超越 BulkheadFullError (BULKHEAD_FULL) | 没有队列;每个实例和方法一个隔间 | 没有什么 |
隐私。 该规则是跟踪规则之一:条目、标签或由此引发的错误
包包含来自代码(类、方法、上下文、类型)、持续时间、类的名称
一个简短的错误 code. 绝不是参数、结果或捕获错误的消息:
他们引用数据。错误被原封不动地重新抛出。
timeout 和取消。 与 signal: true 这种方法是现成的 AbortSignal 作为它的
最后声明的参数,当时间到时终止。声明 signal?: AbortSignal 最后的,
没有默认值,并将其传递给底层调用。呼叫者经过的信号是
保留:任何一方中止。不带参数的方法不接收任何参数。
retry 和抖动。 backoff: { baseMs, factor, maxMs, jitter }. jitter 是一个函数
(delayMs, attempt) => delayMs: fullJitter() 默认情况下, fullJitter(seededRandom) 或者
noJitter 对于一个确定的时间表。 sleep 完全取代了等待。之后的等待
失败的尝试 n 是 baseMs x factor^(n - 1),上限为 maxMs然后颤抖起来。
delayBeforeRetry(n, backoff) 计算它。
circuitBreaker 和指标。 onStateChange: circuitStateGauge() 将状态发布为
该指数 circuit_breaker_state0个关闭,1个半开,2个开。
生意上的拒绝 DomainError)证明依赖项回答:它重置计数
就像一个成功者。一个提供者的策略,它可能会消失一段时间,按照下一个的顺序
部分解释:
import { circuitBreaker, circuitStateGauge, compose, retry, timeout } from '@ddd-ts-kit/aspects';
export const paymentProviderPolicy = compose(
// Outermost: an open circuit ends the retries at once (retry never retries CIRCUIT_OPEN).
retry({ attempts: 3, backoff: { baseMs: 200 } }),
// Five consecutive failures open it for 30 s; the gauge circuit_breaker_state follows it.
circuitBreaker({ failureThreshold: 5, resetAfterMs: 30_000, onStateChange: circuitStateGauge() }),
// Innermost: each attempt has its own 2 s.
timeout(2000),
);
cached 用于读取网关和外部适配器。 从缓存中提供的聚合
在存储库的后面隐藏着它是陈旧的,下一个命令将决定旧的状态。
因此,包装存储库或用例需要显式的 allowKinds: ['repository'],
这是一个记录的决定。
项目编写自己的方面 defineAspect((joinPoint) => async ({ args, proceed }) => ...).
外部函数对每个包装实例的每个方法运行一次,并可能应答 undefined 来
别管那个方法;内部的在每次调用时运行,并且 proceed() 调用里面的东西:
import { defineAspect, type Aspect } from '@ddd-ts-kit/aspects';
import type { Clock, Logger } from '@ddd-ts-kit/kernel';
/** Warns when a call lasts longer than the threshold: names from the code and a duration, never the arguments. */
export function warnWhenSlow(thresholdMs: number, logger: Logger, clock: Clock): Aspect {
return defineAspect((joinPoint) => async ({ proceed }) => {
const startedAt = clock.now().getTime();
try {
return await proceed();
} finally {
const durationMs = clock.now().getTime() - startedAt;
if (durationMs > thresholdMs) {
logger.warn(`${joinPoint.className}.${joinPoint.method} slow`, { durationMs, thresholdMs });
}
}
});
}
选择包装的内容
when(selector, ...aspects) 将方面应用于选择器选择的方法:
| 选择器 | 选择 |
|---|---|
forKinds('repository', 'external') | 绑定的实例 .as(kind) |
forContext('billing') | 模块的实例 |
forClasses('Http*') | 按类名的实例 |
forMethods('find*', 'get*') | 按名称命名的方法 |
allOf(...), anyOf(...), not(...) | 组合 |
import { allOf, cached, forKinds, forMethods, when } from '@ddd-ts-kit/aspects';
// The one place arguments are read: build the key from an identifier, never from personal data.
// `undefined` leaves the call uncached. The cache key also holds the class and the method.
const key = (_method: string, [id]: readonly unknown[]): string | undefined => (typeof id === 'string' ? id : undefined);
export const cacheReads = when(allOf(forKinds('gateway', 'external'), forMethods('find*')), cached({ key, ttlMs: 60_000 }));
订单
compose(a, b), .with(a, b), aspects(a, b) 堆叠的装饰器都是一样的:
第一个是最外层的. 应用程序在一个实例周围嵌套,最外层首先是
decorate 钩子,将各个方面模块放在一起 builder.around(kinds, ...) 在
模块的顺序(张成的空间) kernelModule({ instrumentation }) 那就先来吧
对于存储库,首先是域事件的移交,然后是平台模块事件的移交;
最后是绑定本身的方面(.with(...),或者 aspects 声明)。
类方法上的装饰器在所有方法中。
顺序变化含义:
retry然后timeout每次尝试都有自己的时间限制。反过来,一个极限 涵盖了所有的尝试和等待。timed和logged外retry:每一次通话都有一个测量值, 包括重试。里面,一次一次。cached外retry和circuitBreaker一击不花一分钱,也不会被对方拒绝 开路。circuitBreaker内部retry:每次失败的尝试计数,开路结束 重试一次,因为retry不重试CircuitOpenError.
import { compose, retry, timeout } from '@ddd-ts-kit/aspects';
// Each attempt has its own 2 s: the call may last 3 x 2 s, plus the waits between attempts.
export const limitPerAttempt = compose(retry({ attempts: 3 }), timeout(2000));
// One 2 s limit for the whole call: attempts and waits together.
export const limitForTheCall = compose(timeout(2000), retry({ attempts: 3 }));
适合大多数适配器的顺序,最外层优先:
tracing, logged, timed, cached, retry, circuitBreaker, bulkhead, timeout, the adapter
对于内核模块的插装,跟踪范围是内核模块所在的位置
所说: logged 和 timed 给予 decorate 钩子在跨度之外
给 builder.around 或者绑定在里面。
存储库还有一个包装器 kernelModule() 之间的 decorate 虎克和鱼
它的绑定方面:它保存的域事件的移交,其中的单元
在提交之前,在其结束时进行工作分派。绑定的各个方面
(.with(retry(...)))都在里面,重试保存一次会宣布它的事件。在一个
这些事件的处理程序的工作单元在保存期间不再运行,因此不会重试保存
永远覆盖它。在any之外,save在解析之前进行调度 retry 给予
decorate 在处理程序失败后,Hook将再次运行保存,而不会留下任何事件
调度。将存储库的重试放在其绑定上。
方面运行时
logged, timed 和 cached 需要一个日志,指标和缓存。他们从他们的
选项,或者从组合根安装一次的运行时中获取
installAspectRuntime({ logger, metrics, cache, clock, scheduler }). 时读取运行时
调用永远不会在类被装饰或实例被包装时进行,因此
语句 main.ts 没关系。失踪的合作者加薪 AspectRuntimeError,
这就是它的名字。时钟和调度器默认为系统时钟。 Scheduler 是港口吗?
为“稍后运行此包”(schedule(work, delayMs)类的抽象类
内核端口: systemScheduler 是默认值, ManualScheduler 的 ./testing 一个
规格的。
用例:总线中间件
处理程序不会一个一个地包装;公共汽车是:
import { loggingMiddleware, metricsMiddleware } from '@ddd-ts-kit/aspects';
import { InMemoryCommandBus, InMemoryQueryBus, type Logger, type Metrics } from '@ddd-ts-kit/kernel';
declare const logger: Logger;
declare const metrics: Metrics;
export const commands = new InMemoryCommandBus().use(loggingMiddleware(logger)).use(metricsMiddleware(metrics));
export const queries = new InMemoryQueryBus().use(loggingMiddleware(logger)).use(metricsMiddleware(metrics));
相同的项和级数 logged 和 timed (前缀 use_case),以消息命名
类,而不是它的内容; command ChargeOrder started,然后 succeeded 或者 failed,与
字段 message 和 messageKind,以及系列 use_case_duration_ms 和 use_case_total
贴上标签 message 和 kind. 没有中间件打开一个 ExecutionScope:命令
不携带关联id,并且当路由或
消费者运行,在任何总线之前。在边界处打开。
StdoutLogger
内核的一个小适配器 Logger 端口写入JSON行:
import { StdoutLogger } from '@ddd-ts-kit/aspects';
import { logFieldsOf, type ExecutionScope } from '@ddd-ts-kit/kernel';
import { traceLogFields } from '@ddd-ts-kit/tracing';
declare const scope: ExecutionScope;
// Without tracing: the correlation id, actor and tenant of the current execution context.
export const logger = new StdoutLogger({
level: 'info',
base: { service: 'shop-api' },
fields: () => {
const context = scope.current();
return context === undefined ? {} : logFieldsOf(context);
},
});
// With @ddd-ts-kit/tracing: the same fields, plus trace_id and span_id of the active span.
export const tracedLogger = new StdoutLogger({ base: { service: 'shop-api' }, fields: () => traceLogFields(scope) });
logger.info('payment accepted', { orderId: 'o-42' });
// {"time":"2026-03-01T10:00:00.000Z","level":"info","message":"payment accepted","service":"shop-api","correlationId":"...","origin":"http","actorId":"...","orderId":"o-42"}
fields 在每个条目处读取。此包既不依赖于跟踪包也不依赖于
执行范围:项目选择每个条目所携带的内容。当同样的场来自
几个地方,后者获胜: base,然后是田野 child(...),然后 fields(),然后
日志调用的字段。没有可以覆盖 time, level 或者 message.
它实现了整个端口,由 loggerContract 像pino和Winston适配器:
- 错误。 一个
Error作为字段传递(logger.error('payment failed', { error }))是 写成describeError的内核描述了它:type,code对于一个DomainError,message,stack,然后cause同样的道理。scrubMessages: true写[message removed]对于消息的每一个错误,都不是一个错误DomainError打开它 当原木离开你的手。logged和loggingMiddleware不担心:他们 永远不要传递错误errorType和errorCode. isLevelEnabled(level)回答阈值(level、默认info)。logged和loggingMiddleware先问它:门槛以下没有场是建的fields的函数logged未被调用。flush()解析一次标准输出,占用到目前为止编写的每一行,永远不会 拒绝;之后记录器仍然可用。app.stop()的函数最后调用它。stream将行发送到标准输出以外的另一个流,并且flush()等待 为了那条小溪。一个write项目的每一条线反而收到并赢得stream.- 日志调用永远不会抛出。 无法读取或序列化的字段(getter会抛出,
(圆形物体),在条目或子条目上,给出没有它们的条目,标记
logError: "unserializable fields".
它将凭证隐藏为 @ddd-ts-kit/pino 和 @ddd-ts-kit/winston 做:每一个的价值
关键的 CREDENTIAL_KEYS 内核的,不管它是什么情况(isCredentialKey),是写的
[redacted] 在入口的顶部和下面一层(headers.authorization),在田野里
系的,系子的,系的 base 和 fields. 没有更深的,也不是在列表中。的对象
传递的对象永远不会写入。
包装保证了什么
- 实例被包装在:同一个对象,
instanceof,this和private成员 完好无损。被冻结的实例由相同类型的代理作为前端,其方法运行于 这个代理是this. - 应用于实例的方面通知从外部到达实例的调用。
private被编译器擦除,并且名称没有说明方法是否为公共(下划线 (这是一种习惯,而不是规则),所以公共API不能被可靠地挑选出来:每个异步 方法可通过名称访问private async方法和包含异步对象的字段 包含的函数被封装。区分这些叫声的是它们的来源。而一个 建议调用在实例上运行,该实例对其自身进行调用,对公共或 一个private方法,是该调用的内部,并且不会被同一方面再次建议:retry({ attempts: 3 })围绕一个公共方法,经过一个私有步骤得到三个 尝试,而不是九次logged()和timed()录一个电话。守卫是按人看守的 实例和每个方面,并遵循调用的异步流 (AsyncLocalStorage):同时从外部发出的两个呼叫都是建议的,以此类推 是被建议调用结束后留下的任何东西(计时器、侦听器)。 - 它的后续内容是:从协作者返回到实例的调用,而
建议调用正在运行也是调用的内部;
cached为来自的来电者服务 在外面,不是一个方法读另一个通过this;bulkhead和circuitBreaker数 一个电话。方法装饰器不关心:它建议它所使用的唯一方法at 每一个电话,打进来的this包括在内。 - 跟踪的目的不同:
traceDependency的@ddd-ts-kit/tracing为的子跨度打开 实例对自身的调用。span描述了一个步骤,在这个步骤上有一个通知。 - 冻结的实例保持冻结:它的字段,
private包括在内的,不能分配。状态 它必须改变它持有的对象中的生命值(a)Map(数组)。 - 对一个实例应用相同的方面两次是无操作的。
- getter、符号、生成器、同步方法和生命周期钩子(
LIFECYCLE_HOOKS) 独处。 - 声明的方法
async从其第一次调用开始被建议。一个只返回A的方法 Promise从它的第二个调用中被告知:declare adapter methodsasync. 装饰方法 总是被建议。
测试
包装器将其协作器作为选项,因此规范传递内核的副本。为
装饰类和读取运行时的方面, @ddd-ts-kit/aspects/testing 给了
createAspectFakes() (内存记录器,度量和缓存,固定时钟和
ManualScheduler 在同一时钟)和 withAspectRuntime(runtime, work):
import { expect, it } from 'vitest';
import { Logged, Timeout } from '@ddd-ts-kit/aspects/decorators';
import { createAspectFakes, withAspectRuntime } from '@ddd-ts-kit/aspects/testing';
const fetchRate = (_pair: string): Promise<number> => new Promise(() => undefined); // a provider that never answers
class SlowRates {
@Logged()
@Timeout(2000)
async rateOf(pair: string): Promise<number> {
return fetchRate(pair);
}
}
it('gives up after two seconds, and logs the failure without the pair', async () => {
const fakes = createAspectFakes(); // logger, metrics, cache, clock, scheduler
const rates = new SlowRates();
const pending = withAspectRuntime(fakes, () => rates.rateOf('EUR-USD'));
fakes.scheduler.advance(2000); // time moves when the test says so
await expect(pending).rejects.toMatchObject({ code: 'TIMEOUT' });
expect(fakes.logger.entries.map((entry) => entry.message)).toEqual(['SlowRates.rateOf started', 'SlowRates.rateOf failed']);
});
withAspectRuntime 保留工作及其等待的所有内容,而并发测试则不保留
查看彼此的运行时间。与 createTestApplication 在di包中,应用程序是
运行时: withAspectRuntime(app, () => ...),然后断言 app.logger.entries 和 app.metrics.
装饰器模式
的装饰者 ./decorators 在TypeScript的两种装饰器模式下都可以工作,使用相同的
源线。
| 堆栈 | tsconfig.json |
|---|---|
| class-validator, NestJS, TypeORM实体,任何带有遗留装饰符的库 | "experimentalDecorators": true |
| 别的 | 离开 experimentalDecorators unset:标准(TC39)装饰者不需要标志 |
- 如何检测模式。 通过呼叫:标准的是
(method, context)与一个 Context对象,遗留的那个(prototype, name, descriptor).decoratorOf读起来 已接收,因此一个函数同时服务于两个方面,而一个方面有一个实现。 - 有什么不同。 在标准模式中,每个实例的方法在
实例被构造。遗留模式没有初始化项,因此该方法被替换为
每个实例的原型和通知都是在第一次调用时构建的。状态(电路)
一个隔间,一个飞行)是每个实例和每个方法,几个装饰器
当他们在两者中阅读时,他们都不需要
emitDecoratorMetadata. - 编译器。 TypeScript 5、6和原生的TypeScript 7都可以编译这两种模式。Node没有 运行decorator语法,以两种方式编译代码。
- 轻快地。 它自己降低了遗留装饰器:一个带有
experimentalDecorators需要什么。它不降低标准(规格进口).ts使用的文件@Retry失败,有语法错误):adddecorators()的@ddd-ts-kit/testing/vitest来 Vitest配置。在组合根目录下包装的文件在这两种模式下都不需要。 - 用声明。
@Adapter(...)在课堂上@Retry(...)关于其方法 撰写,与aspects方法的装饰器周围的声明:@Adapter(PaymentGateway, { kind: 'external' }). 一个静态方法,A 由符号和硬私有(#)方法被拒绝。
这个 ./decorators 条目是单独的:包中的其他内容都不会导入它。
限制
- 状态(电路、隔间、单次飞行)存在于每个封装实例的进程中。一个 瞬态绑定在每个实例中获得一个新状态。
retry按原样重复调用:仅对可以安全重复的操作重复。timeout没有signal不会停止基础调用。- 包装调用总是回应一个承诺:不要包装方法返回A的对象 可链查询生成器。
when选择的方面的方法defineAspect,compose或者when;一个普通 函数本身用作方面过滤器。