Skip to main content

· 19 min read
天山飞猫

用一句话概括《A Philosophy of Software Design》,软件设计的核心在于降低复杂性

Chapter 1 Introduction(It’s All About Complexity)

  • 随着时间的流逝,复杂性不断累积,程序员在修改系统时将所有相关因素牢记在心中变得越来越难。这会减慢开发速度并导致错误,从而进一步延缓开发速度并增加成本。在任何程序的生命周期中,复杂性都会不可避免地增加。程序越大,工作的人越多,管理复杂性就越困难。
  • 有两种解决复杂性的通用方法。
    1. 通过使代码更简单和更明显来消除复杂性。
    2. 封装它,以便程序员可以在系统上工作而不会立即暴露其所有复杂性。这种方法称为模块化设计。
  • 为什么采用诸如敏捷开发之类的增量方法:
    1. 通过以这种方式扩展设计,可以在系统仍然很小的情况下解决初始设计的问题。较新的功能受益于较早功能的实施过程中获得的经验,因此问题较少。
    2. 增量方法适用于软件,因为软件具有足够的延展性,可以在实施过程中进行重大的设计更改。
    3. 增量开发意味着永远不会完成软件设计。设计在系统的整个生命周期中不断发生。增量开发还意味着不断的重新设计。
  • 如果软件开发人员应始终考虑设计问题,而降低复杂性是软件设计中最重要的要素,则软件开发人员应始终考虑复杂性。

Chapter 2 The Nature of Complexity

2.1 Complexity defined

  • 如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。
  • 在复杂的系统中,要实施甚至很小的改进都需要大量的工作。在一个简单的系统中,可以用更少的精力实现更大的改进。
    C=pcptpC=\sum_{p}c_{p}t_{p}
  • 系统的总体复杂度(C)由每个部分的复杂度(cp)乘以开发人员在该部分上花费的时间(tp)加权。在一个永远不会被看到的地方隔离复杂性几乎和完全消除复杂性一样好。
  • 避免主观偏见:读者比作家更容易理解复杂性。作为开发人员,不仅要创建可以轻松使用的代码,而且还要创建其他人也可以轻松使用的代码。

2.2 Symptoms of complexity

  1. 变更放大:复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。
  2. 认知负荷:复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。 系统设计人员有时会假设可以通过代码行来衡量复杂性。他们认为,如果一个实现比另一个实现短,那么它必须更简单;如果只需要几行代码就可以进行更改,那么更改必须很容易。但是,这种观点忽略了与认知负荷相关的成本。我已经看到了仅允许使用几行代码编写应用程序的框架,但是要弄清楚这些行是什么极其困难。有时,需要更多代码行的方法实际上更简单,因为它减少了认知负担。

    另一个要考虑的是性能和可读性的选择,不同的场景下有不同的选择。

  3. 未知的未知:复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。

2.3 Causes of complexity

复杂性是由两件事引起的:依赖性和模糊性。

  1. 依赖关系是软件的基本组成部分,不能完全消除。但是,软件设计的目标之一是减少依赖关系的数量,并使依赖关系保持尽可能简单和明显。
  2. 减少模糊性的最佳方法是简化系统设计。

2.4 Complexity is incremental

复杂性不是由单个灾难性错误引起的;它是积少成多的一个过程。复杂性的增量性质使其难以控制。

这也是为什么重构要发生在任何时候,小的重构即安全又可以减少复杂性。

2.5 Conclusion

Chapter 3 Working Code Isn’t Enough(Strategic vs. Tactical Programming)

3.1 Tactical programming

几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员:战术龙卷风。战术龙卷风是一位多产的程序员,他抽出代码的速度比其他人快得多,但完全以战术方式工作。实施快速功能时,没有人能比战术龙卷风更快地完成任务。在某些组织中,管理层将战术龙卷风视为英雄。但是,战术龙卷风留下了毁灭的痕迹。他们很少被将来必须使用其代码的工程师视为英雄。通常,其他工程师必须清理战术龙卷风留下的混乱局面,这使得那些工程师(他们是真正的英雄)的进步似乎比战术龙卷风慢。 那从管理的角度来看,这也属于技术债务管理的范畴,如何保持清醒的头脑面对捷径和债务是最重要的。

