中科大高级软件工程总结

学习收获和感想

本学期有幸选择了孟宁老师的高级软件工程课程,该课程从软件工程的基本概念,由浅入深,深刻全面的讲解了代码中的软件工程的内容。从一个码农必备的修养到软件开发中使用的工具。从一个具体的menu实例来展现软件工程的思想,从开始构建一个软件模型。此外学习了从需求分析到软件设计,再到软件设计的一些基础概论,最后学习了软件危机,“没有银弹”。
通过学习了解了软件开发的详细过程,以及设计一个软件的具体方法和实际设计时需要注意的事项。通过本门课程提升了我的软件设计能力,为今后的工作打下了坚实的基础。此外,我觉得孟老师讲授的高软,不像一些浮于表面的课程,而是以其20余年的代码编程经验和12年教学经验,真正的深入工程化编程实战,从实际的软件设计出发,来传授我们真正实用软件设计的知识。其不仅是一门课程,更像是一门科学,一门哲学。

MVVM模型+观察者模式

代码

在创建Vue对象的时候将View的id="app"与Model(JavaScript对象定义的data)绑定起来,这样’Hello Vue!'就会自动更新到View DOM元素中。View DOM元素button上的事件click绑定Vue对象的方法reverseMessage,这样点击button按钮就能触发reverseMessage,reverseMessage方法只是修改了Model中JavaScript对象定义的message,而页面却能神奇地自动更新message。就是模型数据绑定和DOM事件监听。

<!DOCTYPE html>
<html>
<head>
  <title>My first Vue app</title>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
    <button v-on:click="reverseMessage">Reverse Message</button>
  </div>
 
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue.js!'
      },
      methods: {
        reverseMessage: function () {
          this.message = this.message.split('').reverse().join('')
        }
      }
    })
  </script>
</body>
</html>

(2)编译视图

    <p>{{ message }}</p>
    <button v-on:click="reverseMessage">Reverse Message</button>
...
//创建Watcher在模型中监听视图中出现的占位符/表达式的每一个成员
var watcher = new Watcher("message"):
//绑定监听事件和method
node.addEventListener("click", "reverseMessage"):

Watcher:将Compiler的解析结果,与Observer所观察的对象连接起来建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM,称之为Watcher;

//创建Watcher,观察者模式中的观察者
var Watcher = function(exp, vm, cb){
    this.exp = exp; // 占位符/表达式的一个成员
    this.cb = cb; //更新视图的回调函数
    this.vm = vm; //ViewModel
    this.value = null;
    this.getter = parseExpression(exp).get;
    this.update();
};
 
Watcher.prototype = {
    get : function(){
        Dep.target = this;
        var value = this.getter?this.getter(this.vm):'';
        Dep.target = null;
        return value;
    },
    update :function(){
        var newVal = this.get();
        if(this.value != newVal){
            this.cb && this.cb(newVal, this.value);
            this.value = newVal;
        }
    }
};

(3)重载数据模型

Observer.prototype.transform = function(data){
    for(var key in data){
        this.defineReactive(data,key,data[key]);
    }
};
Observer.prototype.defineReactive = function(data, key, value){
    var dep = new Dep();
    Object.defineProperty(data, key ,{
        enumerable:true,
        configurable:false,
        get:function(){
            if(Dep.target){
                //添加观察者
                dep.addSub(Dep.target);
            }
            return value;
        },
        set:function(newVal){
            if(newVal == value){
                return;
            }
            //data[key] = newVal;//死循环!赋值还会调用set方法
            value = newVal;//为什么可以这样修改?闭包依赖的外部变量
            //遍历newVal
            this.transform(newVal);
            //发送更新通知给观察者
            dep.notify(newVal);
        }
    });
 
    //递归处理
    this.transform(value);
};

(4)观察者模式:将Compiler的解析结果,与Observer所观察的对象连接起来建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM,称之为Watcher;

//观察者模式中的被观察者的核心部分
var Dep = function(){
    this.subs = {};
};
Dep.prototype.addSub = function(target){
    if(!this.subs[target.uid]) {
        //防止重复添加
        this.subs[target.uid] = target;
    }
};
Dep.prototype.notify = function(newVal){
    for(var uid in this.subs){
        this.subs[uid].update(newVal);
    }
};
Dep.target = null;

