《代码中的软件工程》学习总结

本学期选修了孟宁老师的《高级软件工程》课程,经过上课听讲、完成课后作业和阅读教材《代码中的软件工程》,让我对软件工程的理解更加全面和深入,了解到的工具使用、编码规范、软件设计过程、设计模式以及对于软件危机的思考等将深深影响我之后的编程生涯。课程分为五个部分展开,在课程结束之时,我也分别就以下五个部分记录自己学习的心得体会和收获。

一、工欲善其事 必先利其器

本篇介绍了编程过程中常用的几个工具,这些工具可以提升我们编程的效率。

Visual Studio Code

Visual Studio Code(以下简称vscode)是一个轻量且强大的代码编辑器,支持多平台,强大的插件拓展使得vscode支持各种语言和各种功能,课上孟老师在介绍其使用的同时也以其作为例子分析了其作为一款优秀软件而风靡编程界的特点:

  • 简洁而聚焦的产品定位
  • 进程隔离的插件模型
  • UI 渲染与业务逻辑隔离,一致的用户体验
  • 代码理解和调试——LSP和DAP两大协议
  • 集大成的 Remote Development

Git版本控制

Git作为版本控制工具在项目编码过程中十分重要,本节学习了其使用,比如以下命令:

git init # 在一个新建的目录下创建版本库
git clone https://github.com/YOUR_NAME/REPO_NAME.git # 通过clone远端的版本库从而在本地创建一个版本库
git init # 初始化一个本地版本库
git status # 查看当前工作区(workspace)的状态
git add [FILES] # 把文件添加到暂存区(Index)
git commit -m “wrote a commit log infro” # 把暂存区里的文件提交到仓库
git log # 查看当前HEAD之前的提交记录,便于回到过去
git reset —hard HEAD^^/HEAD~100/commit-id/commit-id的头几个字符 # 回退
git reflog # 可以查看当前HEAD之后的提交记录,便于回到未来
git reset —hard commit-id/commit-id的头几个字符 # 回退
git clone命令官方的解释是“Clone a repository into a new directory”,即克隆一个存储库到一个新的目录下。
git fetch命令官方的解释是“Download objects and refs from another repository”,即下载一个远程存储库数据对象等信息到本地存储库。
git push命令官方的解释是“Update remote refs along with associated objects”,即将本地存储库的相关数据对象更新到远程存储库。
git merge命令官方的解释是“Join two or more development histories together”,即合并两个或多个开发历史记录。
git pull命令官方的解释是“Fetch from and integrate with another repository or a local branch”,即从其他存储库或分支抓取并合并到当前存储库的当前分支。

正则表达式

正则表达式是对字符串操作的一种逻辑公式,是程序员手中一把威力无比强大的武器,通过其学习我们可以方便的查询和替换字符串,提高编程效率,对常用的通配符做以下总结:

  • 通配符“.”将匹配任意一个字符。通配符也可称为 dot 和 period。你可以像正则表达式中的任何其他字符一样使用通配符。例如,如果你想匹配“hug”,“huh”,“hut”和“hum”,可以使用正则表达式hu.来匹配这所有四个字符串。
  • 通配符“+”用来查找出现一次或多次的字符,例如hahhhhh,可以使用正则表达式hah+来匹配。
  • 通配符“*”匹配零次或多次出现的字符,使用正则表达式hah*来匹配,还可以匹配ha字符串。
  • 通配符“?”指定可能存在的元素,也就是检查前一个元素存在与否,如正则表达式colou?r、favou?rite中通配符“?”前面的u字符存在和不存在两种情况的字符串都会匹配。

二、工程化编程实战

本篇以menu菜单项目的不断迭代介绍了工程化编程的基本方法,例子深入浅出,有许多点让我印象深刻。

代码风格

之前的编码中我不怎么注重编码的风格,导致代码十分难读,了解了简明、易读、无二义性的代码风格原则后我又学习了googleC++编程规范,现在的代码较之前易读许多。将课程中提到的规范总结如下:

  • 缩进:4个空格;
  • 行宽:< 100个字符;
  • 代码行内要适当多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d));
  • 在一个函数体内,逻揖上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行以示区隔;
  • 在复杂的表达式中要用括号来清楚的表示逻辑优先级;
  • 花括号:所有 ‘{’ 和 ‘}’ 应独占一行且成对对齐;
  • 不要把多条语句和多个变量的定义放在同一行;
  • 命名:合适的命名会大大增加代码的可读性;
    • 类名、函数名、变量名等的命名一定要与程序里的含义保持一致,以便于阅读理解;
    • 类型的成员变量通常用m_或者_来做前缀以示区别;
    • 一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter;
    • 类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写;
    • 类型、类、变量一般用名词或者组合名词,如Member
    • 函数名一般使用动词或者动宾短语,如get/set,RenderPage;

