《重构》读书笔记

前言

  近期在看《重构:改善既有代码的设计》这本书,目前读了几个章节的内容,里面的内容与案例还是比较贴合实际且阅读难度不算大的一本重构书籍。好记性不如烂笔头,为了与大家分享和加深印象特记录本篇文章,文章会跟随我阅读的进度进行持续更新和补充,如有不足之处请在下方评论指点,本人会及时更正以免影响其他同学阅读。

注:项目内引入CheckStyle会让你养成写出优雅代码的习惯。

简介

   《重构:改善既有代码的设计》这本书在各大论坛上被指出是Java程序员必读的三本书之一。(另外两本是《Java编程思想》和《Effective Java》)。其实这本书给我的感觉,不一定局限于Java开发的同学,里面的思想和理念适合多种编程语言的设计。本书的讲解方式是贯穿式的,每个章节都会运用到其他章节的某种重构手法并标记出具体页码,刚开始看的时候还有些不适,总是要翻来翻去,像极了在开发时多个方法互相跳转的模式。
   本书除了引导你感悟什么代码需要重构,使用何种重构方式之外,还会搭配代码片段更生动讲解各个重构方式的思想与优缺点。

一、重构,第一个案例

   本书第一章以一个影片出租店的程序逻辑为主导展开的第一个重构,这个案例的重构没有什么难点,仅仅是让大家对重构有一个初步的认识和理解。(由于过于本章代码过于简单,就不展现在这了)
本章节主要讲解的总结如下:

  • 重构第一步永远是有一个可靠的测试环境,使得一次重构的最终目的得到保障。
  • 重构的技巧,小步前进,频繁测试。
  • 每个方法只做一件事,不要将多个功能汇总到一个方法中。
  • 遵循依赖倒置原则,调用方法的依赖抽象类或接口,不应依赖具体的实现类,这样可以降低客户与实现模块之间的耦合度。
  • 尽量减少方法中的临时变量,简化逻辑,增加可读性。如:
public int getFrequentRenterPoints() {
    int frequent = 1;
    if (getMovie().getPriceCode() == Movie.NEW_RELEASE && daysRented > 1) {
        frequent++;
    }
    return frequent;
}

可改成:

public int getFrequentRenterPoints() {
    if (getMovie().getPriceCode() == Movie.NEW_RELEASE && daysRented > 1) {
    return 2;
    }
    return 1;
}

二、重构原则

1.重构的定义
   通俗的讲就是现有的项目进行内部结构的调整,无论是接手过来的项目,还是因为之前能力有限无法写出优雅的代码,其宗旨都是在不改变现有业务流程的基础上进行改进与优化。
  本章提到了软件开发的两顶帽子,阐述了大家日常开发的两种模式。第一,在添加一个崭新的功能时,不应该去触碰现有的代码,而是只管添加新功能并进行测试。第二,重构阶段不再添加新的功能,只管对原有程序进行改进,最终通过现有的测试用例。

2.为何重构

  • 一个好的重构可以改进软件的合计
  • 重构可以让原本杂乱无章的功能变得更容易维护和理解
  • 在重构的同时,偶尔也能发现潜在而又隐蔽的bug
  • 某些抽离出来的单一职责的方法在后续的开发中,还会有意想不到的复用效果。

3.何时重构

  • 事不过三原则:一个校验或一个私有方法,如果出现三次,则有必要想办法进行提取和抽离。
  • 修补错误时重构:在排查问题时,当我们看着代码需要花费大量时间去努力理解他的用意时,可以根据情况考虑是否进行一次重构,这种情况的重构往往会让我们有意外的收获,比如让我们顺利的找出现有的bug和未浮出水面的bug。
  • 团队codeReview时,是找出坏味道最好的时机,往往自己写的代码当时很难发现问题。大家穿插阅览其他人的代码,会有意外的发现。

4.何时不该重构

  • 当项目比较紧急或已经接近最后期限时,往往草率的重构会带来麻烦。
  • 现有项目中从底层开始根基就不稳且大部分实现方法都比较混乱时,需要根据实际情况考虑重写而不是局部重构。