总结

1、对于上述(1)中代码,创建Vue对象时需要做两件事:

  1. transform模型,重载模型的get和set方法
  2. 编译视图

2、在transform模型时,调用如下方法,重载模型的get和set方法,但重载并未调用set和get方法

Observer.prototype.transform = function(data){
    for(var key in data){
        this.defineReactive(data,key,data[key]);
    }
};

3、编译视图

编译试图主要创建Watcher观察者,以及绑定监听事件。

//创建Watcher在模型中监听视图中出现的占位符/表达式的每一个成员
var watcher = new Watcher("message"):
//绑定监听事件和method
node.addEventListener("click", "reverseMessage"):

在创建Watcher时,首先初始化exp占位符,vm,cb。然后调用this.getter = parseExpression(exp).get获取其要监听的占位符exp的一个成员对应的get方法初始化给this.getter,然后调用this.update(),在该函数中会调用get方法,这里的get方法既是exp占位符的一个成员的get方法。在该get方法中dep.addSub(Dep.target),把该观察者watcher加入到新建的dep被观察者的sub列表中

//创建Watcher,观察者模式中的观察者
var Watcher = function(exp, vm, cb){
    this.exp = exp; // 占位符/表达式的一个成员
    this.cb = cb; //更新视图的回调函数
    this.vm = vm; //ViewModel
    this.value = null;
    this.getter = parseExpression(exp).get;
    this.update();
};
 
Watcher.prototype = {
    get : function(){
        Dep.target = this;
        var value = this.getter?this.getter(this.vm):'';
        Dep.target = null;
        return value;
    },
    update :function(){
        var newVal = this.get();
        if(this.value != newVal){
            this.cb && this.cb(newVal, this.value);
            this.value = newVal;
        }
    }
};

4、模型中数据更改,自动更新

当在如下method方法中修改message时,“=”会被拦截,触发对应模型transform的set方法,该set方法是一个闭包,在其中首先判断是否和原值相等,若不想等则更新value = newVal,在此使用此方法更新类似于直接操作内存,因此不会像data[key]=newVal一样再次出发set方法形成循环递归。而此处的value有相当于C++里的指针或者引用,更新会同时修改传递过来的data[key]。
在set方法中会递归调用transform,因为newVal可能也包含value值需要重载其get和set方法。然后调用dep.notify(newVal)方法,便会遍历把更新通知发给对应的观察者watcher,再调用watcher中的update方法,执行val newVal=this.get()获取的newVal既是从模型中获取的变化的值,接着判断和this.value(view中原本的值)不相等,则调用回调函数更新视图。

      methods: {
        reverseMessage: function () {
          this.message = this.message.split('').reverse().join('')
        }
      }
 
set:function(newVal){
            if(newVal == value){
                return;
            }
            //data[key] = newVal;
//死循环!赋值还会调用set方法
            value = newVal;
//为什么可以这样修改?闭包依赖的外部变量
            //遍历newVal
            this.transform(newVal);
            //发送更新通知给观察者
            dep.notify(newVal);
        }
}
 
 
 
