Loading...
Loading...
Compare original and translation side by side
resources/incremental-adoption.mddomain-driven-designresources/| Resource | Load when... |
|---|---|
| Need a full feature traced through every layer with tests and file map |
| Writing tests, creating fakes, setting up |
| Reads need to JOIN across aggregates, separating read/write paths |
| Placing auth, logging, transactions, or error formatting |
| Introducing hex arch into an existing codebase |
../REFERENCES.mdresources/incremental-adoption.mddomain-driven-designresources/| 资源 | 加载时机... |
|---|---|
| 需要完整功能的全链路追踪,包含测试和文件映射 |
| 编写测试、创建模拟实现、设置 |
| 查询需要跨聚合根进行JOIN操作,需分离读写路径 |
| 处理认证、日志、事务或错误格式化 |
| 在现有代码库中引入六边形架构 |
../REFERENCES.md Driving (left) Driven (right)
┌──────────────────┐ ┌──────────────────┐
│ Route handlers │ │ Repositories │
│ CLI commands │──────┐┌────│ API clients │
│ Event listeners │ ││ │ Email services │
└──────────────────┘ ││ └──────────────────┘
call into ──────► ┌────┐ ◄────── implement
│ │
│ DO │
│ MA │
│ IN │
│ │
call into ──────► └────┘ ◄────── implement
┌──────────────────┐ ││ ┌──────────────────┐
│ Cron triggers │──────┘└────│ File storage │
│ Message queues │ │ Payment gateway │
└──────────────────┘ └──────────────────┘ Driving (left) Driven (right)
┌──────────────────┐ ┌──────────────────┐
│ Route handlers │ │ Repositories │
│ CLI commands │──────┐┌────│ API clients │
│ Event listeners │ ││ │ Email services │
└──────────────────┘ ││ └──────────────────┘
call into ──────► ┌────┐ ◄────── implement
│ │
│ DO │
│ MA │
│ IN │
│ │
call into ──────► └────┘ ◄────── implement
┌──────────────────┐ ││ ┌──────────────────┐
│ Cron triggers │──────┘└────│ File storage │
│ Message queues │ │ Payment gateway │
└──────────────────┘ └──────────────────┘interface// Driven port — defined in domain, implemented by adapters
interface UserRepository {
readonly findById: (id: UserId) => Promise<User | undefined>;
readonly save: (user: User) => Promise<void>;
}
// Driven port — defined in domain, implemented by adapters
interface PaymentGateway {
readonly charge: (amount: Money, paymentInfo: PaymentInfo) => Promise<ChargeResult>;
}
// Driven port — event publishing (outbound to message brokers)
interface OrderEventPublisher {
readonly publish: (event: OrderEvent) => Promise<void>;
}UserRepositoryDatabasePortSqlRowHttpResponseinterface// Driven port — defined in domain, implemented by adapters
interface UserRepository {
readonly findById: (id: UserId) => Promise<User | undefined>;
readonly save: (user: User) => Promise<void>;
}
// Driven port — defined in domain, implemented by adapters
interface PaymentGateway {
readonly charge: (amount: Money, paymentInfo: PaymentInfo) => Promise<ChargeResult>;
}
// Driven port — event publishing (outbound to message brokers)
interface OrderEventPublisher {
readonly publish: (event: OrderEvent) => Promise<void>;
}UserRepositoryDatabasePortSqlRowHttpResponse// Driven adapter — implements the repository port using Drizzle/D1
const createDrizzleUserRepository = (db: D1Database): UserRepository => ({
findById: async (id) => {
const row = await db.select().from(users).where(eq(users.id, id)).get();
return row ? toUser(row) : undefined;
},
save: async (user) => {
await db.insert(users).values(toRow(user)).onConflictDoUpdate({ ... });
},
});
// Driven adapter — implements the same port for tests
const createFakeUserRepository = (initial: readonly User[] = []): UserRepository => {
const store = new Map(initial.map(u => [u.id, u]));
return {
findById: async (id) => store.get(id),
save: async (user) => { store.set(user.id, user); },
};
};// Driven adapter: translate expected infrastructure errors into domain-specific errors
const createDrizzleUserRepository = (db: Database): UserRepository => ({
save: async (user) => {
try {
await db.insert(users).values(toRow(user)).onConflictDoUpdate({ ... });
} catch (e) {
if (isUniqueConstraintError(e)) throw new UserAlreadyExistsError(user.id);
throw e; // unexpected errors (connection lost, disk full) propagate
}
},
});// Driven adapter — implements the repository port using Drizzle/D1
const createDrizzleUserRepository = (db: D1Database): UserRepository => ({
findById: async (id) => {
const row = await db.select().from(users).where(eq(users.id, id)).get();
return row ? toUser(row) : undefined;
},
save: async (user) => {
await db.insert(users).values(toRow(user)).onConflictDoUpdate({ ... });
},
});
// Driven adapter — implements the same port for tests
const createFakeUserRepository = (initial: readonly User[] = []): UserRepository => {
const store = new Map(initial.map(u => [u.id, u]));
return {
findById: async (id) => store.get(id),
save: async (user) => { store.set(user.id, user); },
};
};// Driven adapter: translate expected infrastructure errors into domain-specific errors
const createDrizzleUserRepository = (db: Database): UserRepository => ({
save: async (user) => {
try {
await db.insert(users).values(toRow(user)).onConflictDoUpdate({ ... });
} catch (e) {
if (isUniqueConstraintError(e)) throw new UserAlreadyExistsError(user.id);
throw e; // unexpected errors (connection lost, disk full) propagate
}
},
});| Operation | Pattern | Example |
|---|---|---|
| Write | Repository (one aggregate) | |
| Read (single aggregate) | Repository | |
| Read (cross-aggregate, display) | Query function (JOINs freely) | |
db/queries/// Query function — JOINs across aggregates for display
// Lives in db/queries/, NOT in domain/
const getParticipantEventView = async (db: Database, eventId: string) => {
return db.select({ ... })
.from(events)
.innerJoin(occasions, ...)
.leftJoin(giftClaims, ...)
.where(eq(events.id, eventId))
.all();
};resources/cqrs-lite.md| 操作 | 模式 | 示例 |
|---|---|---|
| 写 | 仓储(单一聚合根) | |
| 读(单一聚合根) | 仓储 | |
| 读(跨聚合根,用于展示) | 查询函数(自由JOIN) | |
db/queries/// Query function — JOINs across aggregates for display
// Lives in db/queries/, NOT in domain/
const getParticipantEventView = async (db: Database, eventId: string) => {
return db.select({ ... })
.from(events)
.innerJoin(occasions, ...)
.leftJoin(giftClaims, ...)
.where(eq(events.id, eventId))
.all();
};resources/cqrs-lite.md// WRONG — creates dependencies internally (untestable, tightly coupled)
const createOrder = async (order: NewOrder) => {
const repo = new DrizzleOrderRepo(getDb()); // hardcoded
const gateway = new StripeGateway(process.env.KEY); // hardcoded
// ...
};
// RIGHT — dependencies as parameters (testable, swappable)
const createOrder = async (
repo: OrderRepository,
gateway: PaymentGateway,
order: NewOrder,
): Promise<OrderResult> => {
const charge = await gateway.charge(order.total, order.payment);
if (!charge.success) return { success: false, reason: charge.error };
const saved = await repo.save({ ...order, chargeId: charge.id });
return { success: true, order: saved };
};// Route handler = composition root + driving adapter
export async function POST(request: Request) {
const { env } = getCloudflareContext();
const db = createDb(env.DB);
// Wire adapters
const repo = createDrizzleOrderRepository(db);
const gateway = createStripeGateway(env.STRIPE_KEY);
// Call use case
const body = CreateOrderSchema.parse(await request.json());
const result = await createOrder(repo, gateway, body);
return NextResponse.json(result);
}// Queue consumer = driving adapter (same structure as route handler)
const handlePledgeMessage = async (message: SQSMessage, env: Env) => {
const db = createDb(env.DB);
const occasionRepo = createDrizzleOccasionRepository(db);
const contributorRepo = createDrizzleContributorRepository(db);
const dto = PledgeSchema.parse(JSON.parse(message.body));
await handlePledge(occasionRepo, contributorRepo, dto);
};createOrderplaceOrderhandlePledgecreateOrderUseCasePlaceOrderHandler// WRONG — creates dependencies internally (untestable, tightly coupled)
const createOrder = async (order: NewOrder) => {
const repo = new DrizzleOrderRepo(getDb()); // hardcoded
const gateway = new StripeGateway(process.env.KEY); // hardcoded
// ...
};
// RIGHT — dependencies as parameters (testable, swappable)
const createOrder = async (
repo: OrderRepository,
gateway: PaymentGateway,
order: NewOrder,
): Promise<OrderResult> => {
const charge = await gateway.charge(order.total, order.payment);
if (!charge.success) return { success: false, reason: charge.error };
const saved = await repo.save({ ...order, chargeId: charge.id });
return { success: true, order: saved };
};// Route handler = composition root + driving adapter
export async function POST(request: Request) {
const { env } = getCloudflareContext();
const db = createDb(env.DB);
// Wire adapters
const repo = createDrizzleOrderRepository(db);
const gateway = createStripeGateway(env.STRIPE_KEY);
// Call use case
const body = CreateOrderSchema.parse(await request.json());
const result = await createOrder(repo, gateway, body);
return NextResponse.json(result);
}// Queue consumer = driving adapter (same structure as route handler)
const handlePledgeMessage = async (message: SQSMessage, env: Env) => {
const db = createDb(env.DB);
const occasionRepo = createDrizzleOccasionRepository(db);
const contributorRepo = createDrizzleContributorRepository(db);
const dto = PledgeSchema.parse(JSON.parse(message.body));
await handlePledge(occasionRepo, contributorRepo, dto);
};createOrderplaceOrderhandlePledgecreateOrderUseCasePlaceOrderHandler| Layer | Location | Contains | Tests |
|---|---|---|---|
| Domain | | Business logic (pure functions), types, port interfaces, use cases (orchestration) | Unit + use case tests (fakes) |
| Adapters (driven) | | Repository impls, API clients, query functions | Integration tests (real DB/MSW) |
| Adapters (driving) | | Route handlers, event listeners | E2E tests (Playwright) |
| Wiring | | Adapter factories, config, composition | Covered by E2E |
| 层 | 位置 | 包含内容 | 测试 |
|---|---|---|---|
| 领域层 | | 业务逻辑(纯函数)、类型、端口接口、用例(编排) | 单元测试 + 用例测试(模拟实现) |
| 适配器(从动) | | 仓储实现、API客户端、查询函数 | 集成测试(真实数据库/MSW) |
| 适配器(驱动) | | 路由处理器、事件监听器 | 端到端测试(Playwright) |
| 组装层 | | 适配器工厂、配置、组合逻辑 | 由端到端测试覆盖 |
| Priority | Boundary | What it proves |
|---|---|---|
| Primary | Use case (faked driven ports) | Feature works end-to-end within the hexagon |
| Complement | Domain pure functions | Complex business rules in isolation |
| Secondary | Driven adapters (real DB/MSW) | Adapter translates correctly |
| Verification | E2E (full stack) | User experience works |
resources/testing-hex-arch.mdresources/worked-example.md| 优先级 | 边界 | 验证内容 |
|---|---|---|
| 首要 | 用例(替换为模拟从动端口) | 六边形内部的功能端到端正常工作 |
| 补充 | 领域纯函数 | 复杂业务规则的隔离验证 |
| 次要 | 从动适配器(真实数据库/MSW) | 适配器转换逻辑正确 |
| 验证 | 端到端(全栈) | 用户体验正常 |
resources/testing-hex-arch.mdresources/worked-example.md| Concern | Where | Why |
|---|---|---|
| Authentication (who are you?) | Driving adapter | Protocol-specific (JWT, session, API key) |
| Authorization (are you allowed?) | Domain | Business rule about permissions |
| Logging | Adapters (both sides) | Side effect, not business logic |
| Transactions | Adapter / composition root | Infrastructure concern, domain unaware |
| Error formatting | Driving adapter | Translates domain results to HTTP/gRPC |
resources/cross-cutting-concerns.md| 关注点 | 位置 | 原因 |
|---|---|---|
| 认证(你是谁?) | 驱动适配器 | 协议相关(JWT、会话、API密钥) |
| 授权(你是否被允许?) | 领域层 | 关于权限的业务规则 |
| 日志 | 适配器(两侧) | 副作用,非业务逻辑 |
| 事务 | 适配器 / 组合根 | 基础设施关注点,领域层不知情 |
| 错误格式化 | 驱动适配器 | 将领域结果转换为HTTP/gRPC格式 |
resources/cross-cutting-concerns.md// ❌ Domain imports Drizzle
import { eq } from 'drizzle-orm';
export const findActiveUsers = async (db) => db.select()...
// ✅ Domain defines the contract; adapter implements it
interface UserRepository {
readonly findActive: () => Promise<readonly User[]>;
}// ❌ Domain imports Drizzle
import { eq } from 'drizzle-orm';
export const findActiveUsers = async (db) => db.select()...
// ✅ Domain defines the contract; adapter implements it
interface UserRepository {
readonly findActive: () => Promise<readonly User[]>;
}// ❌ Business rule in route handler
export async function POST(request: Request) {
const order = await orderRepo.findById(id);
if (order.total > 1000) { await requireManagerApproval(order); } // business rule!
...
}
// ✅ Business rule in domain
const placeOrder = (order: Order): PlaceOrderResult => {
if (order.total > 1000) return { success: false, reason: 'requires-approval' };
...
};// ❌ Business rule in route handler
export async function POST(request: Request) {
const order = await orderRepo.findById(id);
if (order.total > 1000) { await requireManagerApproval(order); } // business rule!
...
}
// ✅ Business rule in domain
const placeOrder = (order: Order): PlaceOrderResult => {
if (order.total > 1000) return { success: false, reason: 'requires-approval' };
...
};// ❌ Route handler hits DB directly
export async function GET(request: Request) {
const users = await db.select().from(users).where(eq(users.active, true));
...
}
// ✅ Route handler calls use case, which uses a port
const result = await getActiveUsers(userRepo);// ❌ Route handler hits DB directly
export async function GET(request: Request) {
const users = await db.select().from(users).where(eq(users.active, true));
...
}
// ✅ Route handler calls use case, which uses a port
const result = await getActiveUsers(userRepo);// ❌ Technology leaks into port
interface UserRepository {
readonly findBySqlQuery: (sql: string) => Promise<User[]>;
readonly getFromRedisCache: (key: string) => Promise<User>;
}
// ✅ Business language
interface UserRepository {
readonly findActive: () => Promise<readonly User[]>;
readonly findById: (id: UserId) => Promise<User | undefined>;
}// ❌ Technology leaks into port
interface UserRepository {
readonly findBySqlQuery: (sql: string) => Promise<User[]>;
readonly getFromRedisCache: (key: string) => Promise<User>;
}
// ✅ Business language
interface UserRepository {
readonly findActive: () => Promise<readonly User[]>;
readonly findById: (id: UserId) => Promise<User | undefined>;
}