如何做代码抽象设计,多种设计模式的应用【四种抽象方式,干货附上代码】

满满干货哈,十分有助于大家整体代码设计,现在就和大家简单介绍一下下面四种模式的抽象设计

1. 注解式
2. 拼接式
3. 实现扩展接口式
4. 函数式接口式

大家不用担心,示例代码中我也基本加了注释,便于大家理解

注解式
核心用到的方式:单例模式+注解+反射+策略模式

直接上代码了哈,注释搞在代码中,大家直接体验

既然是注解,那就先定义一个注解吧
 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BizCode {

    String value() default "";

}

 然后定义一个执行接口

/**
 * 扩展点执行引擎
 * 真正的扩展点在ProcessExtension接口,实现此接口并注解即可,在外部接口使用过程中自然根据BizCode
 * 索引到类
 * 单例模式+注解+反射+策略模式
 */
public interface Engine {

    String process(String bizCode, String params);

}

然后定义一个实现类,大家可以看到ExtensionEngine的构造方法中有个单例的写法
为什么写在构造方法中?是为了在启动时就会执行并加载到一个全局变量中,等会我们往下看就知道了,当然了,此处用其他方式也行,能在启动时加载就好

/**
 * 执行引擎
 */
@Component
public class ExtensionEngine implements Engine {

    public ExtensionEngine() {
        // 注册扩展 在启动时就被加载进来
        ExtensionBuilder.getInstance().build();
    }

    @Override
    public String process(String bizCode, String params) {
        StringBuilder processRecord = new StringBuilder();

        //拿到对应的实例
        ProcessExtension processExtension = ExtensionBuilder.getInstance().getExt(bizCode);
        // 1、前置处理
        processExtension.beforeProcess(params, processRecord);
        // 2、统一处理
        System.out.println("统一处理流程");
        processRecord.append("统一处理 - ");
        // 3、后置处理
        processExtension.afterProcess(params, processRecord);

        return processRecord.toString();
    }
}

我们看到如果拿到了实例就会进行前中后的调用处理,其实就是多条分支策略的处理,定义一个接口

public interface ProcessExtension {

    /**
     * 前置处理
     * @param params 参数
     * @param processRecord 处理对象传递
     */
    void beforeProcess(String params, StringBuilder processRecord);

    /**
     * 后置处理
     * @param params 参数
     * @param processRecord 处理对象传递
     */
    void afterProcess(String params, StringBuilder processRecord);

    // 其他模板动作接口
}

定义两三个实现类,具体你说的算,这就用到我们上面定义的注解了,value用常量定义就行,后来我也试过用枚举,只不过只能用枚举本身,不能枚举.getValue,都可以
一般如果不定义value默认就是类名首字母小写jdProcessExtension,这个大家熟知一下

@BizCode(value = "jd")
public class JdProcessExtension implements ProcessExtension {

    @Override
    public void beforeProcess(String params, StringBuilder processRecord) {
        System.out.println("京东活动前置处理流程");
        processRecord.append("京东活动前置处理流程 - ");
    }

    @Override
    public void afterProcess(String params, StringBuilder processRecord) {
        System.out.println("京东活动后置处理流程");
        processRecord.append("京东活动后置处理流程");
    }
}
@BizCode(value = "tb")
public class TaobaoProcessExtension implements ProcessExtension {

    @Override
    public void beforeProcess(String params, StringBuilder processRecord) {
        System.out.println("淘宝活动前置处理流程");
        processRecord.append("淘宝活动前置处理流程 - ");
    }

    @Override
    public void afterProcess(String params, StringBuilder processRecord) {
        System.out.println("淘宝活动后置处理流程");
        processRecord.append("淘宝活动后置处理流程");
    }
}

下面我们就看下那个单例是如何注册的吧,这个比较关键了!标准单例写法,静态内部类的姿势。

把所有的BizCode注解通过反射识别并注入到了Map<String, ProcessExtension>中,即Map<枚举或String都可以, 接口>
再定义一个根据Key获取Value的就行了
 

/**
 * 业务注册处理类
 * 单例模式
 */
@Slf4j
public class ExtensionBuilder {
    private ExtensionBuilder() {
    }
    public static ExtensionBuilder getInstance() {
        return ExtensionBuilderInstance.INSTANCE;
    }

    /**
     * 静态内部类方式
     */
    private static class ExtensionBuilderInstance {
        private static final ExtensionBuilder INSTANCE = new ExtensionBuilder();
    }

    /**
     * bizcode => 业务扩展类实例
     */
    private Map<String, ProcessExtension> extMap = new ConcurrentHashMap<>();