//发送更新通知给观察者        
Dep.prototype.notify = function(newVal){
    for(var uid in this.subs){
        this.subs[uid].update(newVal);
    }
};
//观察者更新视图
    update :function(){
        var newVal = this.get();
        if(this.value != newVal){
            this.cb && this.cb(newVal, this.value);
            this.value = newVal;
        }

课程总结

工欲善其事必先利其器

VSCODE

通过VSCODE这一软件案例,讲述了一个优秀的软件应该具备的特质和需要具备的前瞻性的架构决策和扎实的工程基础。

GIT

Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。此外也是管理Linux内核开发而开发的一个开放源码的版本控制软件。

//git本地版本库操作基本方法
3. git init # 初始化一个本地版本库
4. git status # 查看当前工作区(workspace)的状态
5. git add [FILES] # 把文件添加到暂存区(Index) 
6. git commit -m "wrote a commit log infro” # 把暂存区里的文件提交到仓库
7. git log # 查看当前HEAD之前的提交记录,便于回到过去
8. git reset —hard HEAD^^/HEAD~100/commit-id/commit-id的头几个字符 # 回退
9. git reflog # 可以查看当前HEAD之后的提交记录,便于回到未来
10. git reset —hard commit-id/commit-id的头几个字符 # 回退
(2)git远程版本库的基本用法

 11. git clone    克隆一个存储库到一个新的目录下。
 12. git fetch    下载一个远程存储库数据对象等信息到本地存储库。
 13. git push     将本地存储库的相关数据对象更新到远程存储库。 
 14. git merge  合并两个或多个开发历史记录。
 15. git pull       从其他存储库或分支抓取并合并到当前存储库的当前分支。
(3)团队合作
1、克隆或同步最新的代码到本地存储库 
git clone https://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git
git pull
2、为自己的工作创建一个分支,该分支应该只负责单一功能模块或代码模块的版本控制;
git checkout -b mybranch
git branch
3、在该分支上完成某单一功能模块或代码模块的开发工作;多次进行如下操作:
git add FILES
git commit -m "commit log"
4、最后,先切换回master分支,将远程origin/master同步最新到本地存储库,再合并mybranch到master分支,推送到远程origin/master之后即完成了一项开发工作。
git checkout master
git pull
git merge --no-ff mybranch
git push

vim

Vim是一个类似于Vi的著名的功能强大、高度可定制的文本编辑器,在Vi的基础上改进和增加了很多特性。讲述了vim的基本用法。其具备三种模式:命令模式,输入模式,底线命令模式。
vim

正则表达式

正则表达式,又称规则表达式,是一种文本模式,包括普通字符和特殊字符,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。
主要讲述了正则表达式的使用方法,以及贪婪匹配和懒惰匹配的区别,捕获组等知识。

工程化编程实战——代码中的软件工程

本章主要通过menu实例来带领我们进行编程实战,通过实验来培养学生的软件工程实战能力。主要学习到了一些软件工程设计中的知识。

编写高质量代码的方法

  1. 通过控制结构简化代码
  2. 通过数据结构简化代码
  3. 设置错误处理
  4. 注意性能优先策略背后隐藏的代价
  5. 拒绝修修补补,要不断重构代码

模块化软件设计

主要讲述了模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns)。软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

此外还有软件设计中的一些基本方法:KISS原则,使用本地化外部接口来提高代码的适应能力,先写伪代码的代码结构更好。

可重用软件设计

主要讲述了消费者和生产者重用,接口等相关知识。
(1)接口的五个基本要素

  • 接口的目的
  • 接口的前置条件
  • 使用接口双方遵守的协议规范
  • 接口的后置条件
  • 接口的质量属性

(2)通用接口定义基本方法

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

(3)RESTful API的四种基本操作

  • GET用来获取资源;
  • POST用来新建资源(也可以用于更新资源);
  • PUT用来更新资源;
  • DELETE用来删除资源。

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

从需求分析到软件设计

主要讲述了需求的类型,需求分析的方法,高质量需求的要求,什么是用例,敏捷统一过程等知识。

需求的类型

  • 功能需求:根据所需的活动描述所需的行为
  • 质量需求或非功能需求:描述软件必须具备的一些质量特性
  • 设计约束(设计约束): 设计决策,例如选择平台或接口组件
  • 过程约束(过程约束): 对可用于构建系统的技术或资源的限制

高质量需求

  • 需求可测试
  • 解决冲突
  • 需求特点(正确性,持续性,无二义性,完整性,可行性,可追溯,相关性)

需求分析的方法

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

用例

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

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

软件科学基础概论

主要讲述了什么是软件,软件的基本构成,软件的基本结构,软件中的一些而特殊机制。此外重点讲述了设计模式相关的内容,以及常用的设计模式的优缺点。此外讲述了MVVM模式,观察者模式。软件结构的描述方法,软件质量的定义,提升软件性能的方法。

统一过程

