如何通过《重构》一书优化代码

一、重构是什么

1.1 重构的定义

书中对重构的定义描述如下:
对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。通过重构,开发人员可以使代码更加简洁、清晰、易于维护,这对于长期项目的稳定性和开发效率至关重要。

1.2 重构与性能优化的区别

很多人会把重构和性能优化混淆,然而它俩并不是相同的概念,这两个概念的相似之处与不同之处如下:
●相同点:两者都需要修改代码,两者都不会改变程序的整体功能
●不同点:重构是为了让代码“更容易理解,更易于修改”(提高代码的可读性以及扩展性),这可能让程序变快,也可能让程序变慢。而性能优化的目的是只是让程序运行得更快,这反而有可能会降低代码的可读性以及扩展性。

二、为什么要重构

在书中,作者详细探讨了重构的重要性和必要性。重构不仅是代码优化的手段,更是提升软件质量、可维护性和开发效率的关键实践。通过重构,开发团队可以更好地应对快速变更的需求,降低技术债务,并保持代码库的健康状态。以下是书中概括的重构的四个关键理由。

2.1 改进软件的设计

如果没有重构,程序的内部设计(或架构)会逐渐腐化,代码库会快速变为人人嫌弃的屎山。当这个代码库上有新需求时,程序员往往需要在腐败的代码库中浪费大量的时间。而经常性的重构则是减缓代码腐化过程的有效手段。
设计不佳的程序通常需要更多代码来完成相同的功能,常见原因是重复代码。改进设计的重要方向之一是消除重复代码。虽然减少代码量对程序的资源占用影响不大,但它能显著简化未来的修改。代码越多,理解和正确修改的难度就越大。消除重复代码可以确保所有事物和行为在代码中只表述一次,这是优秀设计的核心。

2.2 使软件更容易理解

编程的实质就是程序员告诉计算机需要做什么事情,然而代码的读者不仅仅是计算机,还有未来的开发人员(包括未来的自己)。因此,我们在写代码的时候需要下意识让写出的代码更容易被人理解。通过重构,让代码可以清晰地表达程序员的意图,避免开发者在未来因为不理解代码而耗费大量时间。

2.3 帮助找到bug

在重构程序的过程中,自己能够更加深入地理解代码的所做所为,在完全理清楚代码结构的同时,也能使得自己更容易地发现一些隐秘的bug。

2.4 提高编程速度

大部分人总是认为在重构上花时间会降低自己的开发速度,然而重构能够改善代码的内部设计,增加软件的耐久性,从而可以更长时间地保持开发的快速。

三、何时需要重构

营地法则:保证你离开时的代码库一定比来时更健康
《重构》一书中,作者总结了多个需要重构的时机,比如在添加新功能的时候对现有代码库进行重构,或者在cr的时候帮助他人进行重构,但重构归根结底其实就是为了效率。重构的意义不在于把代码库打磨得闪闪发光,而是纯粹经济角度出发的考量。重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。如果一块很烂的代码被隐藏在一个API之下,但此时这段代码并不需要被修改,那就不去重构它。

四、哪些代码需要重构(代码的坏味道)

书中一共总结了24种代码坏味道,这里介绍几种有代表性的坏味道,这些坏味道的确在开发中经常能够见到。

4.1 神秘命名

在开发中,使用有意义的命名有助于提高代码的可读性和可维护性。然而,有时候开发者可能会选择一些含糊或不清晰的命名,这种现象被称为“神秘命名”(Mystery Name)。神秘命名可以说是经常遇到的一些坏味道了,这些命名包括方法名称、字段名称、变量名称等。需要采用重构手段“改变函数声明”、“变量改名”、“字段改名”将这些程序元素改为能清晰地表明自己的功能和用法的好名字。

4.2 重复代码

重复代码是编程中最常见的坏味道之一,指的是在代码的不同部分中存在相同或相似的代码片段。重复代码会导致代码膨胀,使得维护和修改变得困难且容易出错。每当业务逻辑需要变更时,开发者必须小心翼翼地确保所有相关的重复代码都被同步更新,增加了工作量和出错的风险。这个时候需要做的就是采用重构手段“提炼函数”将所有重复的代码提炼为一个独立的方法,未来要修改业务逻辑时仅需要修改提炼出的函数即可。

