You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 02:22:44