统一过程(UP,Unified Process)的核心要义是 用例驱动 (Use case driven)、 以架构为中心(Architecture centric)、 增量且迭代 (Incremental and Iterative)的过程。用例驱动就是我们前文中用例建模得到的用例作为驱动软件开发的目标;以架构为中心的架构是后续软件设计的结果,就是保持软件架构相对稳定,减小软件架构层面的重构造成的混乱;增量且迭代体现在下图中。
统一过程

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

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

显然,第一到第三步主要是计划阶段的工作,第四步是接下来要进一步详述的增量阶段的工作。
在每一次增量阶段的迭代过程中,都要进行从需求分析到软件设计实现的过程,具体敏捷统一过程
将增量阶段分为五个步骤:

  • 用例建模(Use case modeling);
  • 业务领域建模(Domain modeling);
  • 对象交互建模(Object Interaction modeling),使用剧情描述来建模,最后转换为剧情描述表;
  • 形成设计类图(design class diagram);
  • 软件的编码实现和软件应用部署;

七大原则

  • 单一职责原则 (Single Responsibility Principle)
  • 开放­关闭原则 (Open­Closed Principle)
  • 里氏替换原则 (Liskov Substitution Principle)
  • 依赖倒转原则 (Dependence Inversion Principle)
  • 接口隔离原则 (Interface Segregation Principle)
  • 迪米特法则(Law Of Demeter)
  • 组合/聚合复用原则 (Composite/Aggregate Reuse Principle)

设计模式

设计模式的本质是面向对象设计原则的实际运用总结出的经验模型。目的是包容变化,即通过使用设计模式和多态等特殊机制,将变化的部分和不变的部分进行适当隔离。(高内聚,低耦合)

优点:
  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
组成:
  • 该设计模式的名称;
  • 该设计模式的目的,即该设计模式要解决什么样的问题;
  • 该设计模式的解决方案;
  • 该设计模式的解决方案有哪些约束和限制条件。
分类(根据作用对象)
  • 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。比如模板方法模式等属于类模式。
    对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。由于组合关系或聚合关系比继承关系耦合度低,因此多数设计模式都是对象模式。

MVC与MVVM

主要区别在于,MVC中,用户对于M的操作是通过C传递的,然后C将改变传给V,并且M将在发生变化时通知V,然后V通过C获取变化;在MVVM中,用户直接与V交互,通过VM将变化传递给M,然后M改变之后通过VM将数据传递给V,从而实现解耦。
另一个区别是,当M的数据需要进行解析后V才能使用时,C若承担解析的任务,就会变得很臃肿;在MVVM中,VM层承担了数据解析的工作,这时C就只需要持有VM,而不需要直接持有M了。从而完成了数据的解耦。

软件架构复用

  1. 克隆,完整地借鉴相似项目的设计方案,甚至代码,只需要完成一些细枝末节处的修改适配工
    作。
  2. 重构,构建软件架构模型的基本方法,通过指引我们如何进行系统分解,并在参考已有的软件架
    构模型的基础上逐步形成系统软件架构的一种基本建模方法。
架构分解方法
  • 面向功能的分解方法,用例建模即是一种面向功能的分解方法;
  • 面向特征的分解方法,根据数量众多的某种系统显著特征在不同抽象层次上划分模块的方法;
  • 面向数据的分解方法,在业务领域建模中形成概念业务数据模型即应用了面向数据的分解方法;
  • 面向并发的分解方法,在一些系统中具有多种并发任务的特点,那么我们可以将系统分解到不同的并发任务中(进程或线程),并描述并发任务的时序交互过程;
  • 面向事件的分解方法,当系统中需要处理大量的事件,而且往往事件会触发复杂的状态转换关系,这时系统就要考虑面向事件的分解方法,并内在状态转换关系进行清晰的描述;
  • 面向对象的分解方法,是一种通用的分析设计范式,是基于系统中抽象的对象元素在不同抽象层次上分解的系统的方法。
软件架构的描述方法
  • 分解视图 Decomposition View
  • 依赖视图 Dependencies View
  • 泛化视图 Generalization View
  • 执行视图 Execution View
  • 实现视图 Implementation View
  • 部署视图 Deployment View
  • 工作任务分配视图 Work­assignment View

没有银弹的含义

