合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。
让我们暂时撇开平台、框架、技术、设计模式、对象思想、敏捷开发论等。 追问程序本质。
从本质上来说, 程序就是一系列有序执行的指令集合。 如何将指令集合组织成可靠可用可信赖的软件(逻辑之塔), 这是个问题。
程序 = 逻辑 + 控制。 what to do + when to do.
从编程角度来说, 开发者应对的就是逻辑, 逻辑的表达、组织和维护。 逻辑是事物自此及彼的可接受的序列。指令是逻辑的具体实现形式。
也就是说:
1. 要表达什么逻辑;
2. 如何表达该逻辑;
3. 如何维护该逻辑。
软件的复杂性表现在如何表达和维护交互复杂的大型逻辑上。
设计模式体现的是一种逻辑块交互解耦的方法;
应用框架解决的是应用的通用逻辑流的控制的问题,让开发者更多地聚焦具体业务逻辑上;
开发技术是在具体的应用情境下按照既定总体思路去探究具体问题解决的方法。
我们要解决的是通用的问题: 如何以更不易出错的方式去表达和维护大型逻辑 ?
表达和维护大型逻辑的终极诀窍就是: 将大型逻辑切分为容易消化的一小块一小块, “不急不忙地吃掉”。
1. 独立无交互的大型逻辑或接口实现
独立无交互的逻辑通常体现为公共库, 可以解决常见或公共的日常任务, 对其他逻辑无任何依赖和交互, 即自足逻辑。
应对独立无交互的大型逻辑的首要方法是分解为若干的容易实现、测试和复用的小块逻辑, 编写和严格测试。
其次是运用成熟的编程模式去表达逻辑, 尽可能复用经过严格测试的可靠的库。
独立无交互的大型逻辑通过切分逻辑块、严格的单元测试可以获得充分的测试和可靠度。
《编程语言与可复用性》 说明了编程语言如何支持高度灵活的可复用表达。
2. 独立无交互的耗时长的逻辑或接口实现
响应快速的问题: 用户要求等待时间短 与 请求处理时间耗时长 之间的矛盾导致的。
解决独立无交互的耗时长的逻辑依然可以采用切分逻辑块、严格的单元测试的做法使之更容易处理;
此外, 有两种设计思路可以考虑: 并发 与 异步。
并发思路是将切分的相互独立的逻辑块分配给不同的控制线程中执行, 从而降低请求处理时长; 并发方案获得的性能提升取决于串行操作在总操作中的时间占比。
异步思路是“先响应, 后处理, 终通知” 的"先奏后斩"方案。将一步分离成了三步, 为了让用户首先获得答复, 却增加了新的问题: 消息中间件组件的开发与部署、异步消息发送与接收、编程模型的变化和适应。如果整个过程运作良好, 将会达到很好的体验,容易为用户接受。如果其中一步发生差错, 就会导致各种问题, 比如数据不一致, 消息堆积、 请求无法被处理。最终用户等待时间并没有降低, 反而使体验更加糟糕。 当然, 如果成功率为 95%, 也是“可以接受”的, 这样用户可能会怪自己“运气不太好”, 而不会过多怪责系统的不完善。
并发与异步方案的调试难度都比同步方案增加不少。 每一种新的设计方案都会有其优点, 同时也会有其缺点。 权衡优缺点, 择善而从之 。
注意, 并发方案是针对服务端实际处理请求逻辑而言, 而异步方案是针对请求处理之前是否立即回复的方式; 并发与顺序、 异步与同步两两组合, 可得到四种方式:
顺序同步: 最初的编程模型; 优点是简单、安全、 容易维护和调试; 缺点是性能较低, 响应时间和吞吐量都不高; 若请求处理时长非常短, 采用顺序同步的方案佳;
并发同步: 改进的编程模型; 优点是通过并发提高服务端的处理速度和吞吐量, 但若请求处理耗时较长, 响应时间仍然不高, 影响客户端体验; 若通过并发方案处理请求的时长非常短, 或客户端体验要求不高, 可以采用并发同步的方案;
顺序异步: 改善客户端体验的编程模型; 优点是提高了响应时间和客户端体验, 由于其逻辑处理仍然采用顺序方式, 请求处理时长并未有改善, 因此吞吐量并没有改善。 是一种较好的折衷方案; 若请求处理耗时较长, 影响客户端体验, 且请求处理逻辑复杂, 采用并发方案容易出错或难以并发, 可采用顺序异步方案;
并发异步: 同时改善客户端体验和服务端处理速度; 优点是提高了响应时间、客户端体验和处理速度、吞吐量。 缺点是容易出错, 且不易调试; 若客户端对响应体验要求较高, 请求处理逻辑简单(比如简单的数据拉取和汇总), 采用并发方式可有效提升处理速度, 可以采用并发异步方案;
3. 有交互耦合的小块逻辑或接口实现
软件的复杂性真正体现在逻辑块的持续长久的交互上。这是软件开发与维护中极具挑战性的部分。
有交互的逻辑块通常体现在对可变共享资源的访问上。 交互耦合实际上是共享可变导致的。
一种方法是实现并发互斥, 安全但性能比较低; 另一种方法是通过可接受的复制开销,实现不可变资源的共享。
逻辑之间的交互耦合应该交给交互解耦模块去完成, 而不是在自己的接口里实现。 也就是说, 只有交互解耦模块知道所有接口之间的交互, 而接口只做自己知道的事情就可以了。否则, 接口 A 与接口 B 必须知道彼此究竟做了什么, 才能正确地做自己的事情。
假设 接口 A 和接口 B 都修改某个资源的状态。 接口 A 在做某项操作执行必须执行 IF (ConditionX) do something ; DoMyOwnThing ; 接口 B 也要根据 A 的逻辑相应地执行 if (ConditionY) do anotherThing ; DoMyOwnThing. 耦合的接口数量越多, 或者耦合接口之间的耦合资源越多, 对后期维护和扩展将是一个难以应对的噩梦。
对于逻辑块之间的交互解耦, 或者通俗地说, 模块解耦, 您有怎样的高见, 敬请提出!
程序中的逻辑主要是三类:
1. 获取值: 从数据库、网络或对象中获取值。 如果数据库或网络访问足够稳定的话, 可以看成是简单的获取值, 数据库访问和网络访问对获取值是透明的;
2. 检测值: 检测值是否合法, 通常是前置条件校验、 中间状态校验和后置结果校验;
3. 设置(拷贝)值: 设置数据库、对象中的值; 或者发送数据和指令给网络。如果数据库或网络访问足够稳定的话, 可以看成是简单的设置值, 数据库访问和网络访问对设置值是透明的;
这三类逻辑可以称为逻辑元。 具体业务逻辑就是将逻辑元的组合封装成逻辑块, 有效控制逻辑块的时序交互和资源分配。 时序控制不合理和资源缺乏导致错误和异常。 网络通信错误, 是因为网络带宽资源是有限的。 两个程序同时更新一个共享变量, 如果时序不控制, 就会导致错误的结果。
如何应对异常 ? 请参考 《如何使错误日志更加方便排查问题》, 仔细总结了软件错误产生的各种原因及如何预防和定位。 当然, 还有一些复杂的软件错误, 比如事务与并发, 限于开发经验尚浅, 还给不出有效的方案和措施, 需要根据实践学习和深化。
在已确定的设计方案和业务逻辑的情况下, 如何编写BUG更少的代码:
简明扼要的注释 + 契约式编程 + 更短小的逻辑块 + 复用公共库 + 严格测试
1. 在方法前面编写简明扼要的注释: 方法用途, 接收参数, 返回值, 注意事项, 作者, 时间。
2. 契约式编程: 在方法入口处编写前置条件校验,在方法出口处编写后置结果校验 ;
3. 编写和保持短小逻辑块, 易于为人的脑容量一次性处理, 容易测试;
4. 复用经过严格测试的可靠的公共库; 如果库没有经过很好的测试,但有很好的用处, 帮助其添加测试;
5. 对所编写的代码, 如果不是逻辑元, 都要进行严格测试。