性能优先策略背后隐藏的代价

最初学习编程时接触到的就是性能优先策略,在课上了解到的其代价改变了我的一些编程习惯,代价具体如下:

  • 编码成本。当软件工程师的人力成本远大于所消耗的计算资源成本时,提高代码编写的工作效率将更有价值;
  • 测试成本。质量保证的人力成本和质量保证的成效也比所消耗的计算资源成本更有价值;
  • 理解成本。性能优先的策略往往会让代码很难理解,结果需要消耗更多的工时;
  • 修改成本。面向机器的代码修改起来更困难,可扩展性差,同样会消耗更多工时。

模块化编程

模块化编程是面向过程编程的重要方法,其基本原理是关注点的分离,关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

  • 耦合度是指软件模块之间的依赖程度,在软件设计中追求松散耦合。
  • 内聚度是指一个软件模块内部各元素之间互相依赖的紧密程度,理想的内聚是功能内聚。

软件设计中的一些基本方法

  • KISS(Keep It Simple & Stupid)原则
  • 使用本地化外部接口来提高代码的适应能力
    • 不要和陌生人说话原则
  • 保持设计结构和代码结构的一致性
    • 用设计结构框定代码结构
    • 先写伪代码的代码结构更好一些

接口

接口就是互相联系的双方共同遵守的一种协议规范,

接口规格的五个基本要素:

  • 接口的目的;
  • 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
  • 使用接口的双方遵守的协议规范;
  • 接口使用之后的效果,一般称为后置条件;
  • 接口所隐含的质量属性。

接口和耦合度之间的关系

  • 公共耦合
    • 当软件模块之间共享数据区或变量名的软件模块之间即是公共耦合,显然两个软件模块之间的接口定义不是通过显式的调用方式,而是隐式的共享了共享了数据区或变量名。
  • 数据耦合
    • 在软件模块之间仅通过显式的调用传递基本数据类型即为数据耦合。
  • 标记耦合
    • 在软件模块之间仅通过显式的调用传递复杂的数据结构(结构化数据)即为标记耦合,这时数据的结构成为调用双方软件模块隐含的规格约定,因此耦合度要比数据耦合高。但相比公共耦合没有经过显式的调用传递数据的方式耦合度要低。

通用接口定义的基本方法

  • 参数化上下文
  • 移除前置条件
  • 简化后置条件

函数的可重入性与线程安全之间的关系

可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;不可重入的函数一定不是线程安全的。

看待软件质量的几个不同角度

  • 产品的角度,也就是软件产品本身内在的质量特点;
  • 用户的角度,也就是软件产品从外部来看是不是对用户有帮助,是不是有良好的用户体验;
  • 商业的角度,也就是商业环境下软件产品的商业价值,比如投资回报或开发软件产品的其他驱动因素。

这三个角度的软件质量有着内在的联系,比如具有商业价值的软件产品是以用户质量为前提的,具有良好用户质量的软件产品也往往有一些好的产品内在质量特点。

从需求分析到软件设计

本篇介绍了一种从需求分析到软件设计的基本建模方法。按照敏捷统一过程的基本流程,包括获取需求、用例建模、业务领域建模、对象交互建模、形成设计类图、软件编码实现和应用部署。

需求分析

需求分析的概念

需求是期望行为的表述,需求分析就是需求分析师对用户期望的软件行为进行表述,并进一步用对象或实体的状态、属性和行为来定义需求。

需求的类型

  • 功能性需求
  • 非功能性需求
  • 设计约束条件
  • 过程约束条件

高质量需求的特征

  • 便于验证
  • 解决内在冲突
  • 正确、内在一致、准确、完整、可行

需求分析的两类基本方法

  • 原型化方法可以很好地整理出用户接口方式(UI,User Interface),比如界面布局和交互操作过程。
  • 建模的方法可以快速给出有关事件发生顺序或活动同步约束的问题,能够在逻辑上形成模型来整顿繁杂的需求细节。

用例

用例(Use Case)的核心概念中首先它是一个业务过程(business process),经过逻辑整理抽象出来的一个业务过程,这是用例的实质。

