策略模式&模板模式&工厂模式 如何优雅地用在项目中

3,778 阅读4分钟

关于策略模式、模板模式和工厂模式的基础概念和优缺点可以自行了解一下,这里主要讲的是如何优雅地使用这三种模式保证服务符合:SRP(单一职责原则)和OCP(开闭原则)、耦合度低、可扩展性高和减少大量if else代码的场景。


策略模式:


1.环境(Context)角色:持有一个Strategy的引用。

2.抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

3.具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。


这种的经典并简单的策略模式大家也许已经使用过了,但是这样的策略模式会有一个缺点:

策略模式适用于多类型场景,调用策略时必定会有大量的if else,后续如有新的类型的策略需要被使用时则需要增加if else,代码改动较大,从而导致该模块可扩展性不高且会产生大量if else代码,不易维护。


为更好体验到优化的过程,首先给一个需求背景:

某服务需要展示给用户三种不同的趋势图:指数变化趋势图、新增课件数趋势图、新增点评数趋势图。




模块优化Round 1:

为了解决以上的问题,使用策略模式+自定义注解+模板模式(模板模式和优化无关,只是业务需要)来设计一下:



首先需要抽象父类策略的定义:

@Slf4j
public abstract class AbstractTrendChartHandler {

    private final EducationManager educationManager;

    public List<TrendChartDataVo> handle(EducationQuery query){
        return getTrendChartData(query);
    }

    private List<TrendChartDataVo> getTrendChartData(EducationQuery query) {
        DateRangeQueryVo dateRangeQueryVo = new DateRangeQueryVo();
        dateRangeQueryVo.setStartDate(LocalDate.now().minusWeeks(TrendChartConstant.towMonthsWeeks));
        dateRangeQueryVo.setEndDate(LocalDate.now());
        List<TrendChartDataVo> trendChartDataVos = educationManager.getTrendChartData(query.getAreaCode(),
                AreaRankingType.Companion.toAreaRankingType(query.getAreaRankingType()), dateRangeQueryVo);
        return getValuesOperation(trendChartDataVos);
    }

    /**
     * 趋势图的每个点的数值处理(默认是返回与前七天平均值的差值)
     *
     * @param trendChartDataVos
     * @return
     */
    protected List<TrendChartDataVo> getValuesOperation(List<TrendChartDataVo> trendChartDataVos) {
        if (CollectionUtils.isEmpty(trendChartDataVos) || trendChartDataVos.size() < TrendChartConstant.towMonthsWeeks) {
            return new ArrayList<>();
        }
        List<TrendChartDataVo> newTrendChartDataVos = new ArrayList<>();
        IntStream.range(0, trendChartDataVos.size()).forEach(i -> {
            if (i == 0){
                return;
            }
            TrendChartDataVo trendChartDataVo = new TrendChartDataVo();
            trendChartDataVo.setDate(trendChartDataVos.get(i).getDate());
            trendChartDataVo.setValue(trendChartDataVos.get(i).getValue() - trendChartDataVos.get(i - 1).getValue());
            newTrendChartDataVos.add(trendChartDataVo);
        });
        return newTrendChartDataVos;
    }

    @Autowired
    public AbstractTrendChartHandler(EducationManager educationManager) {
        this.educationManager = educationManager;
    }
}


可以看到,handle(EducationQuery query)方法是统一处理方法,子类可以继承也可以默认使用父类方法。getTrendChartData(EducationQuery query)是模板方法,子类一定会执行该方法,并可以重写父类的getValuesOperation(List<TrendChartDataVo> trendChartDataVos)方法。


三个子类如下: 

@Service
@Slf4j
@TrendChartType(TrendChartTypeEnum.COURSEWARE_COUNT)
public class NewClassesHandler extends AbstractTrendChartHandler {

    @Autowired
    public NewClassesHandler(EducationManager educationManager) {
        super(educationManager);
    }
}

@Service
@Slf4j
@TrendChartType(TrendChartTypeEnum.COMMENT_COUNT)
public class NewCommentsHandler extends AbstractTrendChartHandler {

    @Autowired
    public NewCommentsHandler(EducationManager educationManager) {
        super(educationManager);
    }
}

@Service
@Slf4j
@TrendChartType(TrendChartTypeEnum.SCHOOL_INDEX)
public class PigeonsIndexHandler extends AbstractTrendChartHandler {

    public List<TrendChartDataVo> getValuesOperation(List<TrendChartDataVo> trendChartDataVos) {
        if (trendChartDataVos.size() <= TrendChartConstant.towMonthsWeeks) {
            return new ArrayList<>();
        }
        trendChartDataVos.remove(0);
        return trendChartDataVos;
    }

    @Autowired
    public PigeonsIndexHandler(EducationManager educationManager) {
        super(educationManager);
    }
}

可以看到,子类NewClassesHandler和NewCommentsHandler类都默认使用了父类的模板,实现了这两种趋势图的逻辑。而PigeonsIndexHandler类则重写了父类的模板中的方法,使用了父类的逻辑后使用子类中重写的逻辑。