三、代码的坏味道

  这个章节还是比较重要的,但由于这本书的发版时间比较早,有些重构手法已经成了开发中的一些常识,本章会介绍如何识别出代码中存在的坏味道,为之后正确的重构打好基础。经典的坏味道特性总结在下面,当然不局限与此。

1. 重复代码

  1. 比较经典的坏味道体现,一个类中的多个方法有重复实现的代码,可将重复的代码提取出来形成一个新的方法,命名要尽量贴近代码实现的作用,供多个方法调用。
  2. 两个互为兄弟的子类存在重复代码,可将代码提炼到父类中去。如代码并非完全相同,可根据情况将相同和不同处拆分,可以使用模板方法的设计模式完成一次适配。(模板方法后面会有专门的章节提供代码的展示)
  3. 还有一点值得提的是,在项目的开发过程中,会有很多重复的工作出现在不同功能点中,如验签、加解密、某个状态码值类型转换及判断等。这些都可以抽出去来放在合适的工具类中,再之后追加新需求时后人会优先去工具类中获取并引用。

2. 过长函数
   每当感觉一段代码逻辑复杂并过长需要一个注释来说明的时候,优先选择把这段要说明的东西提炼成一个单独的函数专门去做这件事,注意命名要尽量贴合函数用途。

3. 过大的类
   类的设计应当遵循设计模式中的单一职责(SRP)。重构这种类可以尝试用抽取接口的方式思考如何拆解。
4. 过长的参数列

  1. 如果某些方法入参过长,可根据情况将参数封装在一个对象,可降低入参传错字段的风险使代码更加整洁。
  2. 有时可考虑有些字段是否需要调用方来提供,能否自己可以生成或获取。

5. 发散式变化
   发散式变化体现在一个类会受到各种变化的影响,牵一发而动全身。这个对我体会较深,在工作中每个项目组要求代码质量的水准都不同,开发人员往往容易把全部逻辑放在Service层,甚至是Controller层。这样不断的扩展最后将造成很难维护,扩展风险及大。

6. 散弹式修改
  散弹式修改指的是一个小小的变化导致多个类都需要对应去做出调整,应该把需要修改的部分放到一个类中统一做出处理,避免遗漏。

7. 依恋情节
  将数据和对数据操作的行为包装在一起。通俗的讲就是将一起变化的东西放在一块。

8. 数据泥团
  两个类中相同的字段、函数中相同的参数,考虑提取成一个单独的数据类。

9. 夸夸其谈未来性
  检查抽象类、委托、方法的参数没有实际作用的,那么就果断删除掉不要未雨绸缪。

四、构筑测试体系

  本章节比较简短,主要围绕Junit测试模式为主,对重构的地方进行测试。有时,为了提高效率不如试试Groovy测试框架。本人用过一段时间的Groovy来对功能进行测试,给我的感觉Groovy比起Java更简洁、开发效率更高,由于元编程的特性可能会让Groovy性能有所损失(10%左右)。
本人整理的Groovy简单使用,当然Groovy的强大不局限于此

五、重构列表

  本章没有重点,如果是Java开发,使用Idea就能够很好的支持常见的重构说法,还有各种自动提示,底色标黄处都值得你去留意一下,是否潜在异常。

六、 重新组织函数

  本章提到9种针对函数的优化做法,我将列举几种常用的配合代码案例进行整理。来吧,展示

1. 内联函数: 在函数中调用处插入函数代码体,将原函数删除。

	int value = dto.getLargerThanFile();
    int getRating(){
        return (largerThanFile()) ? 2 : 1;
    }

    Boolean largerThanFile(){
        return value > 6;
    }
//重构后
 	int getRating(){
        return (value > 6) ? 2 : 1;
    }

2. 引入解释性变量: 将复杂表达式提取存放一个临时变量中,并命名来表达此用途。

	if((name.indexof("mack")>-1)&&(readBook.indexof("重构")>-1)) {
	        ......;
	}
//重构后
	Boolean isName = name.indexof("mack") > -1;
    Boolean isBook = readBook.indexof("重构") > -1;
    if(isName &&isBook) {
        ......;
    }

