如何使用策略模式處理多種型別請求

一顆向上的草莓發表於2018-10-21

1.需求簡述

現在有一個活動,活動場景包含佈置書籍作業,佈置短文作業,佈置一課一練作業(以後還可能會新增其它型別的活動),每一種活動場景有自己對應的完成邏輯和獎勵。現在定義對應的場景值如下:

活動名稱 活動場景值
佈置書籍作業 11
佈置短文作業 12
佈置一課一練作業 13

2.解決方案

解決方案一:因為佈置作業和完成活動屬於不同的專案,我採用的是訊息佇列的方式(訊息佇列不是本文討論的重點),佈置作業時傳送訊息,傳遞對應的活動場景值和其它必須的引數過來,消費端收到訊息之後,根據對應的場景值作出相應的處理,虛擬碼如下:

if(場景值==11){
    //完成書籍作業的相關邏輯
}else if(場景值==12){
    //完成短文作業的相關邏輯
}else if(場景值==13){
    //完成一課一練作業的相關邏輯
}
複製程式碼

這種方式是最簡單的,也是最容易理解的,但是存在的問題是,如果現在新增新的活動場景,原來的if else後面要新增新的程式碼和判斷邏輯,對原有的程式碼具有侵入性;再者如果型別非常多的話,if else也會有很多,程式碼看起來不夠優雅。 解決方案二:採用策略模式來解決,定義一個策略介面,佈置書籍作業,短文作業,一課一練的處理邏輯都實現策略介面,根據傳入的不同場景值選擇不同的處理類。這也是使用策略模式最難的地方:如何根據傳入的引數,找到對應的處理類,答案是:可以採用spring的getBean,或者是java的反射。我在程式啟動時就載入所有的策略類到記憶體中,處理請求時,根據傳入的引數,選擇對應的處理類。

3.實操程式碼

3.1定義策略介面

/**
 * 活動策略介面
 * @author junzhongliu
 * @date 2018/9/30 17:11
 */
public interface ActivityStrategyInterface {

    /**
     * 教師建立或更新活動記錄
     * @param userId 使用者id
     * @param scene 場景
     * @param condition 本活動完成的條件
     */
    void doActivityAction(Long userId,Integer scene,Integer condition);
}
複製程式碼

3.2自定義註解

為了方便比對傳入的場景值,選擇對應的策略處理類,我自定義了一個註解

/**
 * 活動場景註解
 * @author junzhongliu
 * @date 2018/9/30 17:24
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivitySceneAnnotation {

    /**
     * 活動場景id,預設值1
     */
    int sceneId() default 1;
}
複製程式碼

3.3定義對應的策略處理介面

就是真正處理佈置書籍作業,短文作業,一課一練作業的策略實現類,它們是要實現策略介面的,程式碼如下:

/**
 * 佈置書籍任務
 * @author junzhongliu
 * @date 2018/9/30 17:13
 */
@Slf4j
@Service
@ActivitySceneAnnotation(sceneId = ActivitySceneConstants.BOOK_ACTIVITY)
public class BookStrategy implements ActivityStrategyInterface {

    @Autowired
    TeacherActivityRecordService teacherActivityRecordService;

    @Override
    public void doActivityAction(Long userId, Integer scene, Integer condition) {
        log.info("desc:{},userId:{},scene:{},condition:{}","佈置書籍任務[STRATEGY]",userId,scene,condition);
        teacherActivityRecordService.saveOrUpdateRookieActivityRecord(userId,scene,condition);
    }
}
複製程式碼

這裡的ActivitySceneAnnotation就是上面我們自定義的註解,ActivitySceneConstants.BOOK_ACTIVITY是自定義的常量,真實值是11,對應上面的場景值,可以看到在處理的過程中是呼叫了service來處理的,其它的短文任務,一課一練任務跟這個基本一樣的,只是不同的場景值,只在展示一個佈置短文作業的:


/**
 * 完成短文活動任務
 * @author junzhongliu
 * @date 2018/9/30 17:13
 */
