设计模式最佳实践探索—策略模式

发表于 1年以前  | 总阅读数:228 次

根据不同的应用场景与意图,设计模式主要分为创建型模式、结构型模式和行为型模式三类。本文主要探索行为型模式中的策略模式如何更好地应用于实践中。

前言

在软件开发的过程中,需求的多变性几乎是不可避免的,而作为一名服务端开发人员,我们所设计的程序应尽可能支持从技术侧能够快速、稳健且低成本地响应纷繁多变的业务需求,从而推进业务小步快跑、快速迭代。设计模式正是前辈们针对不同场景下不同类型的问题,所沉淀下来的一套程序设计思想与解决方案,用来提高代码可复用性、可维护性、可读性、稳健性以及安全性等。下面是设计模式的祖师爷GoF(Gang of Four,四人帮)的合影,感受一下大佬的气质~

灵活应用设计模式不仅可以使程序本身具有更好的健壮性、易修改性和可扩展性,同时它使得编程变得工程化,对于多人协作的大型项目,能够降低维护成本、提升多人协作效率。根据不同的应用场景与意图,设计模式主要分为三类,分别为创建型模式、结构型模式和行为型模式。本文主要探索行为型模式中的策略模式如何更好地应用于实践中。

使用场景

策略模式属于对象的行为模式,其用意是针对一组可替换的算法,将每一个算法封装到具有共同接口的独立的类中,使得算法可以在不影响到客户端(算法的调用方)的情况下发生变化,使用策略模式可以将算法的定义与使用隔离开来,保证类的单一职责原则,使得程序整体符合开闭原则。

以手淘中商详页的店铺卡片为例,店铺卡片主要包含店铺名称、店铺logo、店铺类型以及店铺等级等信息,其中不同店铺类型的店铺等级计算逻辑是不同的,为了获取店铺等级,可以采用如下所示代码:


 if (Objects.equals("淘宝", shopType)) {
   // 淘宝店铺等级计算逻辑
   // return 店铺等级;
 } else if (Objects.equals("天猫", shopType)) {
   // 天猫店铺等级计算逻辑
   // return 店铺等级
 } else if (Objects.equals("淘特", shopType)) {
   // 淘特店铺等级计算逻辑
   // return 店铺等级
 } else {
   //  ...
 }

这种写法虽然实现简单,但使得各类店铺等级计算逻辑与程序其他逻辑相耦合,未来如果要对其中一种计算逻辑进行更改或者新增加一种计算逻辑,将不得不对原有代码进行更改,违背了OOP的单一职责原则与开闭原则,让代码的维护变得困难。若项目本身比较复杂,去改动项目原有的逻辑是一件非常耗时又风险巨大的事情。此时我们可以采取策略模式来处理,将不同类型的店铺等级计算逻辑封装到具有共同接口又互相独立的类中,其核心类图如下所示:

这样一来,程序便具有了良好的可扩展性与易修改性,若想增加一种新的店铺等级计算逻辑,则可将其对应的等级计算逻辑单独封装成ShopRankHandler接口的实现类即可,同样的,若想对其中一种策略的实现进行更改,在相应的实现类中进行更改即可,而不用侵入原有代码中去开发。

最佳实践探索

本节仍以店铺等级的处理逻辑为例,探索策略模式的最佳实践。当使用策略模式的时候,会将一系列算法用具有相同接口的策略类封装起来,客户端想调用某一具体算法,则可分为两个步骤:1、某一具体策略类对象的获取;2、调用策略类中封装的算法。比如客户端接受到的店铺类型为“天猫”,则首先需要获取TmShopRankHandleImpl类对象,然后调用其中的算法进行天猫店铺等级的计算。在上述两个步骤中,步骤2是依赖于步骤1的,当步骤1完成之后,步骤2也随之完成,因此上述步骤1成为整个策略模式中的关键。

下面列举几种策略模式的实现方式,其区别主要在于具体策略类对象获取的方式不同,对其优缺点进行分析,并探索其最佳实践。

暴力法

  1. 店铺等级计算策略接口
public interface ShopRankHandler {
    /**
    * 计算店铺等级
    * @return 店铺等级
    */

    public String calculate();
}

2 . 各类型店铺等级计算策略实现类

淘宝店


public class TbShopRankHandleImpl implements ShopRankHandler{
    @Override
    public String calculate() {
        // 具体计算逻辑
        return rank;
    }
}