用例的三个抽象层级:

  • 抽象用例(Abstract use case)。只要用一个干什么、做什么或完成什么业务任务的动名词短语,就可以非常精简地指明一个用例;
  • 高层用例(High level use case)。需要给用例的范围划定一个边界,也就是用例在什么时候什么地方开始,以及在什么时候什么地方结束;
  • 扩展用例(Expanded use case)。需要将参与者和待开发软件系统为了完成用例所规定的业务任务的交互过程一步一步详细地描述出来,一般我们使用一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来。

用例建模的基本步骤

  1. 第一步,从需求表述中找出用例,往往是动名词短语表示的抽象用例;
  2. 第二步,描述用例开始和结束的状态,用TUCBW和TUCEW表示的高层用例;
  3. 第三步,对用例按照子系统或不同的方面进行分类,描述用例与用例、用例与参与者之间的上下文关系,并画出用例图;
  4. 第四步,进一步逐一分析用例与参与者的详细交互过程,完成一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来扩展用例。

用例的四个必要条件

  1. 它是不是一个业务过程?
  2. 它是不是由某个参与者触发开始?
  3. 它是不是显式地或隐式地终止于某个参与者?
  4. 它是不是为某个参与者完成了有用的业务工作?

业务领域建模

  1. 第一步,收集应用业务领域的信息。聚焦在功能需求层面,也考虑其他类型的需求和资料;
  2. 第二步,头脑风暴。列出重要的应用业务领域概念,给出这些概念的属性,以及这些概念之间的关系;
  3. 第三步,给这些应用业务领域概念分类。分别列出哪些是类、哪些属性和属性值、以及列出类之间的继承关系、聚合关系和关联关系。
  4. 第四步,将结果用 UML 类图画出来。

MongoDB数据库结构

  • 内嵌:一对很少且不需要单独访问内嵌内容的情况下可以使用内嵌多的一方。
  • 子引用:一对很多且很多的一端内容因为各种理由需要单独存在的情况下可以通过数组的方式引用多的一方的。
  • 父引用:一对非常多的情况下,请将一的那端引用嵌入进多的一端对象中。

统一过程

统一过程(UP,Unified Process)的核心要义是用例驱动(Use case driven)、以架构为中心(Architecture centric)、增量且迭代(Incremental and Iterative)的过程。

敏捷统一过程的四个关键步骤

  1. 第一,确定需求;
  2. 第二,通过用例的方式来满足这些需求;
  3. 第三,分配这些用例到各增量阶段;
  4. 第四,具体完成各增量阶段所计划的任务。

敏捷统一过程的增量阶段

  1. 用例建模(Use case modeling);
  2. 业务领域建模(Domain modeling);
  3. 对象交互建模(Object Interaction modeling);
    1. 找出关键步骤进行剧情描述(scenario)
    2. 将剧情描述(scenario)转换成剧情描述表(scenario table)
    3. 将剧情描述表转换成序列图的基本方法
    4. 从分析序列图到设计序列图
  4. 形成设计类图(design class diagram);
  5. 软件的编码实现和软件应用部署;

软件科学基础概论

软件的基本结构

  • 顺序结构
  • 分支结构
  • 循环结构
  • 函数调用框架
  • 继承和对象组合
    • 继承复用的缺点
      • 破坏了类的封装性,父类对子类透明,白箱复用
      • 子类与父类耦合度高,不利于类的扩展与维护
      • 限制了复用的灵活性,实现是静态的在编译时已经定义
    • 对象组合的优点
      • 维持了类的封装性,属性内部细节隐藏,黑箱复用
      • 新旧类耦合度低,接口清晰明确
      • 复用灵活性高,运行时动态进行

软件的一些特殊机制

  • 回调函数
    • 回调函数是一个面向过程的概念,是代码执行过程的一种特殊流程。回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,当这个指针调用其所指向的函数时,就称这是回调函数。
    • 回调函数实现了业务模块和工具模块的解耦合,增强了工具模块的复用
  • 多态
  • 闭包
    • 闭包是变量作用域的一种特殊情形,一般用在将函数作为返回值时,该函数执行所需的上下文环境也作为返回的函数对象的一部分
    • 闭包可以让你从内部函数访问外部函数作用域。
  • 异步调用
  • 匿名函数

软件的内在特性

  • 前所未有的复杂度
  • 抽象思维 vs. 逻辑思维
  • 唯一不变的就是变化本身
    • S系统
    • P系统
    • E系统
  • 难以达成的概念完整性和一致性

设计模式

设计模式的本质是面向对象设计原则的实际运用总结出的经验模型。

设计模式的优点

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

设计模式的组成

  • 该设计模式的名称;
  • 该设计模式的目的,即该设计模式要解决什么样的问题;
  • 该设计模式的解决方案;
  • 该设计模式的解决方案有哪些约束和限制条件。