4.3 过长函数

过长函数是编程中常见的坏味道之一,指的是函数或方法包含过多的代码行和逻辑,导致其复杂难懂,难以维护。长函数通常会承担太多职责,这种情况会导致代码的可读性降低,并增加了修改和调试的难度。同样,在《阿里巴巴开发规约》中,推荐的方法体长度不能超过80行。可以采用重构手段“提炼函数”一步步将长方法变短,提炼的时候可以遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立方法中,并以其用途命名。

4.4 过长参数列表

过长参数列表指的是方法或函数的参数列表包含过多的参数。这种情况会导致方法调用时变得复杂和难以理解,增加了代码的维护难度。过长的参数列表通常是职责不清或缺乏适当对象封装的结果。采用重构手法“以查询取代参数”、“引入参数对象”等重构手法缩短参数列表。

4.5 发散式变化

发散式变化指的是一个类由于承担了过多的职责,这个类总是会因为不同的原因发生不同的修改,这样一来,类的维护变得复杂且容易出错,违背了单一职责原则(Single Responsibility Principle)。面对发散式变化,可以首先采用重构手段“搬移函数”将不同的处理逻辑分开,再采用重构手段“提炼类”将类进行拆分,让拆分后的每个类符合单一职责原则。

4.6 霰弹式修改

霰弹式修改指的是一个简单的修改需要在许多不同的地进行小的修改。霰弹式修改正好与发散式变化相反,发散式变化指的是一个类承担了过多的指责,而霰弹式修改则是强调同一个职责分散在多个类或模块中。这导致每次需求变更时都需要修改许多类和方法。霰弹式修改不仅增加了维护的复杂性和难度,还容易引入错误。这个时候,可以采用重构手段“搬移函数”、“搬移字段”等将需要运行同一个职责的代码放进一个模块中。

4.7 重复的switch

重复的 switch(或 if-else)指的是在代码的多个地方重复使用相同的 switch 或 if-else 逻辑。这种坏味道会导致代码的可维护性和可扩展性变差,因为每次需要添加或修改逻辑时,都必须在所有使用 switch 语句的地方进行同步修改,不符合面向对象开闭原则(The Open/Closed Principle)。重复的 switch 逻辑不仅增加了代码量,降低扩展性,在修改时极易引入bug。想要避免这种坏味道,可以采用重构手段“以多态取代条件表达式”,通过引入继承或策略模式,将重复的switch从代码中去除。

五、重构实践:重构自己的代码

在学习了《重构:改善现有代码的设计》之后,我也是迅速回顾了自己之前写的一些尚未上线的代码,当然也是发现了不少“坏味道”。在这里,我将展示一个简单的代码片段,演示在实际开发过程中如何识别代码中的异味并进行重构。

5.1 重构前的代码

代码背景:现在需要为某个商家添加一个促销活动管理页面,该页面能够管理不同类型的促销活动,比如会员活动,满赠活动等。现在为该页面提供一个活动管理类,该类具备两个功能,一是展示不同类型的活动,二是增加不同类型的活动(为了简单就不演示删除和修改方法),代码实现如下:

public class PromotionActivityManager {
    
    @Resource
    private MemberActivityDAO memberActivityDAO;

    @Resource
    private GiftActivityDAO giftActivityDAO;

