You need to sign in or sign up before continuing.
跑课重构部分.md 10.6 KB

#逻辑拆解 1.在跑课项目的老师版和学生版模块中,之前的代码中存在有大量的逻辑判断部分,如在学生版的JobHandler.java和老师版的ExerciseManager.java两个类中,存在大量的if-else逻辑判断,在代码重构的初期工作中,将JobHandler与ExerciseManager中的if-esle逻辑判断拆分出来,将每个逻辑判断的条件体封装到类中,并存放在项目的cn.boxfish.onekey.helper文件夹中,为了区分老师模块与学生模块,将老师模块逻辑判断的类存放到teacher文件夹下,将学生模块逻辑判断拆解的类存放到student文件夹下;

//老师模块初步拆解后的代码: for (Map.Entry> entry : map.entrySet()) { String key = entry.getKey().trim(); Table table = entry.getValue(); if (key.equals("图片")) { TPTeachHelper.build(table,key,exercises,logger); } else if (key.equals("文字")) { WZTeachHelper.build(table,key,exercises); } else if (key.equals("释义")) { SYTeachHelper.build(table,key,exercises); } else if (key.equals("阅读")) { YDTeachHelper.build(table,key,exercises); } else if (key.equals("综合学习")) { ZHXXTeachHelper.build(table,key,exercises,isPriExtend,priExtendMap,extendInfo); } else if (key.equals("同义词")) { TYCTeachHelper.build(table,key,exercises); } else if (key.equals("托福口语")) { TFKYTeachHelper.build(table,key,exercises); } else if (key.equals("托福写作")) { TFXZTeachHelper.build(table,key,exercises); } else { logger.debug("key : " + key); } } 2.将逻辑判断移动到类中;在代码的执行中,会判断条件是否成立,然后进入到类中执行条件体,在重构中需要将条件判断移动到条件体中;使用链式模式,在链中传入逻辑判断的条件。当条件成立的时候就会执行该条件体;

//判断条件放到类中,变为链式模式 public class TFKYTeachHelper extends SheetCommand { public TFKYTeachHelper() { setSheetName("托福口语"); }

public void doBuild(Context context) {
    for (Integer r : context.getTable().rowKeySet()) {
        Map<String, String> row = context.getTable().row(r);
            if (StringUtils.isNotBlank(row.get("题目"))
            || StringUtils.isNotBlank(row.get("主题句"))) {
                OralCover oral = new OralCover(row);
                ChecksumUtils.checksum(oral, row);
                KnowledgeUtils.setCode(oral, context.getKey(), row);

                // AuditUtils.auditKey(oral, row);
                context.getExercises().add(oral);

            } else if (StringUtils.isNotBlank(row.get("核心词"))
            || StringUtils.isNotBlank(row.get("简单句"))) {
                OralContext oral = new OralContext(row);
                ChecksumUtils.checksum(oral, row);
                KnowledgeUtils.setCode(oral, context.getKey(), row);
                // AuditUtils.auditKey(oral, row);
                context.getExercises().add(oral);
            }
    }
}

} 在进行逻辑拆分的时候,不同的逻辑拆分需要判断是否需要执行;前期采用的方法是使用标识符判断的方式,在上下文中设置Flag值默认为false,如果需要执行,在进行逻辑拆分的时候,通过判断falg的值来判断是都需要执行这个逻辑模块;在后期的代码重构中,使用chain链的方式来判断该逻辑模块是否需要执行;

3.拆解后,每个逻辑模块上任然有大量的逻辑判断,按照逻辑拆解的思想,每个类按照模块细分还能进行细化拆解;

4.将拆解完成的逻辑分支合并到develop分支;

#按照跑课模板对每个模块进行拆解 1.完成对跑课逻辑的拆解后,拆解后的逻辑中任然存在一个或多个跑课模块的判断,需要对跑课中的逻辑进行二次拆解,将老师版和学生版第一次拆解得到的helper下的所有类再次进行拆解;将老师版的模块拆后得到的类放在cn.boxfish.onekey.helper.teacher.helper文件夹下,将学生版的模块拆后得到的类放在cn.boxfish.onekey.helper.student.helper文件夹下; //学生版口语考试模块进一步拆解 public class OralTestHelper {

private static final String SHEET_NAME = "口语考试";

public static void build(Context context) {
    if (!context.isFlag()) {
        if (context.getKey().equals(SHEET_NAME)) {
            doBuild(context);
            context.setFlag(true);
        }
    }
}
//口语考试
public static void doBuild(Context context) {
    for (Integer row : context.getTable().rowKeySet()) {
        Map<String, String> map = context.getTable().row(row);

        Context oralContext = new Context();
        oralContext.setMap(map);
        oralContext.setContext(context);

        SpHasShortDialogueHelper.build(oralContext);
        SpHasReadAloudHelper.build(oralContext);
        SpHasCompositionHelper.build(oralContext);
        SpHasCoverHelper.build(oralContext);
        SpHasNot.build(oralContext);
    }
}

}