“在10年内无法找到解决软件危机的杀手锏(银弹)。
软件中的根本困难,即软件概念结构(conceptual structure)的复杂性,无法达成软件概念的完整性和一致性,自然无法从根本上解决软件危机带来的困境。

软件危机与软件过程

主要讲述了软件危机,软件的生命周期,软件过程,软件分阶段开发的方法,以及团队的重要性。

软件的生命周期

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

  • 分析阶段的任务是需求分析和定义,分析阶段一般会在深入理解业务的情况下,形成业务概念原 型 2
  • 设计阶段分为软件架构设计和软件详细设计,前者一般和分析阶段联系紧密,一般合称为“分析 与设计”;后者一般和实现阶段联系紧密,一般合称为“设计与实现”。
  • 实现阶段分为编码和测试,其中测试又涉及到单元测试、集成测试、系统测试等。
  • 交付阶段主要是部署、交付测试和用户培训等。
  • 维护阶段一般是软件生命周期中持续时间最长的一个阶段,而且在维护阶段很可能会形成单独 的项目,从而经历分析、设计、实现、交付几个阶段,最终又合并进维护阶段。

软件过程

软件过程又分为描述性的(descriptive)过程和说明性的(prescriptive)过程。

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

采用不同的过程模型时应该能反映出要达到的过程目标,比如构建高质量软件、早发现缺陷、满足预算和日程约束等。不同的模型适用于不同的情况,我们常见的过程模型,比如瀑布模型、 V模型 、原型化模型等都有它们所能达到的过程目标和适用的情况。

V模型

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

分阶段开发

分阶段开发的交付策略分为两种,一是增量开发(Incremental development),二是迭代开发(Iterative development)。

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

敏捷宣言

  • 个体和互动 高于 流程和工具
  • 工作的软件 高于 详尽的文档
  • 客户合作 高于 合同谈判
  • 响应变化 高于 遵循计划

团队

基本要素
  • 团队的规模
  • 团队的凝聚力
  • 团队协作的基本条件
评价方法

CMM/CMMI用于评价软件生产能力并帮助其改善软件质量的方法,成为了评估软件能力与成熟度的
一套标准,它侧重于软件开发过程的管理及工程能力的提高与评估,是国际软件业的质量管理标
准。
CMMI共有5个级别,代表软件团队能力成熟度的5个等级,数字越大,成熟度越高,高成熟度等级
表示有比较强的软件综合开发能力。

  • 一级,初始级,软件组织对项目的目标与要做的努力很清晰,项目的目标可以实现。但主要取决于实施人员。
  • 二级,管理级,软件组织在项目实施上能够遵守既定的计划与流程,有资源准备,权责到人,对项目相关的实施人员进行了相应的培训,对整个流程进行监测与控制,并联合上级单位对项目与流程进行审查。这级能保证项目的成功率。
  • 三级,已定义级,软件组织能够根据自身的特殊情况及自己的标准流程,将这套管理体系与流程予以制度化。科学管理成为软件组织的文化与财富。
  • 四级,量化管理级,软件组织的项目管理实现了数字化,降低了项目实施在质量上的波动。
  • 五级,持续优化级,软件组织能够充分利用信息资料,对软件组织在项目实施的过程中可能出现的问题予以预防。能够主动地改善流程,运用新技术,实现流程的优化。

什么是DevOps

  • DevOps(Development和Operations的组合)是一组过程、方法与系统的统称,用于促进软件开发、技术运营和质量保障(QA)部门之间的沟通、协作与整合。它的出现是由于软件行业日益清晰地认识到:为了按时交付软件产品和服务,开发和运营工作必须紧密合作。
  • 可以看作软件工程,技术运营,质量保障三者的交集。
  • 传统的软件组织将开发、IT运营和质量保障设为各自分离的部门。DevOps是一套针对这几个部门间沟通与协作问题的流程、方法和系统。
  • 学术界和软件工程实践者并没有对DevOps给出统一的定义,但是从学术的角度看,计算机科学研究人员建议将DevOps定义为“一套旨在缩短从提交变更到变更后的系统投入正常生产之间的时间,同时确保产品高质量的实践方法。

参考文献

代码中的软件工程https://gitee.com/mengning997/se

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余生.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值