0%

由于客观历史原因导致。为了快速收益初期不知情况下产生。定期不清理债务导致。

综合债务偿还规划

根据当前情况,除重写策略以外,其他策略同时进行。按阶段将债务问题逐步偿还。根据阶段性成果适当调整下个阶段的重点工作。

  • 达成共识 > 债务可见 > 及时止损 > 不断改善。

债务的体现

  • 代码在不同项目中复制迁移,并随着时间增加。
  • 代码虽然运行稳定,但是回归测试成本在增加。
  • 迭代维护越来越困难。
  • 新的feature开发周期越来越长,ROI降低。
  • 影响开发人员心态。

技术债务的组成

  • 代码债务:代码重复、坏味道、代码风格混乱、魔法值、条件太多、异常处理、代码风险、安全风险、严重耦合。
  • 设计债务:缺少设计、对非功能性认知不足,系统性风险。
  • 测试债务:缺乏测试、测试覆盖率不足和不使用测试。
  • 质量债务:缺乏稳定和健壮性的技术验证,版本控制模糊,环境参数混乱。
  • 文档债务:无技术文档、设计文档或者文档过期。

技术债务利息

技术债务和贷款买房的思维模式一样,如果借技术债务的收益大于利息的时候,大胆借!

  • 开发速度降低:通常项目正常情况下,在相同的时间间隔下,完成的任务是接近的。随着项目的迭代,项目单位时间内能完成的任务数明显下降,那很可能是技术债务太多导致的。
  • 单元测试代码覆盖率低:现在大部分语言都有单元测试覆盖率的检测工具,通过工具可以很容易知道当前项目单元测试覆盖率如何,如果覆盖率太低或者下降厉害,就说明存在技术债务了。
  • 代码规范检查的错误率高:静态代码分析工具提示的代码债务太多,存在大量历史代码就需要处理。
  • 在线bug越来越多:正常情况下,如果没有新功能开发,Bug 数量会越来越少。但是如果 Bug 数量下降很慢,甚至有增多的迹象,额外增加研发成本、不稳定的产品质量、难以维护的代码。
  • 业务共识问题:理解需求需要时间,加入一个新功能需要写 100 行代码,只要花 20 分钟。但是搞清楚这 100 行代码,应该加到哪个文件里,关联那些系统需要超长时间。

债务偿还策略

策略并没有绝对好坏,需要根据当前项目场景灵活选择。有个简单原则可以帮助你选择,那就是看哪一种策略投入产出ROI更好。

  • 重写:推翻重来,一次还清。
  • 维持:修修补补,只还利息。
  • 重构:新旧交替,分期付款。
  • 投资:好的架构设计、高质量代码就像一种技术投资,能有效减少技术债务的发生。
  • 测试:日常能做好代码审查、保障单元测试代码覆盖率,预防技术债务。
  • 还债:因为进度时间紧等客观原因,导致不得不走捷径,那么就应该把欠下的技术债务记下来,放到任务跟踪系统中,安排在后续的开发任务中,及时还债及时解决,就可以避免债务越来越多。

规划共识

防止技术债务产生的主要方法是了解开发团队存在的技术债务。开发团队必须全面了解技术债务,以及债务对项目的影响。合适的流程可以帮助开发团队避免技术债务积累,例如代码、设计、架构和测试的审查等。而且,这些流程必须是务实的,否则事与愿违。

  • PDCA(Plan-Do-Check-Act)它最早来⾃于质量管理领域,意思是做任何事情,都要经过规划(Plan)、执⾏(Do)、检查 (Check) 和⾏动 (Act) 这四个步骤。这四个步骤提供了⼀个简易的思考和做事框架。这个循环并不是运⾏⼀次就结束了,⽽是周⽽复始、螺旋上升的。
  • WBS(Work Breakdown Structure)任务分解。识别依赖及各环节关键路径,定义完成标准,达成共识并公开透明,变更即时调整。
  • MVP(Minimum Viable Product)最⼩化可实⾏产品。来自产品开发,不要⼀下⼦做⼀个「尽善尽美」的产品,⽽是先花费最⼩的代价做⼀个「可⽤」的产品原型,去验证这个产品是否有价值、是否可⾏,再通过迭代来完善细节。
  • ROI(Return on Investment)投资回报率,通常在广告业务作为重要的衡量标准。因为它是衡量广告投放花费的资金是否对于公司业务产生良好的实际影响,例如产生潜在客户、增加销售额、提升品牌知名度或者推动其他有价值的客户活动。
  • 效率先行。开发过程越快越好,越快越有竞争力。这的确是软件开发,尤其是互联网行业软件开发的不二法则。

