TypeScript问卷系统:类型检查时如何不违反开闭原则(OCP)?
问卷系统中Survey与SurveyRepository的OCP合规设计方案
问题核心
当前SurveyRepository的insert方法通过switch判断问题类型处理存储,新增问题类型时必须修改该方法,直接违反开闭原则(OCP)——对扩展开放、对修改关闭。
可行方案
方案1:让Question子类承担自身持久化逻辑(轻量实现)
给基类Question定义抽象持久化方法,子类各自实现专属存储逻辑,SurveyRepository只需调用统一方法,无需关心具体问题类型。
修改Question基类
abstract class Question { private readonly metadata: QuestionMetadata private required: boolean private question: string // 抽象方法:子类实现自身存储逻辑 abstract persist(questionRepo: QuestionStorage): void; }
子类实现
class TextQuestion extends Question { persist(questionRepo: QuestionStorage): void { questionRepo.saveTextQuestion(this); } } class ChoiceQuestion extends Question { private answerOptions: AnswerOptions private isMultiple: boolean persist(questionRepo: QuestionStorage): void { questionRepo.saveChoiceQuestion(this); } }
改造SurveyRepository
class SurveyRepository extends IRepository { private questionStorage: QuestionStorage; constructor(questionStorage: QuestionStorage) { super(); this.questionStorage = questionStorage; } public insert(survey: Survey): number { // 先存储问卷基础信息 const surveyId = this.saveSurveyBaseInfo(survey); // 遍历问题,调用各自的持久化方法 survey.getQuestions().forEach(question => question.persist(this.questionStorage)); return surveyId; } private saveSurveyBaseInfo(survey: Survey): number { // 存储问卷标题、描述等元数据的逻辑 } } // 定义问题存储接口,新增类型时仅需扩展此接口 interface QuestionStorage { saveTextQuestion(question: TextQuestion): void; saveChoiceQuestion(question: ChoiceQuestion): void; }
方案2:使用访问者模式(解耦式实现)
如果不想让Question类与持久化逻辑耦合,可通过访问者模式将存储逻辑封装到独立访问者中,Question类仅负责接受访问。
定义访问者接口
interface QuestionVisitor { visitTextQuestion(question: TextQuestion): void; visitChoiceQuestion(question: ChoiceQuestion): void; // 新增问题类型时,添加对应visit方法即可 }
修改Question基类
abstract class Question { private readonly metadata: QuestionMetadata private required: boolean private question: string abstract accept(visitor: QuestionVisitor): void; }
子类实现accept方法
class TextQuestion extends Question { accept(visitor: QuestionVisitor): void { visitor.visitTextQuestion(this); } } class ChoiceQuestion extends Question { private answerOptions: AnswerOptions private isMultiple: boolean accept(visitor: QuestionVisitor): void { visitor.visitChoiceQuestion(this); } }
实现存储访问者
class QuestionPersistenceVisitor implements QuestionVisitor { private surveyId: number; private dbConn: DbConnection; // 假设的数据库连接实例 constructor(surveyId: number, dbConn: DbConnection) { this.surveyId = surveyId; this.dbConn = dbConn; } visitTextQuestion(question: TextQuestion): void { // 文本问题的数据库存储逻辑 this.dbConn.execute('INSERT INTO text_questions (survey_id, ...) VALUES (?, ...)', [this.surveyId, /* 其他参数 */]); } visitChoiceQuestion(question: ChoiceQuestion): void { // 选择题的数据库存储逻辑 this.dbConn.execute('INSERT INTO choice_questions (survey_id, options, is_multiple) VALUES (?, ?, ?)', [this.surveyId, question.answerOptions, question.isMultiple]); } }
改造SurveyRepository
class SurveyRepository extends IRepository { private dbConn: DbConnection; constructor(dbConn: DbConnection) { super(); this.dbConn = dbConn; } public insert(survey: Survey): number { const surveyId = this.saveSurveyBaseInfo(survey); const persistenceVisitor = new QuestionPersistenceVisitor(surveyId, this.dbConn); survey.getQuestions().forEach(question => question.accept(persistenceVisitor)); return surveyId; } private saveSurveyBaseInfo(survey: Survey): number { // 存储问卷基础信息的逻辑 } }
方案对比
- 方案1:实现简单,适合小型系统,虽有轻度耦合,但完全符合OCP——新增问题类型仅需新增子类并实现
persist方法,无需修改现有代码。 - 方案2:完全解耦业务逻辑与持久化逻辑,适合大型系统,新增问题类型仅需扩展访问者接口与实现,同样满足OCP要求。
排除方案说明
- 为每种问题类型单独建仓库:拆分问卷存储逻辑,导致SurveyRepository失去统一管理的意义,不推荐。
- 基类加类型字段+仓库映射:后续查询、业务逻辑都会依赖类型判断,违反OCP且代码冗余。
- 单个Question类包含所有属性:产生大量空值,新增类型必须修改类,直接违反OCP。
- 子类持有Question实例:本质是组合模式,但Survey类仍需处理不同类型对象,无法解决OCP问题,反而增加复杂度。
内容的提问来源于stack exchange,提问作者Kilian Reichart
相关产品推荐
相关产品推荐

