×
Community Blog Scenario Execution Tool: Java

Scenario Execution Tool: Java

This article explores optimizing the process in Java through pattern design and tooling to reduce code redundancy and improve development efficiency and quality.

1

By Shaojie

In the practice of software development, faced with complicated and volatile business scenarios, developers often need to design flexible and scalable architectures to meet the requirement of executing different logic according to different scenarios. Based on the Java language, this article discusses how to optimize this process through pattern design and tooling, aiming to reduce code redundancy and improve development efficiency and code quality.

1. Purpose

In the daily development process, we often encounter the requirement of performing different operations according to different scenarios. For example, when it comes to claiming red envelopes, if a user is not eligible, the system should return a message saying Not Eligible. If a user is eligible but hasn't claimed their red envelope, actions such as claiming the red envelope, recording the transaction, and sending a confirmation message are required. For users who have already claimed their red envelopes, other specific operations should be performed.

To avoid repetitive code, various patterns will be used to create abstract classes, specific implementation classes, and factory classes to solve this problem. This approach ensures better code extensibility when adding new scenarios. For instance, when dealing with users claiming red envelopes, you can add new scenarios like checking whether the red envelope has expired or whether it has been used.

Furthermore, a scenario execution tool is developed to simplify the development process.

Advantages:

Users only need to write specific implementation classes without considering other interfaces and factory classes.

• The development process is simplified, but the extensibility and readability mentioned above still exist.

2. Class Diagram

Status Model + Factory Pattern

2

3. Code Details

Now there is a requirement for users to activate a certain service, and the activation result needs to be returned in a message. The activation result message has many statuses: successful activation, successful termination, freeze, and so on. Different operations need to be performed based on the status of this message.

The status model naturally comes to mind. First, define a general status processing interface, and then create various specific status implementation classes according to the activation status to implement the doCallback method. In this way, the code we write will have good extensibility and readability.

3.1 Status Model

1. Scenario processing interface:

public interface SceneHandleBase<A,T,G> {

    /**
     *  Execution method
     */
    G doCallback(T params) ;

}

2. Scenario abstract class:

Encapsulate a layer on top of the scenario processing interface, which is mainly used for exception log printing and monitoring.

@Slf4j
public abstract class AbstractSceneHandleBase<A, T, G> implements SceneHandleBase<A, T, G> {

    /**
     * Execution method of the scenario implementation class
     *
     * @param params
     * @return
     * @throws Exception
     */
    public abstract G execute(T params);


    /**
     * Print execution exception logs and monitor exceptions
     */
    @Override
    public G doCallback(T params) {
        try {
            return execute(params);
        } catch (Exception e) {
            log.error("{}, |StatusHandleBase_doCallback|error|,className:{}, doCallback, params:{}, msg:{}", EagleEye.getTraceId(), this.getClass().getSimpleName(), JSON.toJSONString(params), e.getMessage(), e);
            throw e;
        }
    }

}

3. Specific scenario implementation class:

For successful activation, write the signing table.

@Component
@Slf4j

public class ContractStartedSceneHandleImpl extends AbstractSceneHandleBase<User, ContractEvent, TResult<Boolean>> {

    @Resource
    private PurchasePayLaterBizService purchasePayLaterBizService;

    @Override
    public boolean judge(User params) {
        return true;
    }

    @Override
    public TResult<Boolean> execute(ContractEvent contractEvent) {
        UserSignResult userSignResult = purchasePayLaterBizService.buildUserSignResult(contractEvent, SignStatus.SIGN_SUCCESS);
        return purchasePayLaterBizService.updateSignStatus(userSignResult);
    }
}

For successful termination, write the signing table.

@Component
@Slf4j
public class ContractClosedSceneHandleImpl extends AbstractSceneHandleBase<User, ContractEvent, TResult<Boolean>> {

    @Resource
    private PurchasePayLaterBizService purchasePayLaterBizService;

    @Override
    public boolean judge(User params) {
        return true;
    }

    @Override
    public TResult<Boolean> execute(ContractEvent contractEvent) {
        UserSignResult userSignResult = purchasePayLaterBizService.buildUserSignResult(contractEvent, SignStatus.SIGN_FAIL);
        return purchasePayLaterBizService.updateSignStatus(userSignResult);
    }
}

Although the code written in this way is simplified a lot, we still need to determine the status of the message to execute different specific implementation classes and write if/else in the code. The status execution is as follows:

if(ContractStatusEnum.valueOf(contractEvent.getStatus())==ContractStatusEnum.STARTED){
    ContractStartedSceneHandleImpl.execute("x");
}else if(ContractStatusEnum.valueOf(contractEvent.getStatus())==ContractStatusEnum.CLOSE){
    contractClosedSceneHandleImpl.execute("x");
}
......

For further optimization, it's natural to consider using the factory pattern to manage these implementation classes.

3.2 Factory Pattern

Scenario annotation