    /**
     * 注册业务扩展类实例
     */
    public void build() {
        try {
            log.debug("找出所有注解了BizCode的类并注册");
            // 找出所有注解了BizCode的类
            Reflections extReflections = new Reflections("com.mtgg.laoxiang");
            Set<Class<?>> extClasses = extReflections.getTypesAnnotatedWith(BizCode.class);

            for (Class<?> extClass : extClasses) {
                BizCode[] annotationsByType = extClass.getAnnotationsByType(BizCode.class);
                if (annotationsByType != null && annotationsByType.length > 0) {
                    BizCode bizCode = annotationsByType[0];
                    // 构建 bizcode => 业务扩展类实例
                    extMap.put(bizCode.value(), (ProcessExtension) extClass.newInstance());
                }
            }
            log.debug("ExtensionBuilder加载扩展实例:{}", extMap);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据业务编码,获取对应的业务扩展类实例
     *
     * @param bizCode
     * @return
     */
    public ProcessExtension getExt(String bizCode) {
        return extMap.get(bizCode);
    }
}

调用的时候就直接注入Engine,然后调用process方法就好了,只要指定bizCode就没毛病

下面介绍第二种哈

拼接式
从代码多少上面看这个还真容易一点,优劣等会再说

话说在启动后Spring已经帮我们把该注册的都已经注册成Bean Map了,key就是类名(首字母小写),value就是这个类的实例,我们只要用就好了

以注解式jd,taobao两个实现类以及其实现的接口ProcessExtension为例,不需要自定义注解了
 

@Component
public class JdProcessExtension implements ProcessExtension {
}
@Component
public class TaobaoProcessExtension implements ProcessExtension {
}

直接注入使用,下面这个map就已经有数据了,其value就是子类返回的接口(此处是Spring加载bean的知识,大家可以搜一下Spring加载过程)

@Autowired
private Map<String, ProcessExtension> map;

这个map是什么形式呢,我直接演示下,比如这两个实现类的key就是前面说过的首字母小写的实现类

jdProcessExtension

taobaoProcessExtension

我们只要拿到key获取到value–>ProcessExtension,调用相关接口就能自己往下走了,所以问题是如何拿到key?

可以定义一个常量"%sProcessExtension"; 就是%s+实现类统一后缀;
传入bizCode用String.format替换一下%s就得到了key
比如我传个jd,得到了jdProcessExtension ,用map.get()拿到ProcessExtension实例后调用接口,走的就是jd的实现类,完毕!

下面介绍第三种

扩展接口式

说到底这个就是利用Spring的扩展接口或预加载注解,或构造方法来实现启动时将实例加载到一个Map中的,和第一个结构有点相似,有一个调用层处理。上菜,这里以支付为例

  1. 先定义一个Service
  2. public interface PayCoreService {
        /**
         * @Author: 老乡
         * @Date: 2020/6/13 21:50
         * @Describe: 统一下单
         * 接入新的支付方式只需要增加枚举类,继承PayStrategyContent抽象类,重写支付等功能接口
         */
        Result toPayCore(PayCoreDTO payCoreDTO);
    

    定义一个抽象类实现Service,
    定义抽象方法(下面会定义两个子类继承它并实现抽象方法)
    然后大家可以看到这里也同样是定义了ConcurrentHashMap用来全局保存实例的,key为子类设置的枚举即子类实例,value为Service实例,这里就是为了抽象方法找到到底要调用哪个子类用的
     

    /**
     * @Author: 老乡
     * @Describe: 当增加一个功能的时候可以在抽象类中实现接口,通过抽象方法让子类去实现
     * 如果有的子类没有用到这个功能也可以不重写,将类改为抽象类即可;
     * 不过一般抽象出来的功能都是共用的行为,所以即使暂时不用也可以先重写后放在那里
     * <p>
     * 此抽象类完成了对无法选择的延迟到子类实现,实现了PayStrategy接口,转化找到
     */
    @Slf4j
    public abstract class PayCoreStrategyAbstract implements PayCoreService {
        /**
         * 各种支付策略实例预加载
         */
        public static Map<Integer, PayCoreService> strategyMap = new ConcurrentHashMap<>();
    
        /**
         * @Author: 老乡
         * @Describe: 只在启动时加载一次,加载类别在子类中控制
         * this为当前PayStrategy,因为调用的时候取到具体的实例如支付宝实例,返回的是接口PayStrategy
         */
        @PostConstruct
        public void init() {
            Map<Integer, PayCoreService> integerPayStrategyMap = channelInit();
            for (Map.Entry<Integer, PayCoreService> m : integerPayStrategyMap.entrySet()) {
                strategyMap.put(m.getKey(), m.getValue());
            }
            log.info("所有支付方式初始化完成");
        }
    
        /**
         * @Author: 老乡
         * @Date: 2020/6/13 21:51
         * @Describe: 支付实现类,具体支付类别由子类实现,消除if else分支,接入其他支付时不违反开闭原则
         */
        @Override
        public Result toPayCore(PayCoreDTO payCoreDTO) {
            return payStrategy(payCoreDTO);
        }
    
        public abstract Result payStrategy(PayCoreDTO payCoreDTO);
    
        //获取初始化渠道key
        public abstract Map<Integer, PayCoreService> channelInit();
    }
    

    定义两个子类继承它,key就用枚举会优雅一些

  3. @Slf4j
    @Service
    public class AliPayCoreStrategy extends PayCoreStrategyAbstract {
        
        @Override
        public Result payStrategy(PayCoreDTO payCoreDTO) {
    		//阿里支付业务
            return Result.errorMessage("支付异常");
        }
        
        //此处因为可能有多种支付方式,所以返回Map,如果只有一种就返回一个key就可以
        @Override
        public Map<Integer, PayCoreService> channelInit() {
            Map<Integer, PayCoreService> map = new HashMap<>();
            map.put(PayChannelEnum.ALI_APP.getChannel(), this);
            map.put(PayChannelEnum.ALI_H5.getChannel(), this);
            return map;
        }
    }
    

    一个Service,一个abstract,两个继承类都定义好了,如何调用呢?

  4. public interface PayCoreManager {
        /**
         * @Author: 老乡
         * @Describe: pay
         */
        Result payChoose(PayCoreDTO payCoreDTO);
    
    @Service
    public class PayCoreManagerImpl implements PayCoreManager {
    
        @Override
        public Result payChoose(PayCoreDTO payCoreDTO) {
            //从静态map支付类别中取出对应的支付类别,对应的实例PayStrategy,若没有应检查是否对应
            //不同的类别获取到不同的支付实例
            PayCoreService p = PayCoreStrategyAbstract.strategyMap.get(payCoreDTO.getPayChannel());
            if (p == null) {
                return Result.errorMessage("payType is not valid");
            }
            //调用支付封装接口
            return p.toPayCore(payCoreDTO);
        }
    

    可以看到传入payChannel,直接通过抽象类获取静态变量Map.get取到实例进行调用即可

    函数式接口式
    这种方式主要是对@FunctionalInterface的应用设计,在设计过程中有一些细节我会和大家说到,大家在看的过程中注意一下,并且这个看起来也是比较优雅,较前几种方式都复杂一些的设计,上干货代码示例

    首先定义一个函数式接口,里面有个执行方法execute,注意函数式接口这里只能有一个方法,参数根据业务设置一个即可
     

    @FunctionalInterface
    public interface FruitFunctionExecute {
        void execute(FruitDTO fruitDTO);
    }
    

    重点来了,定义一个抽象类BaseFruitAbstract实现FruitService,继承BaseHelper,此类可以作为基类提供统一的方法处理
    该抽象类实现FruitService接口postEat
    该抽象类实现RedService接口postAddRed,这个没有特殊理由哈,就是表演一下其他的关联业务可以在这里实现,并且此实现方法后面会被map搜集到(有应用的时候可以用此方式)

    此抽象类实现函数式接口方法execute,此执行器可调用beforeExecute,doExecute和afterExecute
    其他的东西我们后面继续说
     

    
    @Slf4j
    public abstract class BaseFruitAbstract extends BaseHelper implements FruitService, RedService {
        //子类map集合 可以直接类名.调用,也可以通过注入接口调用
        public static Map<String, FruitService> FRUIT_MAP = new ConcurrentHashMap<>();
        //函数式接口业务map集合 不用static修饰,因为上层有FRUIT_MAP传过来指定实例,此map可以拿到上下文实例,并找到子类执行器
        protected Map<String, FruitFunctionExecute> BUSINESS_MAP = new ConcurrentHashMap<>();
        @Autowired
        protected LxLoveDao lxLoveDao;
    
        /**
         * 初始化实例
         */
        @PostConstruct
        public void init(){
            setFruitMap(FRUIT_MAP);
            //此处的fruitDTO是函数式接口的入参表示
            BUSINESS_MAP.put(BusinessTypeEnum.WAIT_SUBMIT.getValue(), (fruitDTO)-> postEat(fruitDTO));
            BUSINESS_MAP.put(BusinessTypeEnum.WAIT_SURE.getValue(), (fruitDTO)-> postAddRed(fruitDTO));
        }
    
        /**
         * 函数式接口执行器
         */
        @Override
        public void execute(FruitDTO fruitDTO) {
            System.out.println("执行吧朋友们");
            beforeExecute();
            doExecute(fruitDTO);
            afterExecute();
        }
    
    
        private void beforeExecute(){
    
        }
        private void doExecute(FruitDTO fruitDTO){
            FruitFunctionExecute service = BUSINESS_MAP.get(fruitDTO.getBusinessType());
            service.execute(fruitDTO);
        }
        private void afterExecute(){
    
        }
    
        @Override
        public void postEat(FruitDTO fruitDTO) {
            System.out.println("BaseFruitAbstract postEat");
        }
    
        //RedService接口实现,可用于相关调用
        @Override
        public void postAddRed(FruitDTO fruitDTO) {
            System.out.println("BaseFruitAbstract postAddRed");
        }
    
        //--------------------------------------------------------------抽象方法----------------------------------------------------------
        //如果这个map是子类的,则延迟子类获取实例
        protected abstract void setFruitMap(Map<String, FruitService> map);
    
    }
    

  5. 定义一个苹果的处理器AppleHandler,继承抽象类BaseFruitAbstract,可以重写其方法,或者实现其抽象方法
  6. @Component
    public class AppleHandler extends BaseFruitAbstract {
        @Override
        public void postEat(FruitDTO fruitDTO) {
            System.out.println("AppleHandler");
            lxLoveDao.getOne(1L);
        }
        @Override
        protected void setFruitMap(Map<String, FruitService> map) {
            //key可以通过枚举控制
            map.put(FruitTypeEnum.APPLE.getValue(), this);
        }
    }
    

    同理

  7. @Component
    public class OrangeHandler extends BaseFruitAbstract {
        @Override
        public void postEat(FruitDTO fruitDTO) {
            System.out.println("OrangeHandler");
        }
        @Override
        protected void setFruitMap(Map<String, FruitService> map) {
            map.put(FruitTypeEnum.ORANGE.getValue(), this);
        }
    }
    

    重点来了,继续说抽象类BaseFruitAbstract,最上面定义了两个Map,大家也看到注释了是吧,其通过@PostConstruct在启动时期初始化,第一个FRUIT_MAP是外层要使用的map,此map的key可以自定义业务枚举管理,value为子类实例,所以通过setFruitMap延迟到子类中通过this设置进去即可;
    将其设置为static对当前类生效

    重点2,第二个BUSINESS_MAP是业务map,可以看到put的key也是可以通过枚举管理,value就是FruitFunctionExecute函数式接口,所以定义(fruitDTO)-> postEat(fruitDTO)表示要执行的函数式接口方法,此处的fruitDTO就是FruitFunctionExecute中execute的入参,可以看到此map也将RedService的实现方法加进去维护了起来
    注意BUSINESS_MAP不能设置为static让其属于类,因为在调用层会先通过FRUIT_MAP获取实例并调用已经拿到了子类实例,此处可以直接拿到上下文实例,如果定义为static属于类就拿不到了

    调用方式如下
     

       @GetMapping("/t1")
       public void t1(String fruitType, String businessType){
           FruitService fruitService = BaseFruitAbstract.FRUIT_MAP.get(fruitType);
           FruitDTO fruitDTO = new FruitDTO();
           fruitDTO.setBusinessType(businessType);
           fruitService.execute(fruitDTO);
           System.out.println("execute 完毕");
           fruitService.postEat(fruitDTO);
       }
    

    可以直接通过BaseFruitAbstract的FRUIT_MAP根据枚举拿到实例,然后调用FruitFunctionExecute的execute方法即可,这样走的就是BUSINESS_MAP中维护的方法postEat或postAddRed
    如果通过实例直接调用postEat,则和BUSINESS_MAP无关,即和函数式接口无关,直接走子类方法
    这是类关系

    在这里插入图片描述

     下面是请求后的日志

  8. 在这里插入图片描述

     这时大家有疑问了,为什么直接调用postEat也可以,还要函数式接口干嘛?
    因为首先它比较优雅,并且呢如果BUSINESS_MAP搜集的方法并不是外部实现的方法,而是内部根据不同枚举策略封装的方法,那外部就只能通过函数式接口的方法execute来执行了
    并且controller层不一定知道要调用谁,而只是传枚举驱动后面的调用
    对比

  9. 方式优势劣势
    注解分层,逼王,用到多种设计模式,反射等每一个业务都要定义自己的注解,注解上无法用枚举.getValue来指定
    拼接式简单和子类名耦合,需要自己按类名拼接,不够优雅
    实现接口式分层,优雅调用链可能稍长一点
    函数式接口式分层,优雅,逼王,两种枚举控制Map,多中调用方式,扩展性好设计稍复杂一点

    推荐使用234,其实函数接口式就是实现接口式的升级版

    以上就是如何抽象扩展的代码设计示例,大家快快实践起来!希望能够误导大家哈哈

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值