3.2 Strategic programming

成为一名优秀的软件设计师的第一步是要意识到仅工作代码是不够的。引入不必要的复杂性以更快地完成当前任务是不可接受的。最重要的是系统的长期结构。任何系统中的大多数代码都是通过扩展现有代码库编写的,因此,作为开发人员,最重要的工作就是促进这些将来的扩展。因此,尽管您的代码当然必须工作,但您不应将“工作代码”视为主要目标。您的主要目标必须是制作出出色的设计,并且这种设计也会起作用。这是战略计划。 这也是为什么代码Review显得如此重要的原因了,当然怎么进行CR那就是另一个大的话题了

3.3 How much to invest?

作者给了一个拍脑袋的 10%-20% 的时间。 以我的个人经验来看,作为架构师,我的时间差不多有50%都会投入在与此相关的活动中;作为开发者的话很可能也会高于20%,因为我不太能容忍大量的重复和非显式的依赖。

3.4 Startups and investment

3.5 Conclusion

Chapter 4 Modules Should Be Deep

不幸的是,深度类的价值在今天并未得到广泛认可。编程中的传统观点是,类应该小而不是深。经常告诉学生,类设计中最重要的事情是将较大的类分成较小的类。 正是这些设计的观点造成的不便,在java世界中充满了各种 xxxUtil 或 xxxHelper 的公共方法类去对相关的冗长的吟唱式的调用模型进行了简单的封装。 我更倾向于短小的方法和内容自洽的类。

Chapter 5 Information Hiding (and Leakage)

在时间分解中,执行顺序反映在代码结构中:在不同时间发生的操作在不同的方法或类中。如果在执行的不同点使用相同的知识,则会在多个位置对其进行编码,从而导致信息泄漏。 这种现象太常见了,大概因为人类的认知本能决定的吧。

Chapter 6 General-Purpose Modules are Deeper

这个方向值得注意,是我既往工作中忽视的那一部分

Chapter 7 Different Layer, Different Abstraction

  1. 当相邻的层具有相似的抽象时,问题通常以直通方法的形式表现出来。 在常见的分层结构的java项目中很常见,从dao层到service层再到controller层,恨不能都是一样的,给前端暴露的接口像是一个专有化的操作表的接口。
  2. 慎用装饰器设计模式

Chapter 8 Pull Complexity Downwards

尽量在自己的范围内处理好自己能做的,而不是简答的将一切都交给其他人。不这光在软件设计上是这样,产品设计上更是这样,千万别把自己该干的推给用户,还美其名曰是提供了足够的个性化。

Chapter 9 Better Together Or Better Apart?

考量点有以下:

  1. 信息共享
  2. ​简化接口
  3. 消除重复

Chapter 10 Define Errors Out Of Existence

最近的一项研究发现,分布式数据密集型系统中超过 90%的灾难性故障是由错误的错误处理引起的。当异常处理代码失败时,很难调试该问题,因为它很少发生。 减少异常处理程序数量的四种技术:

  1. 设计不存在异常的api
  2. 异常屏蔽 在更底层的处理层把异常消化掉,很多工具类都在做类似的事情。Apache-Commons、Guava、Hutool
  3. 异常聚合
  4. 使应用程序崩溃 最常见的就是springboot里面的 fail-fast 了吧。

Chapter 11 Design it Twice

可以翻译成三思而行,成功的人们很容易满足在既有的成功经验中,而不停重复让他们成功的方式。但残酷的现实是,没有思考过优缺点而做出的决定很可能在环境变化后产生非预期的结果。

Chapter 12 Why Write Comments? The Four Excuses

核心原因是两点,没见过好的注释,不会写好的注释。在团队文化中需要好的范例和每次CodeReview的过程潜移默化的来影响团队成员。

Chapter 13 Comments Should Describe Things that Aren’t Obvious from the Code