/**
 * @author shaojie
 * @date 2024/07/10
 * Use the enumeration value address during annotation, such as, *.ContractStatusEnum.CLOSED
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface SceneHandleType {

    String value();
}

Add the annotation @SceneHandleType("CLOSED") to the specific implementation class.

In this way, the implementation class can be obtained through the factory class.

public class SceneHandleFactory<A> implements ApplicationContextAware, InitializingBean {

    private final Map<String, List<SceneHandleBase>> statusMap = new HashMap<>();
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * After the factory class is initialized, execute the method
     * Obtain all the implementation classes of StatusHandleBase
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, SceneHandleBase> recommendStrategyMap = applicationContext.getBeansOfType(SceneHandleBase.class);
        if (MapUtils.isEmpty(recommendStrategyMap)) {
            return;
        }
        for (SceneHandleBase strategy : recommendStrategyMap.values()) {
            SceneHandleType statusType = strategy.getClass().getAnnotation(SceneHandleType.class);
            if (null == statusType) {
                continue;
            }
            String statusValue = statusType.value();
            if (null == statusValue) {
                continue;
            }
            statusMap.put(statusValue, strategy);
        }

    }

    /**
     * Obtain the specific scenario processing class
     *
     * @param status
     * @return
     */
    public SceneHandleBase getSceneHandle(String status) {
        return statusMap.get(statusType);
    }

}

The status execution becomes:

