关键要点
- 防腐层隔离外部依赖:通过在边界层转换外部 API 格式,防止第三方服务的变更影响核心业务逻辑
- 领域驱动设计指导:防腐层是实现限界上下文(Bounded Context)之间通信的关键模式
- 适配器模式实现:每个外部 API 对应一个适配器,负责将外部数据模型转换为内部领域模型
- 版本管理策略:防腐层允许同时支持多个 API 版本,平滑迁移
- 测试隔离:通过模拟外部 API,可以在不依赖真实服务的情况下测试核心业务
在健康科技领域,我们经常需要与多个外部服务集成——电子健康记录(EHR)系统、实验室信息系统(LIS)、可穿戴设备 API、保险理赔平台等。每个系统都有独特的数据模型、API 设计理念和变更节奏。
防腐层(Anti-Corruption Layer,简称 ACL)是一种架构模式,专门用于解决这种异构系统集成问题。本教程将深入探讨如何在健康科技项目中实现这一模式。
前置条件:
- 熟悉 TypeScript 和面向对象设计
- 了解领域驱动设计(DDD)基本概念
- 有 RESTful API 开发经验
- 理解适配器模式和依赖注入
理解防腐层模式
什么是防腐层?
防腐层是位于你的核心系统和外部系统之间的隔离层,负责:
- 协议转换:将外部 API 的协议、格式转换为你内部系统的标准
- 模型映射:将外部数据模型映射到你的领域模型
- 错误处理:统一处理各种外部 API 的错误和异常
- 缓存与重试:提供统一的缓存策略和重试机制
为什么需要防腐层?
code
没有防腐层:
核心系统 ←→ 多种异构 API
(任一 API 变更都会影响核心系统)
有防腐层:
核心系统 ←→ 防腐层 ←→ 多种异构 API
(ACL 隔离变更,保护核心系统)
Code collapsed
在健康科技领域,这种模式特别重要:
- 频繁的 API 变更:Epic、Cerner 等大型 EHR 供应商经常更新 API
- 复杂的认证流程:OAuth2、JWT、API Key 等多种认证方式
- 数据格式差异:FHIR R4、HL7 v2、自定义 JSON 等多种格式
- 严格的合规要求:HIPAA、GDPR 等法规要求精确的数据处理
项目架构设计
整体架构
code
┌─────────────────────────────────────────────────┐
│ 应用层 │
│ (用例、工作流、业务逻辑编排) │
└─────────────────┬───────────────────────────────┘
│
┌─────────────────▼───────────────────────────────┐
│ 防腐层 (ACL) │
│ ┌────────────┬────────────┬────────────────┐ │
│ │ 适配器工厂 │ 模型转换器 │ 错误处理器 │ │
│ └────────────┴────────────┴────────────────┘ │
└─────────────────┬───────────────────────────────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼─────┐ ┌──▼──────┐ ┌──▼──────┐
│ Epic API │ │ FHIR API│ │ Lab API │
└───────────┘ └─────────┘ └─────────┘
Code collapsed
实现防腐层
步骤 1:定义内部领域模型
首先,定义你自己的领域模型,不受外部 API 影响:
code
// domain/models/Patient.ts
export interface Patient {
id: string;
demographic: PatientDemographic;
contacts: ContactInfo[];
medicalRecordNumber: string;
sourceSystem: 'EPIC' | 'CERNER' | 'FHIR' | 'CUSTOM';
lastSyncAt: Date;
}
export interface PatientDemographic {
firstName: string;
lastName: string;
dateOfBirth: Date;
gender: 'MALE' | 'FEMALE' | 'OTHER' | 'UNKNOWN';
preferredLanguage: string;
}
export interface ContactInfo {
type: 'EMAIL' | 'PHONE' | 'MAILING_ADDRESS';
value: string;
isPrimary: boolean;
}
export interface VitalSigns {
patientId: string;
recordedAt: Date;
systolicBP?: number;
diastolicBP?: number;
heartRate?: number;
temperature?: number;
respiratoryRate?: number;
spo2?: number;
source: string;
}
Code collapsed
步骤 2:定义防腐层接口
code
// application/interfaces/IPatientRepository.ts
import { Patient, VitalSigns } from '@/domain/models/Patient';
export interface IPatientRepository {
findById(id: string): Promise<Patient | null>;
findByMedicalRecordNumber(mrn: string): Promise<Patient | null>;
search(criteria: PatientSearchCriteria): Promise<Patient[]>;
getVitalSigns(patientId: string, dateRange: DateRange): Promise<VitalSigns[]>;
}
export interface PatientSearchCriteria {
name?: string;
dateOfBirth?: Date;
mrn?: string;
}
export interface DateRange {
start: Date;
end: Date;
}
Code collapsed
步骤 3:实现外部 API 适配器
Epic EHR 适配器
code
// infrastructure/adapters/epic/EpicPatientAdapter.ts
import { Patient, VitalSigns } from '@/domain/models/Patient';
import { EpicApiClient } from './EpicApiClient';
import { IMapper } from '../../interfaces/IMapper';
export class EpicPatientAdapter {
constructor(
private apiClient: EpicApiClient,
private mapper: IMapper<EpicPatientResponse, Patient>
) {}
async getPatient(patientId: string): Promise<Patient> {
const response = await this.apiClient.getPatient(patientId);
return this.mapper.map(response);
}
async getVitalSigns(patientId: string, startDate: Date, endDate: Date): Promise<VitalSigns[]> {
const response = await this.apiClient.getVitals(patientId, startDate, endDate);
return response.vitals.map(v => this.mapper.mapVital(v));
}
async searchPatients(criteria: any): Promise<Patient[]> {
const response = await this.apiClient.search(criteria);
return response.patients.map(p => this.mapper.map(p));
}
}
// Epic API 响应模型(外部)
interface EpicPatientResponse {
id: string;
firstName: string;
lastName: string;
birthDate: string; // ISO 8601 字符串
sex: 'M' | 'F' | 'U';
homePhone?: string;
email?: string;
MRN: string;
}
Code collapsed
FHIR 适配器
code
// infrastructure/adapters/fhir/FhirPatientAdapter.ts
import { Patient } from '@/domain/models/Patient';
import { FhirClient } from './FhirClient';
export class FhirPatientAdapter {
constructor(private fhirClient: FhirClient) {}
async getPatient(id: string): Promise<Patient> {
const resource = await this.fhirClient.readResource('Patient', id);
return this.convertFhirToPatient(resource);
}
private convertFhirToPatient(resource: fhir.Patient): Patient {
const nameEntry = resource.name?.[0];
const telecom = resource.telecom || [];
return {
id: resource.id!,
demographic: {
firstName: nameEntry?.given?.join(' ') || '',
lastName: nameEntry?.family || '',
dateOfBirth: new Date(resource.birthDate!),
gender: this.mapGender(resource.gender),
preferredLanguage: resource.communication?.[0]?.language?.coding?.[0]?.code || 'en',
},
contacts: telecom.map(t => ({
type: this.mapTelecomType(t.system!),
value: t.value || '',
isPrimary: t.use === 'home',
})),
medicalRecordNumber: resource.identifier?.find(i => i.use === 'usual')?.value || '',
sourceSystem: 'FHIR',
lastSyncAt: new Date(),
};
}
private mapGender(gender: fhir.Patient['gender']): Patient['demographic']['gender'] {
const mapping = {
male: 'MALE',
female: 'FEMALE',
other: 'OTHER',
unknown: 'UNKNOWN',
};
return mapping[gender!] || 'UNKNOWN';
}
private mapTelecomType(system: string): Patient['contacts'][0]['type'] {
const mapping = {
email: 'EMAIL',
phone: 'PHONE',
postal: 'MAILING_ADDRESS',
};
return mapping[system as keyof typeof mapping] || 'PHONE';
}
}
Code collapsed
步骤 4:实现模型映射器
code
// infrastructure/mappers/EpicPatientMapper.ts
import { IMapper } from '@/application/interfaces/IMapper';
import { Patient } from '@/domain/models/Patient';
import { EpicPatientResponse } from '../adapters/epic/EpicPatientAdapter';
export class EpicPatientMapper implements IMapper<EpicPatientResponse, Patient> {
map(source: EpicPatientResponse): Patient {
return {
id: source.id,
demographic: {
firstName: source.firstName,
lastName: source.lastName,
dateOfBirth: new Date(source.birthDate),
gender: this.mapSex(source.sex),
preferredLanguage: 'en', // Epic API 可能不提供,使用默认值
},
contacts: this.buildContacts(source),
medicalRecordNumber: source.MRN,
sourceSystem: 'EPIC',
lastSyncAt: new Date(),
};
}
mapVital(source: EpicVitalResponse): VitalSigns {
return {
patientId: source.patientId,
recordedAt: new Date(source.recordedTime),
systolicBP: source.systolicBP,
diastolicBP: source.diastolicBP,
heartRate: source.heartRate,
temperature: source.temperature,
source: 'EPIC',
};
}
private mapSex(sex: 'M' | 'F' | 'U'): Patient['demographic']['gender'] {
const mapping = { M: 'MALE', F: 'FEMALE', U: 'UNKNOWN' };
return mapping[sex];
}
private buildContacts(source: EpicPatientResponse): Patient['contacts'] {
const contacts: Patient['contacts'] = [];
if (source.email) {
contacts.push({ type: 'EMAIL', value: source.email, isPrimary: true });
}
if (source.homePhone) {
contacts.push({ type: 'PHONE', value: source.homePhone, isPrimary: !source.email });
}
return contacts;
}
}
// 基础映射器接口
export interface IMapper<TSource, TDestination> {
map(source: TSource): TDestination;
}
Code collapsed
步骤 5:实现适配器工厂
code
// application/factories/PatientAdapterFactory.ts
import { PatientRepository } from '../repositories/PatientRepository';
import { EpicPatientAdapter } from '../../infrastructure/adapters/epic/EpicPatientAdapter';
import { FhirPatientAdapter } from '../../infrastructure/adapters/fhir/FhirPatientAdapter';
import { EpicPatientMapper } from '../../infrastructure/mappers/EpicPatientMapper';
import { FhirPatientMapper } from '../../infrastructure/mappers/FhirPatientMapper';
import { EpicApiClient } from '../../infrastructure/external/EpicApiClient';
import { FhirClient } from '../../infrastructure/external/FhirClient';
export type SourceSystem = 'EPIC' | 'CERNER' | 'FHIR';
export class PatientAdapterFactory {
private epicAdapter?: EpicPatientAdapter;
private fhirAdapter?: FhirPatientAdapter;
constructor(
private epicClient: EpicApiClient,
private fhirClient: FhirClient
) {}
getAdapter(system: SourceSystem) {
switch (system) {
case 'EPIC':
if (!this.epicAdapter) {
this.epicAdapter = new EpicPatientAdapter(
this.epicClient,
new EpicPatientMapper()
);
}
return this.epicAdapter;
case 'FHIR':
if (!this.fhirAdapter) {
this.fhirAdapter = new FhirPatientAdapter(
this.fhirClient,
new FhirPatientMapper()
);
}
return this.fhirAdapter;
default:
throw new Error(`Unsupported source system: ${system}`);
}
}
}
Code collapsed
步骤 6:实现复合仓储
code
// application/repositories/CompositePatientRepository.ts
import { IPatientRepository, PatientSearchCriteria, DateRange } from '../interfaces/IPatientRepository';
import { Patient, VitalSigns } from '@/domain/models/Patient';
import { PatientAdapterFactory, SourceSystem } from '../factories/PatientAdapterFactory';
export class CompositePatientRepository implements IPatientRepository {
constructor(
private factory: PatientAdapterFactory,
private defaultSystem: SourceSystem = 'EPIC'
) {}
async findById(id: string): Promise<Patient | null> {
// 尝试所有系统,返回第一个找到的结果
const systems: SourceSystem[] = ['EPIC', 'FHIR', 'CERNER'];
for (const system of systems) {
try {
const adapter = this.factory.getAdapter(system);
const patient = await adapter.getPatient(id);
return patient;
} catch (error) {
console.warn(`${system} lookup failed for patient ${id}:`, error);
continue;
}
}
return null;
}
async findByMedicalRecordNumber(mrn: string): Promise<Patient | null> {
const adapter = this.factory.getAdapter(this.defaultSystem);
return adapter.searchPatients({ mrn }).then(pats => pats[0] || null);
}
async search(criteria: PatientSearchCriteria): Promise<Patient[]> {
const results: Patient[] = [];
// 并行搜索所有系统
const searchPromises = ['EPIC', 'FHIR'].map(system =>
this.factory.getAdapter(system as SourceSystem)
.searchPatients(criteria)
.catch(error => {
console.warn(`Search failed on ${system}:`, error);
return [] as Patient[];
})
);
const searchResults = await Promise.all(searchPromises);
results.push(...searchResults.flat());
// 去重(按 medical record number)
const unique = new Map<string, Patient>();
results.forEach(p => unique.set(p.medicalRecordNumber, p));
return Array.from(unique.values());
}
async getVitalSigns(patientId: string, dateRange: DateRange): Promise<VitalSigns[]> {
// 聚合多个系统的生命体征数据
const adapter = this.factory.getAdapter(this.defaultSystem);
return adapter.getVitalSigns(patientId, dateRange.start, dateRange.end);
}
}
Code collapsed
处理 API 版本变更
防腐层的最大优势在于它可以隔离外部 API 的变更。这里展示如何处理版本升级:
code
// infrastructure/adapters/epic/v2/EpicPatientAdapterV2.ts
export class EpicPatientAdapterV2 {
// 新版本 API 可能有不同的端点、认证方式或响应格式
async getPatient(patientId: string): Promise<Patient> {
const response = await this.apiClient.get(`/v2/patients/${patientId}`);
// 新版本的响应格式可能不同
return {
id: response.data.patientId, // 字段名从 id 改为 patientId
demographic: {
firstName: response.data.profile.firstName, // 嵌套结构变化
lastName: response.data.profile.lastName,
dateOfBirth: new Date(response.data.dob), // 字段名简化
gender: this.mapGender(response.data.genderIdentity), // 新增选项
preferredLanguage: response.data.preferences.language || 'en',
},
// ... 其他映射
};
}
private mapGender(identity: string): Patient['demographic']['gender'] {
// 新版本支持更多性别选项
const mapping = {
'male': 'MALE',
'female': 'FEMALE',
'non-binary': 'OTHER',
'prefer-not-to-say': 'UNKNOWN',
};
return mapping[identity as keyof typeof mapping] || 'UNKNOWN';
}
}
Code collapsed
更新工厂以支持版本选择:
code
export class PatientAdapterFactory {
constructor(
private epicClient: EpicApiClient,
private epicClientV2: EpicApiClientV2,
private fhirClient: FhirClient,
private versionConfig: { epic: 'v1' | 'v2' } = { epic: 'v1' }
) {}
getAdapter(system: SourceSystem) {
switch (system) {
case 'EPIC':
if (this.versionConfig.epic === 'v2') {
return new EpicPatientAdapterV2(
this.epicClientV2,
new EpicPatientMapperV2()
);
}
return new EpicPatientAdapterV1(
this.epicClient,
new EpicPatientMapperV1()
);
// ...
}
}
// 运行时切换版本(灰度发布)
setEpicVersion(version: 'v1' | 'v2') {
this.versionConfig.epic = version;
}
}
Code collapsed
错误处理与重试策略
防腐层应该统一处理各种错误情况:
code
// application/decorators/ResilientPatientRepository.ts
export class ResilientPatientRepository implements IPatientRepository {
constructor(
private delegate: IPatientRepository,
private logger: Logger
) {}
async findById(id: string): Promise<Patient | null> {
return this.withRetry(
() => this.delegate.findById(id),
{ maxAttempts: 3, delay: 1000 }
);
}
private async withRetry<T>(
operation: () => Promise<T>,
options: { maxAttempts: number; delay: number }
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (this.isRetryable(error)) {
this.logger.warn(`Attempt ${attempt} failed, retrying...`, { error });
if (attempt < options.maxAttempts) {
await this.delay(options.delay * attempt); // 指数退避
continue;
}
}
throw this.mapError(error);
}
}
throw lastError;
}
private isRetryable(error: any): boolean {
// 网络错误、超时、503 等可重试
return (
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT' ||
error.response?.status === 503 ||
error.response?.status === 429
);
}
private mapError(error: any): Error {
// 将各种外部 API 错误转换为内部错误类型
if (error.response?.status === 401) {
return new AuthenticationError('External API authentication failed');
}
if (error.response?.status === 404) {
return new NotFoundError('Patient not found in external system');
}
return new ExternalApiError('Failed to fetch patient data', error);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 自定义错误类型
class ExternalApiError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
this.name = 'ExternalApiError';
}
}
class AuthenticationError extends ExternalApiError {
constructor(message: string) {
super(message);
this.name = 'AuthenticationError';
}
}
class NotFoundError extends ExternalApiError {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
Code collapsed
性能优化:缓存层
code
// application/decorators/CachedPatientRepository.ts
import { LRUCache } from 'lru-cache';
export class CachedPatientRepository implements IPatientRepository {
private cache = new LRUCache<string, Patient>({
max: 500, // 最多缓存500个患者
ttl: 1000 * 60 * 30, // 30分钟过期
});
constructor(private delegate: IPatientRepository) {}
async findById(id: string): Promise<Patient | null> {
const cacheKey = `patient:${id}`;
let patient = this.cache.get(cacheKey);
if (patient) {
return patient;
}
patient = await this.delegate.findById(id);
if (patient) {
this.cache.set(cacheKey, patient);
}
return patient;
}
// 其他方法类似实现...
// 失效方法(当数据更新时调用)
invalidate(patientId: string): void {
this.cache.delete(`patient:${patientId}`);
}
}
Code collapsed
测试防腐层
使用模拟对象测试,不依赖真实 API:
code
// __tests__/repositories/CompositePatientRepository.test.ts
import { describe, it, expect, vi } from 'vitest';
import { CompositePatientRepository } from '@/application/repositories/CompositePatientRepository';
import { PatientAdapterFactory } from '@/application/factories/PatientAdapterFactory';
describe('CompositePatientRepository', () => {
it('should search across multiple systems', async () => {
// 模拟适配器工厂
const mockFactory = {
getAdapter: vi.fn().mockImplementation((system) => ({
searchPatients: vi.fn().mockResolvedValue(
system === 'EPIC'
? [{ id: '1', name: 'John Doe' }]
: []
),
})),
} as any;
const repository = new CompositePatientRepository(mockFactory);
const results = await repository.search({ name: 'John' });
expect(results).toHaveLength(1);
expect(results[0].id).toBe('1');
});
it('should handle partial failures gracefully', async () => {
const mockFactory = {
getAdapter: vi.fn().mockImplementation((system) => ({
searchPatients: system === 'EPIC'
? Promise.reject(new Error('Epic is down'))
: Promise.resolve([{ id: '2', name: 'Jane Doe' }]),
})),
} as any;
const repository = new CompositePatientRepository(mockFactory);
const results = await repository.search({ name: 'Jane' });
// 即使 Epic 失败,仍应返回 FHIR 的结果
expect(results).toHaveLength(1);
expect(results[0].id).toBe('2');
});
});
Code collapsed
部署与监控
健康检查端点
code
// app/api/health/external/route.ts
export async function GET() {
const checks = {
epic: await checkEpicConnection(),
fhir: await checkFhirConnection(),
cerner: await checkCernerConnection(),
};
const allHealthy = Object.values(checks).every(c => c.healthy);
return NextResponse.json({
status: allHealthy ? 'healthy' : 'degraded',
systems: checks,
}, {
status: allHealthy ? 200 : 503,
});
}
async function checkEpicConnection() {
try {
const start = Date.now();
await epicApiClient.getHealth();
return {
healthy: true,
latency: Date.now() - start,
};
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
Code collapsed
总结
防腐层模式为健康科技应用提供了一种优雅的方式来管理复杂的第三方集成。通过本教程,你学会了:
- 设计与外部系统解耦的领域模型
- 使用适配器模式隔离外部 API
- 实现版本管理和平滑升级
- 添加弹性能力(重试、降级、缓存)
- 编写可测试的集成代码
最佳实践总结
- 单一职责:每个适配器只负责一个外部系统
- 依赖注入:使用工厂模式管理适配器生命周期
- 渐进式迁移:支持多版本并行,逐步切换
- 可观测性:记录所有外部 API 调用,便于问题排查
- 明确边界:防腐层不应该包含业务逻辑