实施步骤

  1. 团队内债务认知建立,技术债务会直接影响到公司业务的成长,间接影响个人的成长。
    • 技术债务产生的负作用,将直接影响相关业务线的发展。
  2. 采用低成本的方式进行预防。
    • 强化开发人员时间管理认知,帮助开发人员更好的安排时间高效完成工作。
  3. 识别债务并标记。
    • 服务、组件依赖混乱;文件、函数、包内容太多职责不清晰。
    • 魔法值、炫技、复杂设计、无设计。函数、接口、参数过多。
  4. 采用PDCA的方式循环迭代。
    • 标记出存在的负债问题。集中汇总到bug池。关联相关业务线。
    • 梳理业务边界,统一代码风格、完善开发文档、接口文档、接口单元测试。降低开发人员协调共识周期长的问题。
  5. 采用WBS方式标记、处理、公开相关问题。
    • 优先处理服务稳定性问题、线上紧急bug问题。
    • 根据bug与业务的关系程度划分处理级别。
  6. 采用MVP方式逐步迭代项目。
    • 落地持续改进能力;快速的增量交付。
    • 提早集成与测试;让问题得以及时暴露,带来了更快的反馈及应对。
    • 及时规避⻛险;意外仍然会有,但⼤多数情况下,急早发现问题,避免更⼤的影响。
    • 降低发布周期;降低修复试错成本。

经过90天的努力,体重最大相差20KG。按照月均计算,已经减重15KG。

为什么减重

已经造成了生活上的不便利,几年前精神旺盛,白天都不困。最近一年中午吃完饭就犯困。

  • 旅游容易暴饮暴食是真的。

  • 2021-11月初在苏州旅游的时候,因为体重原因不让玩水上项目。

  • 2021-11月11日我把床头坐塌了,我突然意识到问题的严重性了。

  • 2021-11月11日买了体脂秤,各项指标显示我的身体已经不非常不健康了,开始了减重。

减重规划

  • 第一阶段减重20KG。 2022-02-13 已达成目标。
  • 第二阶段减少脂肪。体脂率维持在20%。进行中
  • 第三阶段减少脂肪,体脂率维持在15%。

减重计划

  • 控制饮食摄入,在吃饱喝足的情况下。控制碳水、糖、反式脂肪摄入。
    • 营养摄入均衡。
    • 早上吃碳水、中午吃蛋白、晚上吃蔬菜。
  • 保持最低运动量,走路、跳绳。
  • 多喝水,每天必须喝2升水。
  • 保持睡眠时长,睡眠能减重是真的。

减重目标达成

再接再厉

体重已经下降很多了,下一步是就是健康。体脂率下降主要减少脂肪。

END

尝试下使用python Matplotlib可视化数据。

演示代码

  • python3.8运行环境
  • 测试数据,这里使用mysql提供的测试数据库。安装步骤
  • jupyter notebook
  • pip3 install pymysql
1
2
3
4
➜  jupyter-example git:(main) python --version
Python 3.8.7
➜ jupyter-example git:(main) jupyter notebook --version
6.2.0
阅读全文 »

尝试下使用python Matplotlib可视化数据。

演示代码

  • python3.8运行环境
  • 测试数据库
  • jupyter notebook
  • pip3 install pymysql