天猫店


public class TmShopRankHandleImpl implements ShopRankHandler{
    @Override
    public String calculate() {
        // 具体计算逻辑
        return rank;
    }
}

淘特店


public class TtShopRankHandleImpl implements ShopRankHandler{
    @Override
    public String calculate() {
        // 具体计算逻辑
        return rank;
    }
}

3 . 客户端调用

  // 根据参数调用对应的算法计算店铺等级
public String acqurireShopRank(String shopType) {
    String rank = StringUtil.EMPTY_STRING;
    if (Objects.equals("淘宝", shopType)) {
        // 获取淘宝店铺等级计算策略类
        ShopRankHandler shopRankHandler = new TbShopRankHandleImpl();
        // 计算店铺等级
        rank = shopRankHandler.calculate();
    } else if (Objects.equals("天猫", shopType)) {
        // 获取天猫店铺等级计算策略类
        ShopRankHandler shopRankHandler = new TmShopRankHandleImpl();
        // 计算店铺等级
        rank = shopRankHandler.calculate();
    } else if (Objects.equals("淘特", shopType)) {
        // 获取淘特店铺等级计算策略类
        ShopRankHandler shopRankHandler = new TtShopRankHandleImpl();
        // 计算店铺等级
        rank = shopRankHandler.calculate();
    } else {
        //  ...
    }
    return rank;
}
  • 效果

至此,当我们需要新增策略类时,需要做的改动如下:

  1. 新建策略类并实现策略接口
  2. 改动客户端的if else分支
  • 优点

  1. 将店铺等级计算逻辑单独进行封装,使其与程序其他逻辑解耦,具有良好的扩展性。
  2. 实现简单,易于理解。
  • 缺点

客户端与策略类仍存在耦合,当需要增加一种新类型店铺时,除了需要增加新的店铺等级计算策略类,客户端需要改动if else分支,不符合开闭原则。

第一次迭代(枚举+简单工厂)

有没有什么方法能使客户端与具体的策略实现类彻底进行解耦,使得客户端对策略类的扩展实现“零”感知?在互联网领域,没有什么问题是加一层解决不了的,我们可以在客户端与众多的策略类之间加入工厂来进行隔离,使得客户端只依赖工厂,而具体的策略类由工厂负责产生,使得客户端与策略类解耦,具体实现如下所示:

  1. 枚举类

public enum ShopTypeEnum {
    TAOBAO("A","淘宝"),
    TMALL("B", "天猫"),
    TAOTE("C", "淘特");

    @Getter
    private String type;
    @Getter
    private String desc;
    ShopTypeEnum(String type, String des) {
        this.type = type;
        this.desc = des;
    }
}

2 . 店铺等级计算接口


public interface ShopRankHandler {
    /**
    * 计算店铺等级
    * @return 店铺等级
    */

    String calculate();
}

3 . 各类型店铺等级计算策略实现类

淘宝店


public class TbShopRankHandleImpl implements ShopRankHandler{   
    @Override
    public String calculate() {
        // 具体计算逻辑
        return rank;
    }
}

天猫店


public class TmShopRankHandleImpl implements ShopRankHandler{
    @Override
    public String calculate() {
        // 具体计算逻辑
        return rank;
    }
}

淘特店


public class TtShopRankHandleImpl implements ShopRankHandler{
    @Override
    public String calculate() {
        // 具体计算逻辑
        return rank;
    }
}

4 . 策略工厂类


@Component
public class ShopRankHandlerFactory {

    // 初始化策略beans
    private static final Map<String, ShopRankHandler> GET_SHOP_RANK_STRATEGY_MAP = ImmutableMap.<String, ShopRankHandler>builder()
        .put(ShopTypeEnum.TAOBAO.getType(), new TbShopRankHandleImpl())
        .put(ShopTypeEnum.TMALL.getType(), new TmShopRankHandleImpl())
        .put(ShopTypeEnum.TAOTE.getType(), new TtShopRankHandleImpl())
        ;

    /**
     * 根据店铺类型获取对应的获取店铺卡片实现类
     *
     * @param shopType 店铺类型
     * @return 店铺类型对应的获取店铺卡片实现类
     */
    public ShopRankHandler getStrategy(String shopType) {
        return GET_SHOP_RANK_STRATEGY_MAP.get(shopType);
    }

}