    /**
     * 根据活动类型与卖家Id分页查询活动列表
     *
     * @param type 活动类型
     * @param sellerId 卖家Id
     * @param pageNo 页号
     * @param PageSize 页大小     
     * @return 查询出的活动列表
     */
    public List<PromotionActivityDTO> queryPromotionActivityList(String type, Long sellerId, Integer pageNo, Integer PageSize) {
        // 参数校验
        AssertUtils.checkNotBlank(type, "活动类型不能为空");
        AssertUtils.checkNotNull(sellerId, "卖家Id不能为空");
        AssertUtils.checkNotNull(pageNo, "页号不能为空");
        AssertUtils.checkNotNull(pageSize, "页大小不能为空");

        List<PromotionActivityDTO> list = Lists.newArrayList();
        switch (type) {
            case "会员活动" :
                // 封装查询参数
                MemberActivityParam parma = new MemberActivityParam();
                param.setSellerId(sellerId);
                param.setPageNo(pageNo);
                param.setPageSize(pageSize);
                // 数据库查询
                List<MemberActivityDO> list1 = memberActivityDAO.selectByParam(param);
                // 组装返回结果
                for (MemberActivityDO activityDO : list1) {
                    PromotionActivityDTO activityDTO = new PromotionActivityDTO();
                    activityDTO.setId(activityDO.getId());
                    activityDTO.setName(activityDO.getName());
                    activityDTO.setActivityInfo(activityDTO.getMemberInfo());
                    list.add(activityDTO);
                }
                break;
            case "礼赠活动" :
                // 封装查询参数
                GiftActivityParam parma = new GiftActivityParam();
                param.setSellerId(sellerId);
                param.setPageNo(pageNo);
                param.setPageSize(pageSize);
                // 数据库查询
                List<GiftActivityDO> list1 = giftActivityDAO.selectByParam(param);
                // 组装返回结果
                for (GiftActivityDO activityDO : list1) {
                    PromotionActivityDTO activityDTO = new PromotionActivityDTO();
                    activityDTO.setId(activityDO.getId());
                    activityDTO.setName(activityDO.getName());
                    activityDTO.setActivityInfo(activityDTO.getGiftInfo());
                    list.add(activityDTO);
                }
                break;
            default :
                log.error("无效的促销活动类型");  
                throw new RuntimeException("无效的促销活动类型");
        }
        
        return list;
    }

    /**
     * 添加促销活动信息
     *
     * @param type 活动类型
     * @param sellerId 卖家Id
     * @param activityName 活动名称
     * @param activityInfo 活动信息     
     * @return 活动是否添加成功
     */
    public Boolean addPromotionActivity(String Type, Long sellerId, Long activityName, String activityInfo) {
        // 参数校验
        AssertUtils.checkNotBlank(type, "活动类型不能为空");
        AssertUtils.checkNotNull(sellerId, "卖家Id不能为空");
        AssertUtils.checkNotNull(activityName, "活动名称不能为空");
        AssertUtils.checkNotNull(activityInfo, "活动信息不能为空");

        Boolean result = Boolean.FALSE;
        switch (type) {
            case "会员活动" :  
                // 构建会员活动DO
                MemberActivityDO memberActivityDO = new MemberActivityDO();
                memberActivityDO.setSellerId(sellerId);
                memberActivityDO.setName(activityName);
                memberActivityDO.setMemberInfo(activityInfo);
                result = memberActivityDAO.add(memberActivityDO) > 0;
                break;
            case "礼赠活动" :
                // 构建礼赠活动DO
                MemberActivityDO memberActivityDO = new MemberActivityDO();
                memberActivityDO.setSellerId(sellerId);
                memberActivityDO.setName(activityName);
                memberActivityDO.setGiftInfo(activityInfo);
                result = memberActivityDAO.add(memberActivityDO) > 0;
                break;
            default :
                log.error("无效的促销活动类型");
                throw new RuntimeException("无效的促销活动类型"); 
                
        return result;  
    }
}

5.2 识别代码的坏味道并开始重构

这段代码充斥着许多“坏味道”。现在,我们具体来分析一下代码中存在的这些坏味道,并讨论如何针对这些问题进行重构。
●过长参数列表
在PromotionActivityManager类中提供了展示活动以及增加活动两个方法,但这两个方法的参数列表似乎有点太长了,其他人在调用这两个方法的时候可能总是会因为过于复杂的参数导致调用出现问题。因此,采用“引入参数对象”对PromotionActivityManager类进行重构。引入两个类PromotionActivityListQuery以及PromotionActivityAddRequest分别作为两个方法的入参,并且将参数校验放在两个类中,这两个类的定义如下:

PromotionActivityAddRequest分别作为两个方法的入参,并且将参数校验放在两个类中,这两个类的定义如下
@Data
public class PromotionActivityListQuery {

    /**
     * 活动类型 
     */
    private String type;