3. 以查询取代临时变量: 以一个临时变量存储运算得出的结果,将这部分的运算提炼到独立的函数中。即便算法后续有任何的改动,只需要修改此函数即可。

		double basePrice = (principal * interestRate + loan) / 12;
        if (basePrice > 1000) {
            return basePrice * 0.95;
        } else {
            return basePrice * 0.98;
        }
//重构后
		if (getPasePrice() > 100) {
            return getPasePrice() * 0.95;
        }
        else {
            return getPasePrice() * 0.98;
        }
        
        int getPasePrice() {
            return (principal * interestRate + loan) / 12;;
        }

七、在对象之间搬移特性

  面向对象的设计中,“决定把责任放在哪里”是最重要的理念之一。开发中最常见的烦恼是:我们无法从一开始就保证所有的事情做的天衣无缝。在这种情况下,可以尝试一次大胆的重构,改变原来的设计使得代码更加优雅。由于代码实现比较简单,这章就不做代码展示了,这里就介绍一下几种重构手法的思想:
1. 移动函数
类行为尽量做到单一,如果一个类的行为过多,或与其他类有太多的耦合,这时需要做一次搬移。
2. 搬移字段
一个类的字段在另一个类中频繁使用过,考虑将字段搬移。
3. 提炼类
类应该是清楚的抽象,处理一些明确的职责,不应太冗余。
4. 类内联化
它和提炼类刚好相反,如果一个雷不再承担足够的责任、不再有单独存在的理由,可以将这种“萎缩类”塞入另一个类中。
5. 隐藏委托关系
在服务类建立客户所需的所有类,用来隐藏委托的关系。

A–>B
A–>C
进行重构
A–>B–>C

6. 移除中间人
跟隐藏委托关系的手法相反,根据不同的使用情况来判断运用哪种手法。

A–>B–>C
A–>B
A–>C
7. 引入外加函数
当你需要为提供服务的类增加一个函数时,可将这种场景提出来。

Data newStart = new Date(pre.getYear(), pre.getMonth(), pre.getDate() + 1);
//重构后
Date newStart = nextDay(pre);
private static Date nextDay(Date arg) {
    return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}

八、重新组织数据

1. 自封装字段
问题:直接访问一个字段,但是字段之间的耦合关系逐渐变得笨拙。
优化:为这个字段建立取值、设值函数,并且只以这些函数来访问字段,体现灵活性。

//案例
boolean includes(int arg){
    return arg>=low&&arg<=high;
}
//重构
private int low,high;
boolean includes(int arg){
    return arg>=getLow()&&arg<=getHigh();
}
int getLow(){return low;}
int getHigh(){return high;}

2. 以对象取代数据值
问题:一个数据项,需要与其他数据、行为一起使用才有意义。
优化:将数据项变成对象形式
在这里插入图片描述

3. 以对象取代数组
问题:一个数值,每个元素都代表不同的含义。
优化:以对象代替数组,对数组中每一个元素以一个字段来表示,一劳永逸。

String[] row=new String[2];
row[0] = "张三";
row[1] = "25";
//重构为:
User row = new User();
row.setName("张三");
row.setWins("25");

4. 将单向关联改为双向关联
问题:两个类之间有双向关联,但其中一个类现在不再需要另一个类的特性。
优化:去除不必要的关联,大量的双向链接容易造成“僵尸对象”,双向关联迫使两个对象有依赖关系,对其中一个进行修改会引发另一个类的变化。

5. 以字面常量取代魔法数字
问题:一个字面数值,带有特别的含义。
优化:创建一个常量,根据其行为为它命名,并将上述的字面数值替换为这个常量。

double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
//重构
double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;

6. 以字段取代子类
问题:每个子类的唯一差别,只在“返回常量”的函数身上。
优化:修改这些函数,将返回超类中的某个字段,然后销毁子类。

abstract class Person {
abstract boolean isMale();
abstract char getCode();
}
...
class Male extends Person {
    boolean isMale() {
        return true;
    }
    char getCode() {
        return 'M';
    }
}

class Female extends Person {
    boolean isMale() {
        return false;
    }
    char getCode() {
        return 'F';
    }
}

