Spring 實現策略模式--自定義註解方式解耦if...else

諸葛小亮發表於2021-05-22

策略模式

定義

定義一簇演算法類,將每個演算法分別封裝起來,讓他們可以互相替換,策略模式可以使演算法的變化獨立於使用它們的客戶端

場景

使用策略模式,可以避免冗長的if-else 或 switch分支判斷

實現

  1. 策略的定義

    策略的定義需要定義一個策略介面和一組實現這個介面的策略類,因為所有的策略類都實現相同的介面

public interface Strategy{
	void algorithm();
}

public class ConcreteStrategyA implements Strategy {
 @Override
 public void algorithm() {
 //具體的演算法...
 }
}
public class ConcreteStrategyB implements Strategy {
 @Override
 public void algorithm() {
 //具體的演算法...
 }
}
  1. 策略的建立

    在使用的時候,一般會通過型別來判斷建立哪個策略來使用,在策略上下文中,可以使用map維護好策略類

  2. 策略的使用

    策略模式包含一組可選策略,在使用策略時,一般如何確定使用哪個策略呢?最常見的是執行時動態確定使用哪種策略。程式在執行期間,根據配置、計算結果、網路等這些不確定因素,動態決定使用哪種策略

public class StrategyContext{
	private static final Map<String, Strategy> strategies = new HashMap<>();
    
	static {
     strategies.put("A", new ConcreteStrategyA());
     strategies.put("B", new ConcreteStrategyB());
    }
    
    private static Strategy getStrategy(String type) {
         if (type == null || type.isEmpty()) {
    	     throw new IllegalArgumentException("type should not be empty.");
         }
         return strategies.get(type);
	}
    
    public void algorithm(String type){
        Strategy strategy = this.getStrategy(type);
        strategy.algorithm();
    }
}

UML

策略模式的建立和使用--Spring和自定義註解

在介紹策略模式時,在上下文中使用了map儲存好的策略例項,在根據type獲取具體的策略,呼叫策略演算法。
當需要新增一種策略時,需要修改context程式碼,這違反了開閉原則:對修改關閉,對擴充套件開放。

要實現對擴充套件開放,就要對type和具體的策略實現類在程式碼中進行關聯,可以使用自定義註解的方式,在註解中指定策略的type。
策略上下文實現類實現 BeanPostProcessor 介面,在該介面中編寫策略型別與bean的關係並維護到策略上下文中。

package com.masterlink.strategy;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
public class StrategyDemoBeanPostProcessor implements BeanPostProcessor, Ordered {

    private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));

    private final StrategyContext strategyContext;

    private StrategyDemoBeanPostProcessor(StrategyContext context) {
        this.strategyContext = context;
    }

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {

        if (!this.nonAnnotatedClasses.contains(bean.getClass())) {
            // 獲取使用 @StrategyDemo 註解的Class資訊
            Class<?> targetClass = AopUtils.getTargetClass(bean);
            Class<Strategy> orderStrategyClass = (Class<Strategy>) targetClass;
            StrategyDemo ann = findAnnotation(targetClass);
            if (ann != null) {
                processListener(ann, orderStrategyClass);
            }
        }
        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    protected void processListener(StrategyDemo annotation,
                                   Class<Strategy> classes) {
        // 註冊策略
        this.strategyContext
                .registerStrategy(annotation.type(), classes);
    }

    private StrategyDemo findAnnotation(Class<?> clazz) {

        StrategyDemo ann = AnnotatedElementUtils.findMergedAnnotation(clazz, StrategyDemo.class);
        return ann;
    }

}


@Component
public class StrategyContext implements ApplicationContextAware {
    private final Map<String, Class<Strategy>> strategyClassMap = new ConcurrentHashMap<>(64);

    private final Map<String, Strategy> beanMap = new ConcurrentHashMap<>(64);

    private ApplicationContext applicationContext;

    /**
     * 註冊策略
     * @param type
     * @param strategyClass
     */
    public void registerStrategy(String type, Class<Strategy> strategyClass){
        if (strategyClassMap.containsKey(type)){
            throw new RuntimeException("strategy type:"+type+" exist");
        }
        strategyClassMap.put(type, strategyClass);
    }

    /**
     * 執行策略
     * @param type
     */
    public void algorithm(String type){
        Strategy strategy = this.getStrategy(type);
        strategy.algorithm();
    }

    private Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("type should not be empty.");
        }
        Class<Strategy> strategyClass = strategyClassMap.get(type);
        return createOrGetStrategy(type, strategyClass);
    }

    private Strategy createOrGetStrategy(String type,Class<Strategy> strategyClass ){
        if (beanMap.containsKey(type)){
            return beanMap.get(type);
        }
        Strategy strategy = this.applicationContext.getBean(strategyClass);
        beanMap.put(type, strategy);
        return strategy;
    }

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

實用案例