    /**
     * 商家id
     */
    private Long sellerId;

    /**
     * 页号 
     */
    private Integer pageNo;

    /**
     * 页大小 
     */
    private Integer PageSize;

    /**
     * 对自身进行参数校验
     */
    public void checkParams() {
        AssertUtils.checkNotBlank(type, "活动类型不能为空");
        AssertUtils.checkNotNull(sellerId, "卖家Id不能为空");
        AssertUtils.checkNotNull(pageNo, "页号不能为空");
        AssertUtils.checkNotNull(pageSize, "页大小不能为空");
    }
}

@Data
public class PromotionActivityAddRequest {

    /**
     * 活动类型 
     */
    private String type;

    /**
     * 商家id
     */
    private Long sellerId;

    /**
     * 活动名称
     */
    private Integer activityName;

    /**
     * 活动信息
     */
    private Integer activityInfo;

    /**
     * 对自身进行参数校验
     */
    public void checkParams() {
        AssertUtils.checkNotBlank(type, "活动类型不能为空");
        AssertUtils.checkNotNull(sellerId, "卖家Id不能为空");
        AssertUtils.checkNotNull(activityName, "活动名称不能为空");
        AssertUtils.checkNotNull(activityInfo, "活动信息不能为空");
    }
}


神秘命名
在PromotionActivityManager类中可以看到多个不明所以的变量命名,比如两个方法中的list、list1、activityDO、activityDTO等,需要采用“变量命名”将这些变量改为能一眼看出用法的名字。

重复的switch
在PromotionActivityManager类的两个方法中,含有重复判断条件的switch,坏味道有点过于刺鼻了,因此,采用重构手段“以多态代替条件表达式”,对这个类进行重构。在代码中引入活动策略,将不同的活动类型交给不同的活动策略进行处理,这里篇幅所限,只展示一种活动策略类。引入的策略类如下:

public interface PromotionActivityStrategy {

    /**
     * 根据请求分页查询促销活动列表
     *
     * @param query 活动列表查询请求
     * @return 促销活动列表
     */
    List<PromotionActivityDTO> queryPromotionActivityList(PromotionActivityListQuery query);    

    /**
     * 添加促销活动信息
     *
     * @param request 促销活动添加请求
     * @return 是否添加成功
     */
    Integer addPromotionActivity(PromotionActivityAddRequest request);
}
@Component
public class MemberActivityStrategy {

    @Resource
    private MemberActivityDAO memberActivityDAO;

    /**
     * 根据请求分页查询促销活动列表
     *
     * @param query 活动列表查询请求
     * @return 促销活动列表
     */    
    @Override
    public List<PromotionActivityDTO> queryPromotionActivityList(PromotionActivityListQuery query) {
        // 构建查询参数
        MemberActivityParam memberActivityParma = new MemberActivityParam();
        param.setSellerId(query.getSellerId());
        param.setPageNo(query.getPageNo());
        param.setPageSize(query.getPageSize());
        
        // 数据库查询
        List<MemberActivityDO> memberActivityDOList = memberActivityDAO.selectByParam(memberActivityParma);
        
        // 组装返回结果
        List<PromotionActivityDTO> promotionActivityDTOList = Lists.newArrayList();
        for (MemberActivityDO memberActivityDO : memberActivityDOList) {
            PromotionActivityDTO activityDTO = new PromotionActivityDTO();
            promotionActivityDTO.setId(memberActivityDO.getId());
            promotionActivityDTO.setName(memberActivityDO.getName());
            promotionActivityDTO.setActivityInfo(memberActivityDO.getMemberInfo());
            promotionActivityDTOList.add(promotionActivityDTO);
        }
        return promotionActivityDTOList;
    }   
    
    /**
     * 添加促销活动信息
     *
     * @param request 促销活动添加请求
     * @return 是否添加成功
     */
    @Override
    public Integer addPromotionActivity(PromotionActivityAddRequest request) {
        // 构建会员活动DO
        MemberActivityDO memberActivityDO = new MemberActivityDO();
        memberActivityDO.setSellerId(sellerId);
        memberActivityDO.setName(activityName);
        memberActivityDO.setMemberInfo(activityInfo);
        
        // 增加数据记录
        return memberActivityDAO.add(memberActivityDO);
    }    
}

@Component
public class PromotionActivityStrategyFactory implements InitializingBean {