在注释变量声明(例如类实例变量,方法参数和返回值)时,精度最有用。变量声明中的名称和类型通常不是很精确。注释可以填写缺少的详细信息,例如:

  • 此变量的单位是什么?
  • 边界条件是包容性还是排他性?
  • 如果允许使用空值,则意味着什么?
  • 如果变量引用了最终必须释放或关闭的资源,那么谁负责释放或关闭该资源?
  • 是否存在某些对于变量始终不变的属性(不变量),例如“此列表始终包含至少一个条目”? 方法的接口注释包括抽象层面的高级信息和精确性的低级细节:
  • 注释通常以一两个句子开头,描述调用者感知到的方法的行为。这是更高层次的抽象。
  • 注释必须描述每个参数和返回值(如果有)。这些注释必须非常精确,并且必须描述对参数值的任何约束以及参数之间的依赖关系。
  • 如果该方法有任何副作用,则必须在接口注释中记录这些副作用。副作用是该方法的任何结果都会影响系统的未来行为,但不属于结果的一部分。例如,如果该方法将一个值添加到内部数据结构中,可以通过将来的方法调用来检索该值,则这是副作用。写入文件系统也是一个副作用。
  • 方法的接口注释必须描述该方法可能产生的任何异常。
  • 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法;对于二进制搜索方法,必须对要搜索的列表进行排序)。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。

Chapter 14 Choosing Names

这是一个特别容易被忽视的点,应该在项目文档中就定义好命名的规范,并充分考虑到项目及团队的具体情况。如果能保证团队所有人都在utf-8的环境下工作,我不反对直接使用中文来命名变量和方法名,毕竟他们总好过用拼音首字母或拼音来命名。

Chapter 15 Write The Comments First(Use Comments As Part Of The Design Process)

  • 对于新类,我首先编写类接口注释。
  • 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主体保留为空。
  • 我对这些注释进行了迭代,直到基本结构感觉正确为止。
  • 在这一点上,我为类中最重要的类实例变量编写了声明和注释。
  • 最后,我填写方法的主体,并根据需要添加实现注释。
  • 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。例如变量,我在编写变量声明的同时填写了注释。 有点TDD的意思,从我的经验来说,这是一个靠谱的方法。 而且尽早的写注释,还能在重构时需要重新设计时,提供当初的决策原因。

Chapter 16 Modifying Existing Code

Chapter 17 Consistency

可以通过 IDE的统一格式化标准 -> 代码静态检查工具 -> CodeReview 来保证。

Chapter 18 Code Should be Obvious

我喜欢引用的一个设计原则 KISS -> "Keep It Simple! Stupid!!!" 我更喜欢强调Stupid这个单词,减轻别人的认知障碍,是提高沟通效率的重要手段。

Chapter 19 Software Trends

Chapter 20 Designing for Performance

需要识别性能的瓶颈,解决关键节点的性能问题。当然也有一种情况在老系统中比较常见,在经历了几个不负责任的工程师后,在补丁上打补丁的情况会出现,这时,就需要去识别清楚调用链的情况去重构或重写这部分逻辑了。

Chapter 21 Decide What Matten

不同的项目的关注点是不同的,甚至一些一次性的嵌入式设备最关注的是使用寿命,那么省电就变得极其重要了。

Conclusion

软件设计的核心在于降低复杂性

· 2 min read
天山飞猫

今天是 2023年6月27日,我开始阅读《Machine Learning Bookcamp》,并将自己的读后感,逐步维护在本篇日志中。

Let's begin!

1. Introduction to Machine Learning

1.1 Introduction to Machine Learning

ML 是从现有的数据中提取模式的过程,有点像数学的归纳法,但完全没有归纳法中证明的部分。 从这个角度来说,ML是有其局限性的:

  • 他需要大量的数据来训练
  • 当新的数据和训练数据有巨大差异的情况下,他的表现很难相信 这也带来两个个新的需求:
  • 数据,大量的数据
  • 计算能力,更快的计算能力

1.2 ML vs Rule-Based Systems