在我們的平臺中,有一部分是使用的netty框架編寫的tcp服務,在服務端,需要將二進位制轉換為物件,在協議設計階段,定義第一個位元組表示物件型別,比如int,String等,第二三個位元組,表示資料長度,後面的位元組位傳輸內容。
比如,
0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x09,解析出來的內容是int型別數字9。
0x02, 0x00, 0x03, 0x31, 0x32, 0x33, 解析出的內容是String型別,內容是 123。
在不使用策略模式的時候,需要將第一個位元組解析出來,然會使用if--else判斷型別,對後繼的位元組進行解析。
在實際的實現過程中,是使用了策略模式,並且使用註解的方式表示資料型別,實現過程如下。

定義策略介面和註解

定義 CodecStrategyType 註解和編碼解碼器的策略介面 CodecStrategy

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CodecStrategyType {
    /**
     * 編碼解碼型別
     * @return
     */
    byte type();
}

public interface CodecStrategy<T> {
    T decoding(byte[] buffer);
}

/*
* 通用解碼介面
 */
public interface Codec {
    Object decoding(byte[] bytes);
}


策略實現

實現兩種型別的解碼器: Integer  和 String

/**
 * integer解碼
 */
@CodecStrategyType(type = (byte)0x01)
@Service
public class IntgerCodecStrategy implements CodecStrategy<Integer> {
    
    @Override
    public Integer decoding(byte[] buffer) {
        int value;
        value = (int) ((buffer[3] & 0xFF)
                | ((buffer[2] & 0xFF)<<8)
                | ((buffer[1] & 0xFF)<<16)
                | ((buffer[0] & 0xFF)<<24));
        return value;
    }
}

@CodecStrategyType(type = (byte)0x02)
@Service
public class StringCodecStrategy implements CodecStrategy<String> {

    @Override
    public String decoding(byte[] bufferr) {
        return new String(bufferr);
    }
}

策略上下文和策略註冊

策略上下文類 CodecStrategyContext 提供了統一解碼入口,將 byte[] 轉換為 Object 型別,同時提供策略的註解介面 void registerStrategy(Byte type, Class<CodecStrategy<?>> strategyClass) ,註冊解碼型別對應的策略實現類。
策略上下文類同時還提供了策略Bean的建立,根據型別從Spring 的 ApplicationContext 獲取策略bean,並快取到map。
策略Bean處理類 CodecStrategyTypeBeanPostProcessor 中解析 CodecStrategyType 註解中指定的型別。


@Component
public class CodecStrategyContext implements ApplicationContextAware, Codec {
    private final Map<Byte, Class<CodecStrategy<?>>> strategyClassMap = new ConcurrentHashMap<>(64);

    private final Map<Byte, CodecStrategy<?>> beanMap = new ConcurrentHashMap<>(64);

    private ApplicationContext applicationContext;

    /**
     * 註冊策略
     * @param type
     * @param strategyClass
     */
    public void registerStrategy(Byte type, Class<CodecStrategy<?>> strategyClass){
        if (strategyClassMap.containsKey(type)){
            throw new RuntimeException("strategy type:"+type+" exist");
        }
        strategyClassMap.put(type, strategyClass);
    }

    /**
     * 執行策略
     */
    @Override
    public Object decoding(byte[] bytes){
        Byte type = bytes[0];
        CodecStrategy<?> strategy =this.getStrategy(type);
        byte l1 = bytes[1];
        byte l2= bytes[2];
        short length =  (short) ((l2 & 0xFF)
                | ((l1 & 0xFF)<<8));
        byte[] contentBytes = new byte[length];
        arraycopy(bytes,3,contentBytes,0, length);
        return strategy.decoding(contentBytes);
    }

    private CodecStrategy<?> getStrategy(Byte type) {
        Class<CodecStrategy<?>> strategyClass = strategyClassMap.get(type);
        return createOrGetStrategy(type, strategyClass);
    }

    private CodecStrategy<?> createOrGetStrategy(Byte type, Class<CodecStrategy<?>> strategyClass ){
        if (beanMap.containsKey(type)){
            return beanMap.get(type);
        }
        CodecStrategy<?> strategy = this.applicationContext.getBean(strategyClass);
        beanMap.put(type, strategy);
        return strategy;
    }

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

@Component
public class CodecStrategyTypeBeanPostProcessor implements BeanPostProcessor, Ordered {

    private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));

    private final CodecStrategyContext strategyContext;

    private CodecStrategyTypeBeanPostProcessor(CodecStrategyContext context) {
        this.strategyContext = context;
    }

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {

        if (!this.nonAnnotatedClasses.contains(bean.getClass())) {
            // 獲取使用 @StrategyDemo 註解的Class資訊
            Class<?> targetClass = AopUtils.getTargetClass(bean);
            Class<CodecStrategy<?>> orderStrategyClass = (Class<CodecStrategy<?>>) targetClass;
            CodecStrategyType ann = findAnnotation(targetClass);
            if (ann != null) {
                processListener(ann, orderStrategyClass);
            }
        }
        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    protected void processListener(CodecStrategyType annotation,
                                   Class<CodecStrategy<?>> classes) {
        // 註冊策略
        this.strategyContext
                .registerStrategy(annotation.type(), classes);
    }

    private CodecStrategyType findAnnotation(Class<?> clazz) {

        CodecStrategyType ann = AnnotatedElementUtils.findMergedAnnotation(clazz, CodecStrategyType.class);
        return ann;
    }

}