常用的设计模式

  • 单例模式
  • 原型模式
  • 建造者模式
  • 代理模式
  • 适配器模式
  • 装饰模式
  • 外观模式
  • 享元模式
  • 策略模式
  • 命令模式
  • 模板方法
  • 职责链
  • 中介者模式
  • 观察者模式
    • 指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为,这样所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式。

设计模式背后的设计原则

  • 开闭原则
    • 对拓展开放,对修改关闭
  • Liskov替换原则
    • 继承必须确保超类所拥有的性质在子类中仍然成立
  • 依赖倒置原则
    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象
    • 其核心思想是:要面向接口编程,不要面向实现编程。
  • 单一职责原则
    • 单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分
  • 迪米特法则
    • 只与你的直接朋友交谈,不跟“陌生人”说话
    • 含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
  • 合成复用法则
    • 它要求在软件复用时,要尽量先使用组合或者聚合关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循Liskov替换原则。

常见的软甲架构

三层架构

  • 界面层
  • 业务逻辑层
  • 数据访问层

MVC架构

MVC即为Model-View-Controller(模型-视图-控制器),MVC中M、V和C所代表的含义如下:

  • Model(模型)代表一个存取数据的对象及其数据模型。模型用来封装核心数据和功能,它独立于特定的输出表示和输入行为,是执行某些任务的代码
  • View(视图)代表模型包含的数据的表达方式,一般表达为可视化的界面接口。视图用来向用户显示信息,它获得来自模型的数据,决定模型以什么样的方式展示给用户。
  • Controller(控制器)作用于模型和视图上,控制数据流向模型对象,并在数据变化时更新视图。控制器可以使视图与模型分离开解耦合。控制器是和视图联合使用的,它捕捉鼠标移动、鼠标点击和键盘输入等事件,将其转化成服务请求,然后再传给模型或者视图。

MVVM

MVVM即 Model-View-ViewModel

特点:

  • 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  • 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
  • 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
  • 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。

两种不同层级的软件架构复用方法

  • 克隆(Cloning),完整地借鉴相似项目的设计方案,甚至代码,只需要完成一些细枝末节处的修改适配工作。
  • 重构(Refactoring),构建软件架构模型的基本方法,通过指引我们如何进行系统分解,并在参考已有的软件架构模型的基础上逐步形成系统软件架构的一种基本建模方法。

软件架构风格和策略

  • 管道-过滤器
    • 面向数据流的软件体系结构,最典型的应用是编译系统。
  • 客户-服务
    • 客户-服务模式的架构风格是指客户代码通过请求和应答的方式访问或者调用服务代码。
  • P2P
    • P2P(peer-to-peer)架构是客户-服务模式的一种特殊情形,P2P架构中每一个构件既是客户端又是服务端
  • 发布-订阅
    • 在发布-订阅架构中,有两类构件:发布者和订阅者。如果订阅者订阅了某一事件,则该事件一旦发生,发布者就会发布通知给该订阅者。观察者模式体现了发布-订阅架构的基本结构。
  • CRUD
    • CRUD 是创建(Create)、 读取(Read)、更新(Update)和删除(Delete)四种数据库持久化信息的基本操作的助记符,表示对于存储的信息可以进行这四种持久化操作。CRUD也代表了一种围绕中心化管理系统关键数据的软件架构风格。
  • 层次化
    • •较为复杂的系统中的软件单元,仅仅从平面展开的角度进行模块化分解是不够的,还需要从垂直纵深的角度将软件单元按层次化组织,每一层为它的上一层提供服务,同时又作为下一层的客户。

软件架构的描述方法

软件架构模型是通过一组关键视图来描述的,同一个软件架构,由于选取的视角(Perspective)和抽象层次不同可以得到不同的视图,这样一组关键视图搭配起来可以完整地描述一个逻辑自洽的软件架构模型。

  • 分解视图 Decomposition View
    • 分解视图用软件模块勾划出系统结构,往往会通过不同抽象层级的软件模块形成层次化的结构。
  • 依赖视图 Dependencies View
    • 依赖视图展现了软件模块之间的依赖关系。
  • 泛化视图 Generalization View
    • 泛化视图展现了软件模块之间的一般化或具体化的关系,典型的例子就是面向对象分析和设计方法中类之间的继承关系。
  • 执行视图 Execution View
    • 执行视图展示了系统运行时的时序结构特点,比如流程图、时序图等。
    • 执行实体可以最终分解到软件的基本元素和软件的基本结构,因而与软件代码具有比较直接的映射关系。
  • 实现视图 Implementation View
    • 实现视图是描述软件架构与源文件之间的映射关系。
    • 实现视图有助于码农在海量源代码文件中找到具体的某个软件单元的实现。
  • 部署视图 Deployment View
    • 部署视图是将执行实体和计算机资源建立映射关系。
  • 工作任务分配视图 Work-assignment View
    • 工作分配视图将系统分解成可独立完成的工作任务,以便分配给各项目团队和成员。