基于规则的系统:

  • 数据 + 代码(把规则描述的算法用代码的形式表达) => 软件
  • 在使用过程中,由于数据中出现的新情况,我们要不断的维护代码;最终,要么运行变慢,要么难以维护

机器学习:

  • 数据 + 结果 => 模型
  • 在使用过程中,只有当新的模式出现,我们才需要重新训练模型

· 4 min read
天山飞猫

前一阵发现了一个号称比Redis性能高25倍的新内存数据库出现了,叫 Dragonfly

而且他还全面支持redis的api,今天终于有空了,把这个新玩具跑起来看看,是不是真的和他说的一样。

0. 部署

直接Docker部署还是很方便的。我本机为了测试和开发方便,都是用docker-compose管理的。大概这样: docker-compose.yml

version: '3'
services:
mysql:
image: mysql
restart: always
environment:
- MYSQL_ROOT_PASSWORD=123456
volumes:
- d:\\Dockers\\Mysql8:/var/lib/mysql
- d:\\Dockers\\Mysql-files:/var/lib/mysql-files
ports:
- 3306:3306
redis:
restart: always
image: redis
ports:
- 6379:6379
dragonfly:
restart: always
image: docker.dragonflydb.io/dragonflydb/dragonfly
ports:
- 6378:6379
rabbitmq:
image: mayan31370/docker-image-rabbitmq:delayed-exchange_stomp
restart: always
ports:
- 5672:5672
- 15672:15672
- 61613:61613
mongo:
restart: always
image: mongo

1. 用JMH测试一波试试

建了一个简单的JMH项目,写了一下测试


import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;