使用和測試

測試Integer和String型別的策略:

  1. 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x09,解析出來的內容是int型別數字9。
  2. 0x02, 0x00, 0x03, 0x31, 0x32, 0x33, 解析出的內容是String型別,內容是 123。

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {CodecStrategyTest.CodecStrategyTestConfig.class})
public class CodecStrategyTest {

    @Resource
    Codec codec;

    @Test
    public void testInterDecoding(){
        byte[] buffer = new byte[]{
                0x01,0x00,  0x04, 0x00, 0x00,0x00, 0x09
        };
        Integer decoding = (Integer)codec.decoding(buffer);
        assertThat(decoding)
                .isNotNull()
                .isEqualTo(9);
    }

    @Test
    public void testStringDecoding(){
        byte[] buffer = new byte[]{
                0x02, 0x00, 0x03, 0x31, 0x32,0x33
        };
        String decoding = (String)codec.decoding(buffer);
        assertThat(decoding)
                .isNotNull()
                .isEqualTo("123");
    }

    @ComponentScan({"com.masterlink.strategy"})
    @Configuration
    public static class CodecStrategyTestConfig {
    }
}

擴充套件複雜型別

自定義複雜型別User類,對應協議型別為 0xA0, 第2 、3 位元組表示整個物件的欄位長度,緊接著是 Integer 型別的age 和 String 型別的name,
比如 0xA0, 0x00 0x10 0x00, 0x04, 0x00, 0x00, 0x00, 0x17, 0x00, 0x08, 0x5A,0x68,0x61,0x6E,0x67,0x53, 0x61,0x6E, 對應的user物件是

{
  "age": 23,
  "name": "ZhangSan"
}
@Data
public class User {
    private Integer age;
    private String name;
}

實現解碼策略類

已知 User 中的基礎型別依賴了 Integer 和 String ,所以在User的解碼策略類中,依賴了 IntgerCodecStrategy 和 StringCodecStrategy


@CodecStrategyType(type = (byte) (0xA0))
@Service
public class UserCodeStrategy implements CodecStrategy<User> {
    private final StringCodecStrategy stringCodecStrategy;
    private final IntgerCodecStrategy intgerCodecStrategy;

    public UserCodeStrategy(StringCodecStrategy stringCodecStrategy, IntgerCodecStrategy intgerCodecStrategy) {
        this.stringCodecStrategy = stringCodecStrategy;
        this.intgerCodecStrategy = intgerCodecStrategy;
    }

    @Override
    public User decoding(byte[] buffer) {
        byte ageL1 = buffer[0];
        byte ageL2 = buffer[1];
        short ageLength =  (short) ((ageL2 & 0xFF)
                | ((ageL1 & 0xFF)<<8));
        byte[] ageBytes = new byte[ageLength];
        System.arraycopy(buffer,2, ageBytes,0,ageLength);

        byte nameL1 = buffer[0+ageLength];
        byte nameL2 = buffer[1+ageLength];

        short nameLength =  (short) ((nameL2 & 0xFF)
                | ((nameL1 & 0xFF)<<8));

        byte[] nameBytes = new byte[nameLength];
        System.arraycopy(buffer,2+ageLength+2, nameBytes,0,nameLength);

        User user = new User();
        user.setAge(intgerCodecStrategy.decoding(ageBytes));
        user.setName(stringCodecStrategy.decoding(nameBytes));
        return user;
    }
}

測試

通過測試可以發現很輕鬆的就擴充套件了一個複雜型別的解碼演算法,這樣隨著協議的增加,可以做到對修改程式碼關閉,對擴充套件程式碼開放,符合開閉原則。


    @Test
    public void testUserDecoding(){
        byte[] buffer = new byte[]{
                (byte)0xA0, (byte)0x00 ,(byte)0x10 ,(byte)0x00, (byte)0x04,
                (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x17, (byte)0x00,
                (byte)0x08, (byte)0x5A, (byte)0x68, (byte)0x61, (byte)0x6E,
                (byte)0x67, (byte)0x53, (byte)0x61, (byte)0x6E
        };
        User user = (User)codec.decoding(buffer);
        assertThat(user)
                .isNotNull();
        assertThat(user.getAge()).isEqualTo(23);
        assertThat(user.getName()).isEqualTo("ZhangSan");
    }

總結

  1. 使用策略模式,可以避免冗長的if-else 或 switch分支判斷
  2. 掌握自定義註解的是使用方式
  3. 與使用 @Service("name") 註解相比,自定義註解方式支撐和擴充套件的型別或更靈活

關注我的公眾號,一起探索新知識新技術

相關文章