以上是策略规则,接下来是策略获取类 TrendChartHandlerContext类:

@SuppressWarnings("unchecked")
public class TrendChartHandlerContext {

    private Map<Integer, Class> handlerMap;

    TrendChartHandlerContext(Map<Integer, Class> handlerMap) {
        this.handlerMap = handlerMap;
    }

    public AbstractTrendChartHandler getInstance(Integer type) {
        Class clazz = handlerMap.get(type);
        if (clazz == null) {
            throw new IllegalArgumentException("not found handler for type: " + type);
        }
        return (AbstractTrendChartHandler) BeanTool.getBean(clazz);
    }
}

该类用于将前端传入的type转为子类对象。

TrendChartHandlerContext中的handlerMap的值则为这三种趋势图的类型的枚举中的值。由

TrendChartHandlerProcessor类统一扫描自定义注解的值,并统一将类型和子类对象放入handlerMap中。


使用策略:

    /**
     * 查看指数/新增课件数/新增点评数走势图
     *
     * @param query
     * @return
     */
    @GetMapping("/charts")
    public Object queryDeviceBrand(@Validated(value = {EducationGroup.GetTrendChart.class}) EducationQuery query) {
        return ResultBuilder.create().ok().data(educationService.queryTrendChartData(query)).build();
    }

service逻辑实现:

    public List<TrendChartDataVo> queryTrendChartData(EducationQuery query) {
        return trendChartHandlerContext.getInstance(query.getAreaRankingType()).handle(query);
    }

可以看到,使用策略时只需要调用策略的handler方法即可,无需关注type,规避掉大量的if else代码。



工具类:

@Component
public class BeanTool implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        if (applicationContext == null) {
            applicationContext = context;
        }
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
}

public class ClassScaner implements ResourceLoaderAware {

    private final List<TypeFilter> includeFilters = new LinkedList<>();
    private final List<TypeFilter> excludeFilters = new LinkedList<>();

    private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);

    @SafeVarargs
    public static Set<Class<?>> scan(String[] basePackages, Class<? extends Annotation>... annotations) {
        ClassScaner cs = new ClassScaner();

        if (ArrayUtils.isNotEmpty(annotations)) {
            for (Class anno : annotations) {
                cs.addIncludeFilter(new AnnotationTypeFilter(anno));
            }
        }

        Set<Class<?>> classes = new HashSet<>();
        for (String s : basePackages) {
            classes.addAll(cs.doScan(s));
        }

        return classes;
    }

    @SafeVarargs
    public static Set<Class<?>> scan(String basePackages, Class<? extends Annotation>... annotations) {
        return ClassScaner.scan(StringUtils.tokenizeToStringArray(basePackages, ",; \t\n"), annotations);
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourcePatternResolver = ResourcePatternUtils
                .getResourcePatternResolver(resourceLoader);
        this.metadataReaderFactory = new CachingMetadataReaderFactory(
                resourceLoader);
    }

    public final ResourceLoader getResourceLoader() {
        return this.resourcePatternResolver;
    }

    public void addIncludeFilter(TypeFilter includeFilter) {
        this.includeFilters.add(includeFilter);
    }

    public void addExcludeFilter(TypeFilter excludeFilter) {
        this.excludeFilters.add(0, excludeFilter);
    }

    public void resetFilters(boolean useDefaultFilters) {
        this.includeFilters.clear();
        this.excludeFilters.clear();
    }

    public Set<Class<?>> doScan(String basePackage) {
        Set<Class<?>> classes = new HashSet<>();
        try {
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                    + org.springframework.util.ClassUtils
                    .convertClassNameToResourcePath(SystemPropertyUtils
                            .resolvePlaceholders(basePackage))
                    + "/**/*.class";
            Resource[] resources = this.resourcePatternResolver
                    .getResources(packageSearchPath);

            for (int i = 0; i < resources.length; i++) {
                Resource resource = resources[i];
                if (resource.isReadable()) {
                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
                    if ((includeFilters.size() == 0 && excludeFilters.size() == 0) || matches(metadataReader)) {
                        try {
                            classes.add(Class.forName(metadataReader
                                    .getClassMetadata().getClassName()));
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        } catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "I/O failure during classpath scanning", ex);
        }
        return classes;
    }

    protected boolean matches(MetadataReader metadataReader) throws IOException {
        for (TypeFilter tf : this.excludeFilters) {
            if (tf.match(metadataReader, this.metadataReaderFactory)) {
                return false;
            }
        }
        for (TypeFilter tf : this.includeFilters) {
            if (tf.match(metadataReader, this.metadataReaderFactory)) {
                return true;
            }
        }
        return false;
    }
}


这样就算解决类if else的问题了,但是大家会发现有大量的工具需要引入,增加了许多代码量,看上去会不够美观。


模块优化Round 2:

未完待续