Clean架构下Repository与DataSource实现选型及命名困惑
我正在当前项目中实现Clean Architecture,在Repository、DataSource相关术语及实现上存在困惑。我希望使用内存仓库测试useCases,聚焦业务逻辑,但对相关概念及实现方式存疑。
我定义了基础DataSource接口如下:
export interface DataSource<T> { create(request: any): Promise<T> // more methods here but removed for simplicity }
还定义了PatientRepository接口:
export interface PatientRepository { createPatient(patient: Patient): Promise<void> // more methods here but removed for simplicity }
生产环境将使用Sequelize,单元测试则用本地实体数组。我纠结于PatientRepository的实现方案:
方案1
创建实现PatientRepository的PatientSequelizeRepository,继承实现DataSource契约的抽象SequelizeRepository;同时创建PatientInMemoryRepository,继承抽象InMemoryRepository。测试时用PatientInMemoryRepository,生产用PatientSequelizeRepository。
方案2
创建一个实现PatientRepository的PatientRepositoryImpl,通过构造函数注入DataSource,同时实现通用的InMemoryDataSource和SequelizeDataSource。默认注入SequelizeDataSource,也可替换其他数据源:
export class PatientRepositoryImpl implements PatientRepository { constructor(dataSource: DataSource<Patient>) { } async createPatient(patient: Patient) { return this.dataSource.create(patient) } } export const patientRepository = new PatientRepositoryImpl(new SequelizeDataSource())
我对命名存疑:是否应为Patient创建专属的PatientSequelizeDataSource和PatientInMemoryDataSource?哪种方案更优,是否有其他方案?
方案对比与选型
核心依据Clean Architecture的依赖倒置原则:高层业务逻辑(UseCase)只依赖抽象(Repository接口),底层数据实现依赖高层抽象,而非反过来。
方案2更贴合架构设计
方案2的优势非常明显:
PatientRepositoryImpl仅依赖抽象接口,完全不耦合具体持久化技术,符合依赖倒置- 测试时只需替换为
InMemoryDataSource<Patient>,就能彻底隔离外部数据库,专注测试业务逻辑 - 后续切换ORM(如TypeORM)或存储方式时,只需新增对应
DataSource<T>实现,无需修改Repository和上层业务代码
方案1的问题:
- 每个实体都要编写两套Repository实现,重复代码多,维护成本高
- Repository直接继承DataSource实现,混淆了职责边界——Repository负责业务数据编排,DataSource才是具体数据读写的组件
命名建议
不需要为Patient创建专属的PatientSequelizeDataSource和PatientInMemoryDataSource:
- 泛型
DataSource<T>已经明确了处理的实体类型,SequelizeDataSource<Patient>语义清晰且简洁 - 后续新增其他实体(如Doctor)时,可直接复用
SequelizeDataSource<Doctor>,避免重复造轮子 - 若某个实体需要特殊的数据源逻辑(比如Patient有复杂的关联查询),再单独创建专属数据源即可,符合**YAGNI(You Ain't Gonna Need It)**原则
其他优化方案
如果项目中有多个Repository,可以进一步抽象通用Repository实现,减少重复代码:
// 通用基础Repository实现 export abstract class BaseRepositoryImpl<T> implements BaseRepository<T> { constructor(protected dataSource: DataSource<T>) {} async create(entity: T): Promise<T> { return this.dataSource.create(entity); } // 其他通用CRUD方法... } // Patient专属Repository继承通用实现,扩展业务方法 export class PatientRepositoryImpl extends BaseRepositoryImpl<Patient> implements PatientRepository { async getPatientWithMedicalRecords(id: string): Promise<Patient> { // 实现Patient专属的业务查询逻辑 } }
这种方式既保留了通用CRUD的复用性,又能让每个Repository专注于自身业务需求。
内容的提问来源于stack exchange,提问作者z0mbieKale