1
2
3
4
➜  jupyter-example git:(main) python --version
Python 3.8.7
➜ jupyter-example git:(main) jupyter notebook --version
6.2.0
阅读全文 »

尝试下使用python可视化数据。

云词

演示代码

  • python3.8运行环境
  • 测试数据库
  • jupyter notebook
  • pip3 install pymysql
  • pip3 install WordCloud
  • pip3 install jieba
1
2
3
4
➜  jupyter-example git:(main) python --version
Python 3.8.7
➜ jupyter-example git:(main) jupyter notebook --version
6.2.0
阅读全文 »

RediSearch 是一个高性能的全文搜索引擎,它可以作为一个 Redis Module(扩展模块)运行在 Redis 服务器上。

RediSearch

  • Redis 哈希中多个字段的全文索引
  • 无性能损失的增量索引
  • 文档排名(使用tf-idf,具有可选的用户提供的权重)
  • 场加权
  • 使用 AND、OR 和 NOT 运算符的复杂布尔查询
  • 前缀匹配、模糊匹配和精确短语查询
  • 支持双变音拼音匹配
  • 自动完成建议(带有模糊前缀建议)
  • 多种语言中基于词干的查询扩展(使用Snowball
  • 支持中文标记化和查询(使用Friso
  • 数字过滤器和范围
  • 使用Redis 地理空间索引进行地理空间搜索
  • 强大的聚合引擎
  • 支持所有 utf-8 编码文本
  • 检索完整文档、选定字段或仅检索文档 ID
  • 排序结果(例如,按创建日期)

官方文档

运行环境

  • docker
1
2
mkdir -p redisearch/data
docker run -p 6379:6379 -v $PWD/redisearch/data:/data -d redislabs/redisearch:latest
  • 检查安装
1
2
3
4
5
6
7
➜  docker-run redis-cli 
127.0.0.1:6379> MODULE LIST
1) 1) "name"
2) "search"
3) "ver"
4) (integer) 20015
127.0.0.1:6379> exit
  • 测试下
1
2
3
4
5
6
7
127.0.0.1:6379> FT.ADD idx docCn 1.0 LANGUAGE chinese FIELDS txt "Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。从盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。[8]"
OK
127.0.0.1:6379> FT.SEARCH idx "数据" LANGUAGE chinese HIGHLIGHT SUMMARIZE
1) (integer) 1
2) "docCn"
3) 1) "txt"
2) "<b>\xe6\x95\xb0\xe6\x8d\xae</b>\xe5... <b>\xe6\x95\xb0\xe6\x8d\xae</b>\xe8\xbf\x9b\xe8\xa1\x8c\xe5\x86\x99\xe6\x93\x8d\xe4\xbd\x9c\xe3\x80\x82\xe7\x94\xb1\xe4\xba\x8e\xe5\xae\x8c\xe5\x85\xa8\xe5\xae\x9e\xe7\x8e\xb0\xe4\xba\x86\xe5\x8f\x91\xe5\xb8\x83... <b>\xe6\x95\xb0\xe6\x8d\xae</b>\xe5\x86\x97\xe4\xbd\x99\xe5\xbe\x88\xe6\x9c\x89\xe5\xb8\xae\xe5\x8a\xa9\xe3\x80\x82[8... "
  • 对中文支持不够好
阅读全文 »

简单优化的下博客。

优化内容

  • SEO优化
  • 增加站内搜索
  • 增加网站地图
  • 添加字数统计和阅读时长
  • GitHub Fork Me
  • 允许复制代码
  • 图片懒加载

SEO优化

hexo 默认生成文章命名方式,在中文标题下很不友好。可以选择生成永久的链接。

  • 使用abbrlink插件
1
npm install hexo-abbrlink --save
  • 配置_config.yml
1
2
3
4
5
6
#permalink: :year/:month/:day/:title/
#permalink_defaults:
permalink: posts/:abbrlink/
abbrlink:
alg: crc32 #support crc16(default) and crc32
rep: dec #support dec(default) and hex
  • 生成的链接将会是这样的(官方样例):