5 . 客户端调用


@Resource
ShopRankHandlerFactory shopRankHandlerFactory;
// 根据参数调用对应的算法计算店铺等级
public String acqurireShopRank(String shopType) {
    ShopRankHandler shopRankHandler = shopRankHandlerFactory.getStrategy(shopType);
    return Optional.ofNullable(shopRankHandler)
        .map(shopRankHandle -> shopRankHandle.calculate())
        .orElse(StringUtil.EMPTY_STRING);
}
  • 效果

至此,当我们需要新增策略类时,需要做的改动如下:

  1. 新建策略类并实现策略接口
  2. 增加枚举类型
  3. 工厂类中初始化时增加新的策略类

相比上一种方式,策略类与客户端进行解耦,无需更改客户端的代码。

  • 优点

将客户端与策略类进行解耦,客户端只面向策略接口进行编程,对具体策略类的变化(更改、增删)完全无感知,符合开闭原则。

  • 缺点

需要引入额外的工厂类,使系统结构变得复杂。

当新加入策略类时,工厂类中初始化策略的部分仍然需要改动。

第二次迭代 (利用Spring框架初始化策略beans)

在枚举+简单工厂实现的方式中,利用简单工厂将客户端与具体的策略类实现进行了解耦,但工厂类中初始化策略beans的部分仍然与具体策略类存在耦合,为了进一步解耦,我们可以利用Spring框架中的InitializingBean接口与ApplicationContextAware接口来实现策略beans的自动装配。InitializingBean接口中的afterPropertiesSet()方法在类的实例化过程当中执行,也就是说,当客户端完成注入ShopRankHandlerFactory工厂类实例的时候,afterPropertiesSet()也已经执行完成。因此我们可以通过重写afterPropertiesSet()方法,在其中利用getBeansOfType()方法来获取到策略接口的所有实现类,并存于Map容器之中,达到工厂类与具体的策略类解耦的目的。相比于上一种实现方式,需要改动的代码如下:

1 . 店铺等级计算接口


public interface ShopRankHandler {
    /**
    * 获取店铺类型的方法,接口的实现类需要根据各自的枚举类型来实现,后面就不贴出实现类的代码
    * @return 店铺等级
    */
    String getType();
    /**
    * 计算店铺等级
    * @return 店铺等级
    */
    String calculate();
}

2 . 策略工厂类

  @Component
public class ShopRankHandlerFactory implements InitializingBean, ApplicationContextAware {

    private ApplicationContext applicationContext;
    /**
     * 策略实例容器
     */
    private Map<String, ShopRankHandler> GET_SHOP_RANK_STRATEGY_MAP;

    /**
     * 根据店铺类型获取对应的获取店铺卡片实现类
     *
     * @param shopType 店铺类型
     * @return 店铺类型对应的获取店铺卡片实现类
     */
    public ShopRankHandler getStrategy(String shopType) {
        return GET_SHOP_RANK_STRATEGY_MAP.get(shopType);
    }

    @Override
    public void afterPropertiesSet() {
        Map<String, ShopRankHandler> beansOfType = applicationContext.getBeansOfType(ShopRankHandler.class);

        GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType)
                            .map(beansOfTypeMap -> beansOfTypeMap.values().stream()
                                    .filter(shopRankHandle -> StringUtils.isNotEmpty(shopRankHandle.getType()))
                                    .collect(Collectors.toMap(ShopRankHandler::getType, Function.identity())))
                            .orElse(new HashMap<>(8));
    }

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

}
  • 效果

至此,当我们需要新增策略类时,需要做的改动如下:

  1. 新建策略类并实现策略接口
  2. 增加枚举类型

相比于上一种方式,可以省略工厂类在初始化策略beans时要增加新的策略类这一步骤。

  • 优点

借助Spring框架完成策略beans的自动装配,使得策略工厂类与具体的策略类进一步解耦。

  • 缺点

需要借助Spring框架来完成,不过在Spring框架应用如此广泛的今天,这个缺点可以忽略不计。

▐ 最终迭代(利用泛型进一步提高策略工厂复用性)

经过上面两次迭代以后,策略模式的实现已经变得非常方便,当需求发生改变的时候,我们再也不用手忙脚乱了,只需要关注新增或者变化的策略类就好,而不用侵入原有逻辑去开发。但是还有没有改进的空间呢?