//去除不必要的子类即可:
class Person{
private final boolean _isMale;
...
}

九、简化条件表达式

  本章提供了一些重构的经典手法,专用来简化一些复杂难以理清的条件逻辑。
1. 分解条件表达式
问题:开发功能点初期经常会产生一些复杂的条件,如(if-then-else)语句。
优化:将if、then、eles段落中分别提炼出相对独立的函数。

if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
	charge = quantity * this.winterRate + this.winterServiceCharge;
} else {
	charge = quantity * this.summerRate;
}
//重构
//思路:将每个分支拆开,提炼为一个个独立函数
if (notSummer(date)) {
	charge = winterCharge(quantity);
} else {
	charge = summerCharge(quantity);
}

pirvate boolean notSummer(Date date) {
	return date.before(SUMMER_START) || date.after(SUMMER_END);
}

private double summerCharge() {
	return quantity * this.summerRate;
}

private double winterCharge() {
	return quantity * this.winterRate + this.winterServiceCharge;
}

2. 合并条件表达式
问题:你有一系列条件测试,都得到相同结果。
优化:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立的函数。

double disabilityAmount() {
	if (this.seniority < 2) return 0;
	if (this.monthsDisabled > 12) return 0;
	if (this.isPartTime) return 0;
	...
}
//重构
//思路:将一系列都在做一件事的条件检查,进行联系和拼接
double disabilityAmount() {
	if (isNotEligibleForDisability()) return 0;
	...
}

boolean isNotEligibleForDisability() {
	reutrn ((this.seniority < 2) || (this.monthsDisabled > 12) || (this.isPartTime));
}

3. 合并重复的条件片段
问题:条件表达式冗余,每个分支上都有一些相同的代码。
优化:将重复代码提炼到表达式之外。

if (isSpecialDeal()) {
	total = price * 0.95;
	send();
} else {
	total = price * 0.98;
	send();
}
//重构
if (isSpecialDeal()) {
	total = price * 0.95;
} else {
	total = price * 0.98;
}
send();

4. 移除控制标记
问题:在一系列布尔表达式中,某个变量带有“控制标记”的作用。
优化:以break语句或return语句取代控制标记。

void checkSecurity(String[] people) {
	boolean found = false;
	for (int i = 0; i < people.length; i++) {
		if (!found) {
			if (people[i].equals("Don")) {
				sendAlert();
				found = true;
			}
			if (people[i].equals("John")) {
				sendAlert();
				found = true;
			}
		}
	}
}
//重构
void checkSecurity(String[] people) {
	for (int i = 0; i < people.length; i++) {
		if (people[i].equals("Don")) {
			sendAlert();
			break;
		}
		if (people[i].equals("John")) {
			sendAlert();
			break;
		}
	}
}

5. 以多台取代条件表达式
问题:项目中会有一些条件表达式,它根据对象类型的不同而选择不同的行为。
优化:将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
  如果同一组表达式在程序许多地点出现,那么使用多态的收益是最大。使用条件表达式时,如果你想添加一种新类型,就必须查找并更新所有条件表达式。但如果改用多态,只需建立一个新的子类,并在其中提供适当的函数就行了。类的用户不需要了解这个子类,这就大大降低了系统个部分之间的依赖,是系统升级更加容易。

class Employee {
	Employee(EmployeeType type) {
		setType(type);
	}
	int getType() {
		return this.type.getTypeCode();
	}
	void setType(EmployeeType arg) {
		this.type = arg;
	}	
	private EmployeeType type;
	
	int payAmount() {
		switch(getType()) {
			case EmployeeType.ENGINEER:
				return this.monthlySalary;
			case EmployeeType.SALESMAN:
				return this.monthlySalary + this.commission;
			case EmployeeType.MANAGER:
				return this.monthlySalary + this.bonus;
			default:
				throw new RuntimeException("Incorrect Employee");
		}
	}
}

abstract class EmployeeType {
	abstract int getTypeCode();
}

class Engineer extends EmployeeType {
	int getTypeCode() {
		return ENGINEER;
	}
}

class Manager extends EmployeeType {
	int getTypeCode() {
		return SALESMAN;
	}
}