2.在对模块进行二次拆解的时候,需要判断每个模块是否需要进入执行,在初期的时候,代码的重构需要使用设置标识符flag,初始值为false;对于互斥的逻辑判断,在执行该模块的代码之前先判断是否flag的值是否为false,如果为false就执行该模块的逻辑,执行完成后设置flag为true;在后来的代码重构中,拆解的时候让该类继承ModelCommand类,使用无参构造的方式调用ModelCommand中的方法,使用闭包的方式将上下文对象传入进去,得到的结果是该类的执行条件的结果值; //对学生模块的“APP学生版”中模块拆解,其中的一个模块 public class APPIsCoverNew extends ModelCommand { public APPIsCoverNew() { setCallback(context -> CoverNew.isCoverNew(context.getMap())); }

public void doBuild(Context context) {
    CoverNew obj = new CoverNew(context.getIndex(), context.getMap());
    ChecksumUtils.checksum(obj, context.getMap());
    KnowledgeUtils.setCode(obj, context.getSheetName(), context.getMap());
    context.getIndex().addCourses(obj);
}

}

3.在完成二次拆解后,第一次拆解中各个逻辑模块中就会出现大量的重读代码,将相同的代码抽取到sheetCommand中,将链作为SheetCommand的属性,提取到sheetCommand中; //拆解完成APP学生版模块 public class StudentHelper extends SheetCommand {

public StudentHelper() {
    setSheetName("APP学生版");
    addCommand(new APPHasGrammar());
    addCommand(new APPIsGrammarSum());
    addCommand(new APPIsBigCover());
    addCommand(new APPIsAppreciation());
    addCommand(new APPIsEquals());
    addCommand(new APPIsExpress());
    addCommand(new APPIsOpenQuestion());
    addCommand(new APPIsNotBlank());
    addCommand(new APPIsSceneVideo());
    addCommand(new APPIsNotBlankVideo());
    addCommand(new APPIsNotBlankAudio());
    addCommand(new APPIsNotBlankWZ());
    addCommand(new APPIsNotBlankTPO());
    addCommand(new APPIsCloseTest());
    addCommand(new APPIsScene());
    addCommand(new APPIsCoverNew());
    addCommand(new APPIsLoadingPage());
    addCommand(new APPIsNothing());
}

}

#将变量抽取到上下文对象中 1.在逻辑的拆分的时候,每个逻辑模块都传入了多个参数,为了统一各个模块,将每个类中方法的参数传入到上下文对象中;在项目的cn.boxfish.onekey.helper.context文件夹下创建上下文类Context,将老师模块与学生模块中方法的参数设置为上下文属性,在传递参数的时候,只需要初始化Context对象然后将参数存入上下文对象中,在调用模块中的方法的时候,参数只需要传入context对象;

2.在设置上下文对象的属性时,需要注意,不是所有的参数都需要存放在上下文对象中,有些参数是需要从另一个参数中得到的,可以直接从另一个参数中获取即可;

3.修改初始化上下文的位置,上下文对象在一次跑课中,只使用一次,在初始化的时候,应该是在跑课开始的时候创建上下文对象,不能在全局位置创建上下文对象;

#调整跑课代码顺序 1.定义抽象类cn.boxfish.single.export.AbstractExportService,实现ApplicationEventPublisherAware接口,将学生模块与老师模块公共的功能代码移动到该类中;初始化赋值、提供excel解析完成的主数据、扩展数据等;

2.整理学生版跑课与老师版跑课代码:将学生版跑课代码中初始化链和初始化上下文的代码放在cn.boxfish.single.export.ExportStudentService类中;将老师版中初始化链和上下文的代码放在ExportTeacherService中,将具体的细节交给子类实现;

3.整理nlp相关的代码:将调用nlp接口保存数据的方法和获得听力时长和短句数的方法保存到cn.boxfish.onekey.nlp.NlpHandler中,给跑课代码调用 / /与nlp相关的代码放在NlpHandler类中 @Value("${nlp.article.url}") private String nlpUrl;

private AsyncRestTemplate restTemplate = new AsyncRestTemplate();

public void saveNlpArticle(String lessonId, String type, Integer phraseCount, Long listeningDuration) { restTemplate.postForEntity( nlpUrl.replace("::lessonId", lessonId) .replace("::type", type) .replace("::phraseCount", String.valueOf(phraseCount)) .replace("::listeningDuration", String.valueOf(listeningDuration)), null, Object.class); }

public void handle(Context context) { Map> sheetMap = context.getSheetMap(); File file = context.getFile().toFile(); String projectId = context.getProjectId(); String type = context.getType();

//获得课的短句数
Integer phraseCount = CalculCount.count(sheetMap, file, projectId);
//获得课中音频时长
Long audioDuration = AudioDuration.countAudioDuration(sheetMap);
//获得课中视频时长
Long videoDuration = VideoDuration.countVideoDuration(sheetMap);
//听力时长
Long listeningDuration = audioDuration + videoDuration;

logger.info("NLP数据获取-{}: id:[{}]", type, projectId);
try {
    this.saveNlpArticle(projectId, type, phraseCount, listeningDuration);
} catch (Exception e) {
    logger.error("NLP数据获取出错-{}: id:[{}],错误:{{}}", type, projectId, e);
}

}