1. 程式人生 > 程式設計 >策略模式&模板模式&工廠模式 如何優雅地用在專案中

策略模式&模板模式&工廠模式 如何優雅地用在專案中

關於策略模式、模板模式和工廠模式的基礎概念和優缺點可以自行了解一下,這裡主要講的是如何優雅地使用這三種模式保證服務符合: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:

未完待續