@State(Scope.Benchmark)
public class MyBenchmark {

private StatefulRedisConnection<String, String> redisConnection;
private StatefulRedisConnection<String, String> dragonflyConnection;

@Setup(Level.Trial)
public void init() {
redisConnection = RedisClient.create(
RedisURI.builder().withHost("localhost").withPort(6379).build()).connect();
dragonflyConnection = RedisClient.create(
RedisURI.builder().withHost("localhost").withPort(6378).build()).connect();
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
public void testRedisOpsForValue() {
redisConnection.sync().set("a","1");
redisConnection.sync().get("a");
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void testDragonflyOpsForValue() {
dragonflyConnection.sync().set("a","1");
dragonflyConnection.sync().get("a");
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
public void testRedisOpsForHash() {
redisConnection.sync().hset("b","b","1");
redisConnection.sync().hget("b","b");
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void testDragonflyOpsForHash() {
dragonflyConnection.sync().hset("b","b","1");
dragonflyConnection.sync().hget("b","b");
}
@TearDown(Level.Trial)
public void finish() {
redisConnection.close();
dragonflyConnection.close();
}
}

2. 结果分析

BenchmarkModeCntScoreErrorUnits
MyBenchmark.testDragonflyOpsForHashthrpt153966.922±866.481ops/s
MyBenchmark.testDragonflyOpsForValuethrpt153904.415±955.113ops/s
MyBenchmark.testRedisOpsForHashthrpt156836.774±615.484ops/s
MyBenchmark.testRedisOpsForValuethrpt156940.175±602.979ops/s

这里用了同步操作,只能看出两个速度差不多,甚至redis在单线程上的性能更加的优秀

通过redis-cli查看内存使用情况,也是redis更胜一筹。 Dragonfly会比Redis多占用30%的内存。

3. 使用 memtier_benchmark 来进行并发测试

memtier_benchmark 是一个由Redis Labs开发的命令行工具,专门用来测试非关系型键值对的数据库的性能。 转送门:https://github.com/RedisLabs/memtier_benchmark 按照README指引,一路操作即可,我是用的win10的WSL2中的ubuntu22.04,你呢?

1          Threads
30 Connections per thread
20000 Requests per client

Set key value

Redis

CMDOps/secAvg. Latencyp99 Latencyp99.99 LatencyKB/sec
Set19591.091.548033.0230010.87900707.88
HSet19206.981.560392.799006.015001198.26
SAdd19053.651.572932.975008.383001002.62
LUA18514.211.618502.911007.135001120.98

Dragonfly

CMDOps/secAvg. Latencyp99 Latencyp99.99 LatencyKB/sec
Set16645.991.762233.215009.47100601.47
HSet16178.981.847373.7590012.607001009.35
SAdd16709.351.818623.3910012.15900879.26
LUA569.6053.0916861.1830070.6550034.49

不知道是我配置的原因,还是Dragonfly只在高配服务器上有更加优秀的表现,这个有待进一步的测试,就我的开发环境,还是老实用Redis吧。 尤其lua脚本的执行效率,实在有点令人发指。

· 2 min read
天山飞猫

最近AI话题是超级火热,我也加入玩玩,正好家里有几台3070显卡的游戏本,这下可以好好玩玩了。

我这里采用最简单的安装方法,不过因为在国内,部分资源访问比较慢,所以主要精力都放在找镜像资源上了。

0. 基础准备

  • 更新显卡驱动
  • 安装特定版本的cuda(11.7)
  • 安装python
  • 安装git

1. 安装Stable-Diffusion-WebUI

我不知道你访问Github的速度怎么样,反正我这里的网络是时好时断的,然后我找到了这个:

ghproxy.com GitHubProxy,这个域名还挺好记得 这下就方便了,找个地方开始吧

git clone https://ghproxy.com/https://github.com/AUTOMATIC1111/stable-diffusion-webui.git

真的是速度惊人啊

2. 配置pip镜像

理论上,我们可以开始运行了,双击webui-user.bat文件就行了。 。。。。 天呐,我等到地老天荒,还没下好,看来又要祭出镜像大法了 用阿里云的PyPI 镜像就行了

注意: 别被网页上的说明文件骗了,那是针对linux用户的,windows的配置文件在别的地方,我也是懒得找,可以直接用命令配置,这样就不用关心操作系统了。

pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/

3. 替换launch.py中的几个依赖库的github地址

好久不写shell脚本了,还好有[AI降临派]

sed -i 's|https://github.com/|https://ghproxy.com/https://github.com/|g' launch.py

接下来,交给时间吧,马上就能体验到SD的强大了

· 3 min read
天山飞猫

昨天上午,突然收到阿里云的短信,告诉我ICP备案终于审核通过了

然后下午,一通折腾,终于把现在这个网站弄到公网上了。

搭建CI/CD工具

当然,作为一个标准“懒人”,我是绝对不能容忍“自己每次都要手动把构建好的文件手动上传的服务器”这种重复性劳动的

那就需要用到CI/CD的工具了,接下来进入选型的阶段了,我直接选择了DroneCI。 原因嘛。。当然是因为还没用过它

作为一个比较新的工具,文档还是比较齐全的。安装说明文档,和我的Gogs关联在了一起

该写部署脚本了

语法极其简单,作为有过Github ActionGitlab CI 经验的小白,还是轻松搞定

hook都不用自己建,DroneCI已经都做完了,提交代码,看看构建的日志咯。。。

峰回路转

嗯?日志怎么不动了?算了,先去下盘围棋。。。。。

一小时后,我去?怎么还卡在这?是我页面没刷新吗?

好像不对,网页挂了

ssh到服务器上看看。。。timeout了

直觉,服务器是不是cpu或磁盘满了,赶紧上阿里云看一下,cpu 100% 已经50多分钟了,果断重启

好吧,我的机器太弱鸡了(1核1G)

突然想起了内网穿透这个大杀器

我的服务器弱,可我开发机器可不弱啊(12核32G)

更新部署方案:

用阿里云的服务器做前端,主要跑个nginx做反向代理,再跑个frp的服务端来做内网穿透

家里开发机器跑具体的服务,再跑个frp的客户端

完美,只要家里不停电,就可以一直用了,毕竟电费可比阿里云的服务费便宜多了。

· One min read
天山飞猫

上次写博客还是 2019 年,疫情之前的事了。一转眼,3年多过去了,我身边也发生了一些变故,现在是时候重新开始写博客了。

这里会把我在学习工作中的一些体会分享一下,另外也会把自己做的一些开源工作放在这里