@Slf4j
@Service
@ActivitySceneAnnotation(sceneId = ActivitySceneConstants.PASSAGE_ACTIVITY)
public class PassageStrategy implements ActivityStrategyInterface {

    @Resource
    TeacherActivityRecordService teacherActivityRecordService;

    @Override
    public void doActivityAction(Long userId, Integer scene, Integer condition) {
        log.info("desc:{},userId:{},scene:{},condition:{}","佈置短文任務[STRATEGY]",userId,scene,condition);
        teacherActivityRecordService.saveOrUpdateRookieActivityRecord(userId,scene,condition);
    }
}
複製程式碼

3.4根據場景值選擇對應的處理類

我這裡是在程式啟動時,採用@PostConstruct註解,將實現ActivityStrategyInterface介面的所有策略類都載入到記憶體中了,使用者請求傳過來一個場景值,根據這個場景值,選擇對應的處理類,全部程式碼如下:

/**
 * 策略處理類的工廠類
 * @author junzhongliu
 * @date 2018/9/30 17:17
 */
@Service
public class ActivityStrategyFactory {

    private static final Map<String,ActivityStrategyInterface> STRATEGY_BEAN_CACHE = Maps.newConcurrentMap();
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 根據不同的場景建立不同的策略
     * 實現思路:遍歷策略列表的所有策略,獲取策略的註解,
     * 比對場景值是否一致,場景值一致則返回當前策略的例項物件
     * @param scene 場景值
     * @return
     */
    public ActivityStrategyInterface createStrategy(Integer scene) {

        Optional<ActivityStrategyInterface> strategyOptional =
                STRATEGY_BEAN_CACHE
                        .entrySet()
                        .stream()
                        .map(e -> {
            ActivitySceneAnnotation validScene = e.getValue().getClass().getDeclaredAnnotation(ActivitySceneAnnotation.class);
            if (Objects.equals(validScene.sceneId(),scene)) {
                    return e.getValue();
            }
            return null;
        }).filter(Objects::nonNull)
                        .findFirst();
        if(strategyOptional.isPresent()){
            return strategyOptional.get();
        }
        throw new RuntimeException("策略獲得失敗");
    }

    /**
     * 初始化策略列表
     */
    @PostConstruct
    private void init() {
        STRATEGY_BEAN_CACHE.putAll(applicationContext.getBeansOfType(ActivityStrategyInterface.class));
    }

}
複製程式碼

到現在為止,策略相關的處理已經定義完了,接下來看如何使用

3.5如何使用

我是在訊息的消費處呼叫了ActivityStrategyFactory,傳入場景值,獲取處理類,程式碼如下:

/**
 * 消費其它模組的訊息,建立或更新教師活動記錄
 * @author junzhongliu
 * @date 2018/9/30 16:50
 */
@Slf4j
@Service
public class CreateActivityRecordMessageConsumer implements MessageConsumer<CreateActivityRecordMessage> {

    @Autowired
    private ActivityStrategyFactory strategyFactory;

    @Override
    public CreateActivityRecordMessage newMessageInstance() {
        return new CreateActivityRecordMessage();
    }

    @Override
    public void consume(CreateActivityRecordMessage message) throws Exception {
        log.info("desc:{},param:{}","建立任務記錄消費訊息[CONSUMER]",JSONObject.toJSONString(message));
        Long userId = message.getUserId();
        Integer scene = message.getScene();
        Integer condition = message.getCondition();
        if(Objects.isNull(userId) || Objects.isNull(scene) || Objects.isNull(condition)){
            return;
        }
        //建立具體的執行策略,並執行活動行為
        ActivityStrategyInterface strategy = strategyFactory.createStrategy(message.getScene());
        strategy.doActivityAction(userId,scene,condition);
    }

}
複製程式碼

這是整個過程的全部程式碼,如果現在新增其它活動場景(比如佈置假期作業),那麼直接寫一個佈置假期作業的處理類,新增一個對應的場景值就可以了,對原有程式碼不侵入。

相關文章