1
2
3
4
5
6
7
8
9
10
crc16 & hex
https://post.x.com/posts/66c8.html

crc16 & dec
https://post.x.com/posts/65535.html
crc32 & hex
https://post.x.com/posts/8ddf18fb.html

crc32 & dec
https://post.x.com/posts/1690090958.html
阅读全文 »

编写第一个CLI小程序练手。

项目需求

  • 访问一个网站,输出页面内容成md文件。

初始化项目

1
cargo new learning-05
  • 使用vs code 打开项目。

添加依赖

  • 修改Cargo.toml
  • 引入 reqwest 和 html2md
    • reqwest 是一个http客户端
    • html2md 顾名思义就是html转markdown
1
2
reqwest = { version = "0.11", features = ["blocking"]}
html2md = "0.2"%

编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs;

fn main() {
let url = "https://z201.vip";
let output = "z201.md";
println!("url {} output {}",url,output);
let body = reqwest::blocking::get(url).unwrap().text().unwrap();

println!("Converting html to markdown...");
let md = html2md::parse_html(&body);

fs::write(output, md.as_bytes()).unwrap();
println!("Converted markdown has been saved in {}.", output);
}
  • Console
1
2
3
url https://z201.vip output z201.md
Converting html to markdown...
Converted markdown has been saved in z201.md.

迭代代码

版本一

  • 这里将url 和 output作为参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::fs;
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("args {:?} ",args);
// 获取第一个参数
let url = &args[1];
// 获取第二个参数
let output = &args[2];
// 判断参数是否为空,不知道这个是否有效。
assert_eq!(url.is_empty(),false);
assert_eq!(output.is_empty(),false);

println!("url {} output {}",url,output);
let body = reqwest::blocking::get(url).unwrap().text().unwrap();

println!("Converting html to markdown...");
let md = html2md::parse_html(&body);

fs::write(output, md.as_bytes()).unwrap();
println!("Converted markdown has been saved in {}.", output);
}
  • Console