class Salesman extends EmployeeType {
	int getTypeCode() {
		return MANAGER;
	}
}
//重构
class EmployeeType {
	int payAmount(Employee emp) {
		switch (code) {
			case ENGINNER:
				return emp.getMonthlySalary();
			case SALESMAN:
				return emp.getMonthlySalary() + emp.getCommission();
			case MANAGER:
				return emp.getMonthlySalary() + emp.getBenus();
			default:
				throw new IllegalArgumentException("Incorrect Employee Code");
		}
	}
}



class EmployeeType {
	abstract int payAmount(Employee emp);
}

class Enginner extend EmployeeType {
	int payAmount(Employee emp) {
		return emp.getMonthlySalary();
	}
}

class Salesman extend EmployeeType {
	int payAmount(Employee emp) {
		return emp.getMonthlySalary() + emp.getCommission();
	}
}

class Manager extend EmployeeType {
	int payAmount(Employee emp) {
		return emp.getMonthlySalary() + emp.getBenus();
	}
}

6. 引入断言
问题:一段代码需要对程序状态做出某种假设。
优化:以断言明确表现这种假设。
  使用断言明确标明对输入条件的严格要求和限制,单恋可以辅助交流和测试。

double getExpenseLimit () {
  return (_expLimit != NULL_EXPENSE) ? _expLimit : _primaryPro.getExpenseLimit();
}
//重构
double getExpenseLimit () {
  Assert.isTrue((_expLimit != NULL_EXPENSE) || _primaryPro != NULL );
  return (_expLimit != NULL_EXPENSE) ? _expLimit : _primaryPro.getExpenseLimit();
}

十、简化函数调用

  在对象技术中,最重要的概念“接口”(interface)。容易被理解和被使用的接口,是开发良好面向对象软件的关键。本章会依次介绍一些使接口变得更简洁易用的重构手法,其实概念很简单但会在开发中经常被忽略。
1. 函数改名
问题:函数的名称未能表达函数的用途。
优化:修改函数名称。
个人补充:在开发初期阶段,一些函数功能不完善使得最初的命名与最终的函数用途不一致,建议在codeReview过程中找到并修改
2. 添加参数
问题:某个函数需要从调用端得到更多信息。
优化:为此函数添加一个对象参数,让该对象带进函数所需信息。
个人补充:当函数入参达到5个时,就要考虑是否将参数进行封装,以对象行形式传入函数。
3. 移除参数
问题:函数本体不再需要某个函数。
优化:将无引用参数进行删除,以免后期维护产生疑惑。
4. 以明确函数取代参数
问题:你有一个函数,其中完全取决于参数值而采取不同行为。
优化:针对该参数的每一个可能值,建立一个独立函数。

static final int ENGINEER = 0;
	static final int SALESMAN = 1;
	static final int MANAGER = 2;
	static Employee create(int type) {
		switch (type) {
			case ENGINEER:
				return new Engineer();
			case SALESMANA:
				return new Salesman();
			case MANAGER:
				return new Manager();
			default:
				throw new IllegalArgumenntException("Incorrect type code value");
		}
	}
//重构
static Employee createEngineer() {
		return new Engineer();
	}
	static Employee createSalesman() {
		return new Salesman();
	}
	static Employee createManager() {
		return new Manager();
	}
	static Employee create(int type) {
		switch (type) {
			case ENGINEER:
				return Employee.createEngineer();
			case SALESMANA:
				return Employee.createSalesman();
			case MANAGER:
				return Employee.createManager();
			default:
				throw new IllegalArgumenntException("Incorrect type code value");
		}
	}

5. 封装向下转型
问题:某个函数返回的对象,需要由函数调用者执行向下转型。
优化:将向下转型动作移到函数中。

Object lastReading() {
		return readings.lastElement();
	}
//重构:当拥有一个集合时,上述那么做就很有意义。
Reading lastReading() {
		return (Reading) readings.lastElement();
	}

十一、处理概括关系