设想一下有一个新业务同样需要策略模式来实现,如果为其重新写一个策略工厂类,整个策略工厂类中除了新的策略接口外,其他代码均与之前的策略工厂相同,出现了大量重复代码,这是我们所不能忍受的。为了最大程度避免重复代码的出现,我们可以使用泛型将策略工厂类中的策略接口参数化,使其变得更灵活,从而提高其的复用性。

理论存在,实践开始!代码示意如下:

1 . 定义泛型接口


public interface GenericInterface<E> {
     E getType();
}

2 . 定义策略接口继承泛型接口


public interface StrategyInterfaceA extends GenericInterface<String>{

    String handle();
}
public interface StrategyInterfaceB extends GenericInterface<Integer>{

    String handle();
}
public interface StrategyInterfaceC extends GenericInterface<Long>{

    String handle();
}

3 . 实现泛型策略工厂

  public class HandlerFactory<E, T extends GenericInterface<E>> implements InitializingBean, ApplicationContextAware {
    private ApplicationContext applicationContext;
    /**
     * 泛型策略接口类型
     */
    private Class<T> strategyInterfaceType;

    /**
     * java泛型只存在于编译期,无法通过例如T.class的方式在运行时获取其类信息
     * 因此利用构造函数传入具体的策略类型class对象为getBeansOfType()方法
     * 提供参数
     *
     * @param strategyInterfaceType 要传入的策略接口类型
     */
    public HandlerFactory(Class<T> strategyInterfaceType) {
        this.strategyInterfaceType = strategyInterfaceType;
    }
    /**
     * 策略实例容器
     */
    private Map<E, T> GET_SHOP_RANK_STRATEGY_MAP;
    /**
     * 根据不同参数类型获取对应的接口实现类
     *
     * @param type 参数类型
     * @return 参数类型对应的接口实现类
     */
    public T getStrategy(E type) {
        return GET_SHOP_RANK_STRATEGY_MAP.get(type);
    }

    @Override
    public void afterPropertiesSet() {
        Map<String, T> beansOfType = applicationContext.getBeansOfType(strategyInterfaceType);
        System.out.println(beansOfType);

        GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType)
                .map(beansOfTypeMap -> beansOfTypeMap.values().stream()
                        .filter(strategy -> StringUtils.isNotEmpty(strategy.getType().toString()))
                        .collect(Collectors.toMap(strategy -> strategy.getType(), Function.identity())))
                .orElse(new HashMap<>(8));
        System.out.println(GET_SHOP_RANK_STRATEGY_MAP);
    }

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

4 . 有了上述泛型策略工厂类,当我们需要新建一个策略工厂类的时候,只需要利用其构造函数传入相应的策略接口即可。生成StrategyInterfaceA、StrategyInterfaceB与StrategyInterfaceC接口的策略工厂如下:


public class BeanConfig {
    @Bean
    public HandlerFactory<String, StrategyInterfaceA> strategyInterfaceAFactory(){
        return new HandlerFactory<>(StrategyInterfaceA.class);
    }
    @Bean
    public HandlerFactory<Integer, StrategyInterfaceB> strategyInterfaceBFactory(){
        return new HandlerFactory<>(StrategyInterfaceB.class);
    }
    @Bean
    public HandlerFactory<Long, StrategyInterfaceC> strategyInterfaceCFactory(){
        return new HandlerFactory<>(StrategyInterfaceC.class);
    }

}
  • 效果

此时,若想新建一个策略工厂,则只需将策略接口作为参数传入泛型策略工厂即可,无需再写重复的样板代码,策略工厂的复用性大大提高,也大大提高了我们的开发效率。

  • 优点

将策略接口类型参数化,策略工厂不受接口类型限制,成为任意接口的策略工厂。

  • 缺点

系统的抽象程度、复杂度变高,不利于直观理解。

结束语

学习设计模式,关键是学习设计思想,不能简单地生搬硬套,灵活正确地应用设计模式可以让我们在开发中取得事半功倍的效果,但也不能为了使用设计模式而过度设计,要合理平衡设计的复杂度和灵活性。

本文是对策略模式最佳实践的一次探索,不一定是事实上的最佳实践,欢迎大家指正与讨论。

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Ovoy1CnN92xCYHbbO4H_DQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236883次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6960次阅读
 目录