高质量软件

IEEE将软件质量定义为,一个系统、组件或过程符合指定要求的程度,或者满足客户或用户期望的程度。

  • 从用户的角度看,高质量就是恰好满足或超出了用户的预期目标。
  • 从工业生产的角度看,高质量就是符合标准规范的程度。
  • 从产品的角度看,高质量意味着产品具有良好的产品内在特性。
  • 从市场价值的角度看,高质量意味着客户愿意为此付费的数量。

几种重要的软件质量属性

  • 易于修改维护(Modifiability)
  • 良好的性能表现(Performance)
  • 安全性(Security)
  • 可靠性(Reliability)
  • 健壮性(Robustness)
  • 易用性(Usability)
  • 商业目标(Business goals)

软件危机和软件过程

软件危机的表现

  • 开发软件的成本日益增长、开发软件进度难以控制,软件质量不稳定,任何代码修改都可能造成其他代码意想不到的错误,结果导致软件难以维护。
  • 难以汇集众多参与人员的设计理念形成完整的、一致的软件复杂概念结构,从而使得大型软件项目往往会进展缓慢、成本暴涨及错误百出

没有银弹的含义

软件工程专家们所找到的各种方法都是舍本逐末,它们解决不了软件中的根本困难,即软件概念结构(conceptual structure)的复杂性,无法达成软件概念的完整性和一致性,自然无法从根本上解决软件危机带来的困境。

软件过程模型

分析、设计、实现、交付和维护五个阶段

描述性和说明性的过程

  • 描述性的过程试图客观陈述在软件开发过程中实际发生什么。
  • 说明性的过程试图主观陈述在软件开发过程中应该会发生什么。

原型化的瀑布模型

在瀑布模型的基础上增加一个原型化(prototyping)阶段,可以有效将风险前移,改善整个项目的技术和管理上的可控性。

原型就是根据需要完成的软件的一部分,完成哪一部分是根据开发原型的目标确定,比较常见的有用户接口原型和软件架构原型。

V模型

V模型也是在瀑布模型基础上发展出来的,我们发现单元测试、集成测试和系统测试是为了在不同层面验证设计,而交付测试则是确认需求是否得到满足。也就是瀑布模型中前后两端的过程活动具有内在的紧密联系,如果将模块化设计的思想拿到软件开发过程活动的组织中来,可以发现通过将瀑布模型前后两端的过程活动结合起来,可以提高过程活动的内聚度,从而改善软件开发效率。

增量和迭代开发过程

  • 增量开发就是从一个功能子系统开始交付,每次交付会增加一些功能,这样逐步扩展功能最终完成整个系统功能的开发。
  • 迭代开发是首先完成一个完整的系统或者完整系统的框架,然后每次交付会升级其中的某个功能子系统,这样反复迭代逐步细化最终完成系统开发

CMM/CMMI的5个等级

  • 初始级
  • 管理级
  • 已定义级
  • 量化管理级
  • 持续优化级

敏捷方法

互联网使得知识的获取变得更加容易,很多软件可以由一个小团队来实现。同时,技术更新的速度在加快,用户需求的变化也在加快,开发流程必须跟上这些快速变化的节奏。于是敏捷方法就产生了。

敏捷宣言:

  • 个体和互动 高于 流程和工具
  • 工作的软件 高于 详尽的文档
  • 客户合作 高于 合同谈判
  • 响应变化 高于 遵循计划
  • 也就是说,尽管右项有其价值,我们更重视左项的价值。

DevOps

DevOps(Development和Operations的组合)是一组过程、方法与系统的统称,用于促进软件开发、技术运营和质量保障(QA)部门之间的沟通、协作与整合。它的出现是由于软件行业日益清晰地认识到:为了按时交付软件产品和服务,开发和运营工作必须紧密合作。

以上是对本课程内容的总结,很荣幸本学期选修了孟宁老师的这门课程,学习到如此多软件工程领域的知识,相信这些知识会对我之后的编程生涯产生非常有益的影响。

学号后三位:290
参考资料:代码中的软件工程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值