statusHandle = statusFactory.getSceneHandle(contractEvent.getStatus();
TResult<Boolean> res = statusHandle.doCallback(contractEvent);

At this point, the whole execution method becomes very concise. After that, if a new activation status is added, only an implementation class needs to be added to guarantee the extensibility of the code.

However, there are many similar situations, such as receiving red envelopes mentioned at the beginning. It will waste a lot of time to write a bunch of code to implement factory classes, annotations, and abstract interfaces.

Therefore, if a general scenario executor can be extracted, users only need to consider the implementation class, and the rest of the interfaces and factory classes are implemented. This would greatly improve efficiency.

3.3 Scenario Executor

First, CLOSED should no longer be used as a value for the annotation, @SceneHandleType("CLOSED"). In a system, there may be many business usage scenario executors. In this way, names cannot be managed in a unified way, and are easy to be repeated. Factory classes are also difficult to obtain entity classes based on annotation values.

Therefore, scenario enumeration is used here to manage various scenarios of this business. Different businesses are isolated through different scenario enumeration. The full address of the enumeration value is used as the annotation value. This ensures the uniqueness of the annotation value.

In this way, the factory class obtains the input parameter of the specific implementation class as the enumeration value, just like the specific scenario implementation class. For the generality of the factory class, a scenario enumeration interface is also required for the input parameters of the factory class method.

Scenario enumeration interface:

The enumeration class implements this interface. The getSceneName method is used to obtain the name of the enumeration value for concatenating the full address of the enumeration value.

public interface SceneEnumBase {

    /**
     * Obtain the name of the attribute that implements the enumeration class
     * @return
     */
    String getSceneName();
}

Activation status enumeration class:

The annotation @SceneHandleType value on the specific scenario implementation class is the full address of the corresponding enumeration value.

What is the enumeration value?

• The enumeration value here is the x in if(x){y}, which maps the condition to an enumeration value.

• If the red envelope has not been received, the user needs to receive the red envelope. Then the enumeration value is the red envelope has not been received, and needs to receive the red envelope is the execute in the specific execution class.

• It can also be a status value of the activation result enumeration.

public enum ContractStatusEnum implements SceneEnumBase {
    /**
     * Effective
     */
    STARTED,

    /**
     * Freeze
     */
    FROZEN,

    /**
     * Exit
     */
    CLOSED,

    /**
     * Not activated
     */
    NO_ENTRY;




    /**
     * Obtain the name of the attribute that implements the enumeration class
     * @return
     */
    @Override
    public String getSceneName() {
        return this.name();
    }
}

Specific scenario implementation class

The annotation is the full address of the enumeration value.

@SceneHandleType("*.ContractStatusEnum.CLOSED")
@Component
@Slf4j
public class ContractClosedSceneHandleImpl extends AbstractSceneHandleBase<User, ContractEvent, TResult<Boolean>> {
......

Status processing factory class

Method for obtaining the specific implementation class:

public class SceneHandleFactory<A> implements ApplicationContextAware, InitializingBean {
    ......
public SceneHandleBase getSceneHandle(SceneEnumBase status) {
        String statusType = String.join(".", status.getClass().getName(), status.getSceneName());// concatenate the full address of the enumeration
        return statusMap.get(statusType);
    }
}

There may be more than one operation for the same scenario enumeration value. For example, in the scenario that the user has received a red envelope, if this red envelope has been used, operation a is executed, otherwise, operation b is executed.

At this point, the judgeConditions method of the scenario abstract interface is used, which selects the appropriate scenario implementation class in the method of obtaining the bean in the factory class.

public interface SceneHandleBase<A,T,G> {

    /**
     * If there are multiple scenario implementation classes for the same enumeration value, you can use this method to determine which scenario implementation class to use
     */
    Boolean judgeConditions(A params);

    /**
     *  Execution method
     */
    G doCallback(T params) ;

}

Factory class

public class SceneHandleFactory<A> implements ApplicationContextAware, InitializingBean {

    private final Map<String, List<SceneHandleBase>> statusMap = new HashMap<>();
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * After the factory class is initialized, execute the method
     * Obtain all the implementation classes of StatusHandleBase
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, SceneHandleBase> recommendStrategyMap = applicationContext.getBeansOfType(SceneHandleBase.class);
        if (MapUtils.isEmpty(recommendStrategyMap)) {
            return;
        }
        for (SceneHandleBase strategy : recommendStrategyMap.values()) {
            SceneHandleType statusType = strategy.getClass().getAnnotation(SceneHandleType.class);
            if (null == statusType) {
                continue;
            }
            String statusValue = statusType.value();
            if (null == statusValue) {
                continue;
            }
            List<SceneHandleBase> list = statusMap.getOrDefault(statusValue, new ArrayList<>());// The same annotation value can correspond to multiple scenario implementation classes
            list.add(strategy);
            statusMap.put(statusValue, list);
        }

    }

    /**
     * Obtain the specific scenario processing class based on the condition
     *
     * @param status
     * @return
     */
    public SceneHandleBase getSceneHandle(SceneEnumBase status, A params) {
        String statusType = String.join(".", status.getClass().getName(), status.getSceneName());
        List<SceneHandleBase> bases = statusMap.get(statusType);
        if (CollectionUtils.isNotEmpty(bases)) {
            for (SceneHandleBase base : bases) {
                if (base.judgeConditions(params)) {// Filter the scenario implementation class
                    return base;
                }
            }
        }
        return null;
    }

}

4. Usage

Add the dependency

<dependency>
  <groupId>*</groupId>
  <artifactId>*</artifactId>
  <version>*</version>
</dependency>

Inject factory beans

    @Bean
    public SceneHandleFactory getStatusHandleFactory() {
        return new SceneHandleFactory();
    }

Define scenario enumeration based on conditions, implement SceneEnumBase, and directly copy the getSceneName method

public enum ContractStatusEnum implements SceneEnumBase {
    /**
     * Effective
     */
    STARTED,

    /**
     * Freeze
     */
    FROZEN,

    /**
     * Exit
     */
    CLOSED,

    /**
     * Not activated
     */
    NO_ENTRY;

    /**
     * Obtain the name of the attribute that implements the enumeration class
     * @return
     */
    @Override
    public String getSceneName() {
        return this.name();
    }
}

Scenario implementation class:

1.  Add an annotation

@Component
@StatusHandleType("*.CJCreditContractStatusEnum.CLOSED")

The full address of the enumeration and the enumeration value are in quotation marks.

2.  Two implementation methods

judge(): Multiple implementation classes can be set for the same enumeration value. When the factory class obtains the specific implementation class, the implementation class of this enumeration value is obtained through this method.

execute(): The implementation method of the specific implementation class.

@StatusHandleType("*.ContractStatusEnum.CLOSED")
@Component
@Slf4j
public class ContractClosedSceneHandleImpl extends AbstractSceneHandleBase<User, ContractEvent, TResult<Boolean>> {

    @Resource
    private PurchasePayLaterBizService purchasePayLaterBizService;

    @Override
    public boolean judge(User params) {
        return true;
    }

    @Override
    public TResult<Boolean> execute(ContractEvent contractEvent) {
        UserSignResult userSignResult = purchasePayLaterBizService.buildUserSignResult(contractEvent, SignStatus.SIGN_FAIL);
        return purchasePayLaterBizService.updateSignStatus(userSignResult);
    }
}

Execution

SceneHandleBase<User, ContractEvent, TResult<Boolean>> statusHandle = statusFactory.getSceneHandle(ContractStatusEnum.valueOf(contractEvent.getStatus()),null);
TResult<Boolean> res = statusHandle.doCallback(contractEvent);

5. Summary

This article expounds the whole process from the original condition branch logic to the optimization using design patterns, and finally realizing a highly abstract scenario execution tool through a step-by-step practical case study. This process not only demonstrates the technical depth but more importantly, reflects the application value of object-oriented design principles, that is, improving the flexibility and extensibility of software systems through high-cohesion and low-coupling design.

The proposal of the scenario execution tool greatly reduces the coding burden on developers in the face of changeable business scenarios, allowing them to focus more on the implementation of business logic rather than cumbersome architecture construction, thus making the entire solution both powerful and easy to integrate.


Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.

0 0 0
Share on

Alibaba Cloud Community

1,048 posts | 257 followers

You may also like

Comments

Alibaba Cloud Community

1,048 posts | 257 followers

Related Products