1
2
3
4
5
6
7
➜  learning-05 git:(master) ✗ cargo run https://z201.vip z201.md
Finished dev [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/learning-05 'https://z201.vip' z201.md`
args ["target/debug/learning-05", "https://z201.vip", "z201.md"]
url https://z201.vip output z201.md
Converting html to markdown...
Converted markdown has been saved in z201.md.

版本二

  • 换一种方式来处理参数
  • 打包测试
增加依赖
1
2
3
reqwest = { version = "0.11", features = ["blocking"]}
html2md = "0.2"
structopt = "0.3.13"
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::fs;
use structopt::StructOpt; // 使用StructOpt传递参数

#[derive(StructOpt, Debug)]
struct Cli{
url:String,
output:String,
}

fn main(){
let args = Cli::from_args();
// println!("args {:?} ",args);
let url = &args.url;
let output = &args.output;
println!("url {}\n output {}",url,output);
let body = reqwest::blocking::get(url).unwrap().text().unwrap();
println!("Converting html to markdown...");
let md = html2md::parse_html(&body);
fs::write(output, md.as_bytes()).unwrap();
println!("Converted markdown has been saved in {}.", output);
}

#[test]
fn check() {
println!("test")
}

打包

1
cargo build --release
  • 慢慢等待

测试下

1
2
3
4
5
6
cd target/release
./learning-05 https://z201.vip z201.md
url https://z201.vip
output z201.md
Converting html to markdown...
Converted markdown has been saved in z201.md.

END

Rust的核心功能之一 ownership ,运行的程序都需要使用计算机管理内存的方式,比如Java 具备垃圾回收,还有一些语言需要手动去释放内存。而Rust则是第三种方式,通过所有权管理内存,编译器在编译时会根据一些列规则检查,在运行时所有权系统的任务功能都不会减慢程序。

  • 所有权和生命周期是 Rust 和其它编程语言的主要区别,也是 Rust 其它知识点的基础。

所有权

  • 栈是程序运行的基础。每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存被称为帧(frame)。

  • 栈以放入值的顺序存储并以相反的顺序取出值。这也被称为 后进先出 (last in , first out) 。添加数据的时候加 进栈 (pushing anto the stack) ,而移出数据叫 出栈 (poping off th stack)。

  • 在调用的过程中,一个新的帧会分配足够的空间存储寄存器的上下文。在函数里使用到的通用寄存器会在栈保存一个副本,当这个函数调用结束,通过副本,可以恢复出原本的寄存器的上下文,就像什么都没有经历一样。此外,函数所需要使用到的局部变量,也都会在帧分配的时候被预留出来。

  • 栈的操作时非常快的,这主要得益于它存取数据的方式,数据的存取位置总时在栈顶,而不需要重新寻找一个位置去存放或者读取。另一个属性就是,栈中所有的数据都必须占据已知且固定的大小。

  • 但是工作中我们依然要避免大量数据存放栈中,避免栈溢出(stack overfow),运行程序调用栈超出了系统运行的最大空间,就无法创建新的。就会出现溢出现象

在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上。

  • 堆主要存放大小未知,可能大小变化的数据,堆事缺乏组织的。当向堆中存放数据时,要申请一个空间,操作系统在堆的某处足够大的空间,把它标记为已经使用。并返回一个指针(pointer),这个过程叫做堆上分配内存(allocating on the heap)。
  • 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。当代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
案例一
1
2
3
4
5
fn say(name : String){
println("name {}" , name);
}
say("tomcat".to_string());
say("jetty".to_string());
  • 字符串的数据结构,在编译器是不确定的,运行期才代码才知道大小。比如tomcatjetty,当say方法执行的时候才知道参数的具体长度。
案例二
1
2
3
let mut arr = Vec::new();
arr.push(1);
arr.push(2);
  • 列表初始化长度是4,而不是2。在堆上内存分配会使用libc提供的mallo函数。系统调用的代价是昂贵的,所以我们要避免频繁地 malloc()。
内存泄漏

如果手工管理堆内存的话,堆上内存分配后忘记释放,就会造成内存泄漏。一旦有内存泄漏,程序运行得越久,就越吃内存,最终会因为占满内存而被操作系统终止运行。

堆越界

如果堆上内存被释放,但栈上指向堆上内存的相应指针没有被清空,就有可能发生使用已释放内存(use after free)的情况。

GC垃圾回收处理
  • 比如Java采用垃圾回收(GC),来自动管理内存,定期标记(mark)找出不在被引用的对象,然后清理(sweep)掉。
  • 但是GC是不确定的,可能引起STW(Stop The World)。
ARC自动引用计数处理方式
  • 在编译时,它为每个函数插入 retain/release 语句来自动维护堆上对象的引用计数,当引用计数为零的时候,release 语句就释放对象。

栈与堆

  • 栈上存放的数据是静态的,固定大小,固定生命周期;堆上存放的数据是动态的,不固定大小,不固定生命周期。
阅读全文 »

函数

函数遍布于 Rust 代码中,Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。

  • Rust 中的函数定义以 fn 开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。
  • 可以使用函数名后跟圆括号来调用我们定义过的任意函数。Rust 不关心函数定义于何处,只要定义了就行。

案例

1
2
3
4
5
6
7
8
fn main() {
println!("Hello, world!");
hello();
}

fn hello(){
print!("hello function")
}
  • Console
1
2
Hello, world!
hello function

函数参数

函数也可以被定义为拥有 参数(parameters),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。

  • 在函数签名中,必须 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。
  • 多个参数时,使用逗号分隔。

案例

1
2
3
4
5
6
7
8
fn number(x:i8){
print!("value {} ",x)
}

fn main() {
println!("Hello, world!");
number(1);
}
  • Console
1
2
Hello, world!
value 1
  • number函数声明一个x的参数,x的类型被指定为i8
阅读全文 »