    @Resource
    private MemberActivityStrategy memberActivityStrategy;

    @Resource
    private GiftActivityStrategy giftActivityStrategy;

    /**
     * 根据请求分页查询促销活动列表
     *
     * @param query 活动列表查询请求
     * @return 促销活动列表
     */
    public PromotionActivityStrategy getStrategy(String type) {
        switch (type) {
            case "会员活动" :
                return memberActivityStrategy;
                break;
            case "礼赠活动" :
                return giftActivityStrategy;
                break;
            default:
                return null;
        }
            
    }
}
public class PromotionActivityManager {
    
    @Resource
    private PromotionActivityStrategyFactory promotionActivityStrategyFactory;

    /**
     * 根据活动类型与卖家Id分页查询活动列表
     *
     * @param query 活动列表查询请求
     */
    public List<PromotionActivityDTO> queryPromotionActivityList(PromotionActivityListQuery query) {
        // 参数校验
        AssertUtils.checkNotNull(query, "请求不能为空");
        query.checkParams();
        
        // 根据类型拿到策略
        PromotionActivityStrategy promotionActivityStrategy = promotionActivityStrategyFactory.getStrategy(query.getType());
        if (Objects.isNull(promotionActivityStrategy)) {
            log.error("无效的促销活动类型");
            throw new RuntimeException("无效的促销活动类型");
        }

        // 返回结果
        return promotionActivityStrategy.queryPromotionActivityList(query);
    }

    /**
     * 添加促销活动信息
     *
     * @param request 促销活动添加请求
     */
    public Integer addPromotionActivity(PromotionActivityAddRequest request) {
        // 参数校验
        AssertUtils.checkNotNull(request, "请求不能为空");
        request.checkparams();    

        // 根据类型拿到策略
        PromotionActivityStrategy promotionActivityStrategy = promotionActivityStrategyFactory.getStrategy(query.getType());
        if (Objects.isNull(promotionActivityStrategy)) {
            log.error("无效的促销活动类型");
            throw new RuntimeException("无效的促销活动类型");
        }
                
        return result;  
    }
}

●重复代码
经过一定量的重构后,代码看上去似乎健康了不少,但仔细看依旧能看出不少坏味道,
比如在PromotionActivityManager类的两个方法中,根据类型拿到对应策略这段代码实现相同,如果未来策略类有修改的话,这两个方法中的这段逻辑均需要进行修改,因此采用“提取方法”将这段逻辑提取出来。
●过长函数
在策略类的查询和添加方法,以及管理类的两个方法中,代码行数仍然较多。此时,我们依然需要采用“提取方法”这一重构手法。正如之前提到的提炼方法的准则所说的:“每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立的方法中。”因此,我们决定继续对活动策略类和活动管理类继续采用“提取方法”进行重构。又经过两次重构后,最终得到的所有代码如下展示:

public interface PromotionActivityStrategy {

    /**
     * 根据请求分页查询促销活动列表
     *
     * @param query 活动列表查询请求
     * @return 促销活动列表
     */
    List<PromotionActivityDTO> queryPromotionActivityList(PromotionActivityListQuery query);    

    /**
     * 添加促销活动信息
     *
     * @param request 促销活动添加请求
     * @return 是否添加成功
     */
    Integer addPromotionActivity(PromotionActivityAddRequest request);
}
@Component
public class MemberActivityStrategy {

    @Resource
    private MemberActivityDAO memberActivityDAO;

    /**
     * 根据请求分页查询促销活动列表
     *
     * @param query 活动列表查询请求
     * @return 促销活动列表
     */    
    @Override
    public List<PromotionActivityDTO> queryPromotionActivityList(PromotionActivityListQuery query) {        
        // 1 在数据库中查询
        List<MemberActivityDO> memberActivityDOList = memberActivityDAO.selectByParam(prepareMemberActivityParma(query));
        
        // 2 组装返回结果
        return wxMemberTabEventDTOS.stream()
                .map(this::convertMemberActivityDO2ActivityDTO)
                .collect(Collectors.toList());
    } 