1. 字段上移
问题:两个子类拥有相同的字段。
优化:将该字段移至超类。
在这里插入图片描述
2. 函数上移
问题:有些函数,在各个子类中产生完全相同的结果。优化:将该函数移至超类。
优化:将该函数移至超类。
在这里插入图片描述
3. 提炼子类
问题:超类中的某个字段只被部分(而非全部)子类用到。优化:将这个字段移到需要它的那些子类去。
优化:将这个字段移到需要它的那些子类去。
在这里插入图片描述
4. 提炼接口
问题:若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。优化:将相同的子集提炼到一个独立接口中。
优化:将相同的子集提炼到一个独立接口中。
在这里插入图片描述
①该方法仅仅用于按照时间计算费用,以及判断元购会否有特殊技能的用途

double charge(Employee emp, int days) {
        int base = emp.getRate() * days;
        if (emp.hasSpecialSkill()) {
            return base * 1.05;
        }
        return base;
    }

②除了提供员工信息之外,Employee还有很多其他功能,这时可将charge仅涉及的两个功能定义为接口并实现。

interface Billable {
        public int getRate();
        public boolean hasSpecialSkill();
    }

    clas Employee implements Billable ...


    double charge(Billable emp, int days) {
        int base = emp.getRate() * days;
        if (emp.hasSpecialSkill()) {
            return base * 1.05;
        }
        return base;
    }

十二、大型重构

1. 将过程化设计转化为对象设计
问题:有一些传统过程化风格的代码。
优化:将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中。

  • 针对每一个记录类型,将其转变为只含有访问函数的数据对象。
  • 针对每一处过程化风格,将该处的代码提炼到一个独立类中。
  • 针对每一段长长的程序,实施提炼手法及其他相关重构将它分解,再以移动手法将分解后的函数分别移到它所相关的数据类中。
  • 重复上述步骤,直到原始类中的所有函数都被移除,如果原始类是一个完全过程化的类,将它拿掉。

2. 将领域和表述/显示分离
问题:某些GUI类之中包含了领域逻辑。
优化:将领域逻辑分离出来,为它们建立独立的领域类。
为每个窗口建立一个领域类。

  • 如果窗口内有一张表格,新建一个类来表示其中的行,再以窗口所对应之领域类中的一个集合来容纳所有的行领域对象。
  • 检查窗口中的数据,如果数据只被用于UI,就把它留着;如果数据被领域逻辑使用,而且不显示于窗口上,就用移动字段手法将它搬移到领域类中,如果数据同时被UI和领域逻辑使用,就对它实施重复观察数据,使它同时存在于两处,并保持两处之间的同步。
  • 检查展现类中的逻辑,实施提炼方法将展现逻辑从领域逻辑中分开,一旦隔离了领域逻辑,再运用Move Method将它移到领域类。
  • 以上步骤完成后,就拥有了两组彼此分离的类:展现类用以处理GUI,领域类包含所有业务逻辑。

十三、重构,复用与实现

  1. 现实的检验
  • 重构的潜在因素:在编写代码时对自己所做的事情没有完整的了解,并且受到生产进度的压力。
  • 重构被用于开发框架、抽取可复用组件、使软件架构更清晰、使新功能的增加更容易。
  • 重构可以帮助你充分利用以前的投资,减少重复劳动,使程序更简洁有力。
  • 重构时,你必须找出待重构的这一部分程序被什么地方引用。
  1. 为什么开发者不愿意重构它们的程序
  • 不知道如何重构。
  • 重构的利益是长远的,何必现在付出这些努力。
  • 代码重构是一项额外工作,需在完成开发需求基础上额外抽出时间进行重构。
  • 重构可能破坏现有程序。

十四、重构工具

总结

  本书主要涉及重构中的各种细节问题,基本都是比较常见的开发中遇到的常常被人忽略的一个个小点。本书代码案例都是比较简单的片段来说明对应说法的思想,整体看下来还是比较容易理解的。
①从如何识别代码的坏味道
②重新组织函数、对象、数据
③简化表达式、简化函数调用
④处理概况(继承)关系
⑤大型重构
⑥结合实际
  面对项目中已有代码,有些模块还是有点不知所措,我觉得还是欠缺一些思考,以及对项目的整体把控不够。接下来的时间在逐渐熟练掌握项目的同时,也将这些重构手法运用到项目当中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值