    /**
     * 添加促销活动信息
     *
     * @param request 促销活动添加请求
     * @return 是否添加成功
     */
    @Override
    public Integer addPromotionActivity(PromotionActivityAddRequest request) {
        // 1 构建会员活动DO
        MemberActivityDO memberActivityDO = prepareMemberActivityDO(request);
        
        // 2 增加数据记录
        return memberActivityDAO.add(memberActivityDO);
    } 
    

    /**
     * 准备活动列表数据库查询参数
     *
     * @param query 活动列表查询请求
     * @return 活动列表数据库查询参数
     */ 
    private MemberActivityParam prepareMemberActivityParma(query) {
        MemberActivityParam memberActivityParma = new MemberActivityParam();
        param.setSellerId(query.getSellerId());
        param.setPageNo(query.getPageNo());
        param.setPageSize(query.getPageSize());
    }

    /**
     * 将MemberActivityDO转换为PromotionActivityDTO
     *
     * @param memberActivityDO 需要转换的数据库模型
     * @return PromotionActivityDTO
     */ 
    private PromotionActivityDTO convertMemberActivityDO2ActivityDTO(MemberActivityDO memberActivityDO) {
        PromotionActivityDTO promotionActivityDTO = new PromotionActivityDTO();
        promotionActivityDTO.setId(memberActivityDO.getId());

}
@Component
public class PromotionActivityStrategyFactory implements InitializingBean {

    @Resource
    private MemberActivityStrategy memberActivityStrategy;

    @Resource
    private GiftActivityStrategy giftActivityStrategy;

    /**
     * 根据请求分页查询促销活动列表
     *
     * @param query 活动列表查询请求
     * @return 促销活动列表
     */
    public PromotionActivityStrategy getStrategy(String type) {
        switch (type) {
            case "会员活动" :
                return memberActivityStrategy;
                break;
            case "礼赠活动" :
                return giftActivityStrategy;
                break;
            default:
                return null;
        }
            
    }
}
public class PromotionActivityManager {

@Resource
private PromotionActivityStrategyFactory promotionActivityStrategyFactory;

/**
* 根据活动类型与卖家Id分页查询活动列表
*
* @param query 活动列表查询请求
*/
public List<PromotionActivityDTO> queryPromotionActivityList(PromotionActivityListQuery query) {
// 1 参数校验
AssertUtils.checkNotNull(query, "请求不能为空");
query.checkParams();

// 2 根据类型拿到策略并判空校验
PromotionActivityStrategy promotionActivityStrategy = getPromotionActivityStrategy(query.getType());

// 3 返回结果
return promotionActivityStrategy.queryPromotionActivityList(query);
}

/**
* 添加促销活动信息
*
* @param request 促销活动添加请求
*/
public Integer addPromotionActivity(PromotionActivityAddRequest request) {
// 1 参数校验
AssertUtils.checkNotNull(request, "请求不能为空");
request.checkParams(request);

// 2 根据类型拿到策略并判空校验
PromotionActivityStrategy promotionActivityStrategy = getPromotionActivityStrategy(query.getType());

// 3 返回结果
return promotionActivityStrategy.addPromotionActivity(request);
}

/**
* 根据类型字符串拿到对应活动策略并进行校验
*
* @param type 促销活动类型
* @return 对应活动策略
*/
private PromotionActivityStrategy getPromotionActivityStrategy(String type) {
PromotionActivityStrategy promotionActivityStrategy = promotionActivityStrategyFactory.getStrategy(query.getType());
if (Objects.isNull(promotionActivityStrategy)) {
log.error("无效的促销活动类型");
throw new RuntimeException("无效的促销活动类型");
}
}
}

六、总结
作为一个刚进入职场的新人,《重构 改善既有代码的设计》这本书对我确实帮助巨大。尽管重构需要额外的时间和精力,但它带来的长期收益是不可忽视的。在日常工作中,我会将重构融入整个开发过程,并时刻提醒自己注意代码的可读性和扩展性。因此,希望通过这篇文章,能够将这本对我帮助巨大的书推荐给更多的开发者同学。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值