永利国际网站函数式编程与面向对象编程[5]:编程的本质

那么,对于程序的复合而言,正确的代码块是怎样的?它们的表面积必须要比它们的体积增长的更为缓慢。我喜欢这个比喻,因为几何对象的表面积是以尺寸
的平方的速度增长的,而体积是以尺寸的立方的速度增长的,因此表面积的增长速度小于体积。代码块的表面积是是我们复合代码块时所需要的信息。代码块的体积
是我们为了实现它们所需要的信息。一旦代码块的实现过程结束,我们就可以忘掉它的实现细节,只关心它与其他代码块的相互影响。在面向对象编程中,类或接口
的声明就是表面。在函数式编程中,函数的声明就是表面。我把事情简化了一些,但是要点就是这些。

            a.   操作顺序的依赖。
比如资源更新操作必须在指定资源已经创建的情况下进行。

逻辑块之间的交互耦合与可扩展性

软件的复杂性真正体现在逻辑块的持续长久的交互耦合和可扩展上。这是软件开发与维护中极具挑战性的部分。

逻辑块之间的交互耦合通常体现在三种情境:

a. 操作顺序的依赖。
比如资源更新操作必须在指定资源已经创建的情况下进行。b.
对共享有限资源的并发申请。 比如打印机只有两台,
却有多个应用程序连接上去请求打印文档;c. 对共享可变状态的并发访问。
比如两个操作同时要更新数据库中的同一条记录;

三种情境的复杂性均是由并发引起的。 假设所有操作都是串行进行的,
逻辑块的交互无非是“你方唱罢我登场”的次序控制,
而资源对单个请求通常是足够的; 一旦采用了并发方案,
就难以控制逻辑块的执行次序和资源分配的具体情况了,
容易导致某资源对单个请求不足的情况,
从而阻塞多个请求的处理甚至死锁。并发提升了应用的性能,
却增加了出错的风险和几率。并发控制是大型逻辑交互的本质性难点。并发控制的难点在于时序的合理控制和有效资源的合理分配。

对于 a 情境, 通常采用添加前置条件来求解,
在操作之前校验相关资源是否满足、实体状态是否合理,
实体之间的关联是否正确; 若前置条件不满足, 则直接返回错误提示,
或者暂时挂起以备后续继续执行;

对于 b 情境, 需要创建一个可靠适用的资源分配算法 和资源分配模块 ,
应用程序不再“自行”去拉取资源, 而是向资源分配模块申请资源,
由资源分配模块根据实际申请的整体情况及申请条件来决定如何分配资源;

对于 c 情境, 需要进行安全的互斥访问, 谨慎地控制。

逻辑块之间的交互耦合应该交给交互解耦模块去完成,
而不是在自己的接口里实现。

也就是说, 只有交互解耦模块知道所有接口之间的交互,
而接口只做自己知道的事情就可以了。否则, 接口 A 与接口 B
必须知道彼此究竟做了什么, 才能正确地做自己的事情。 假设 接口 A 和接口 B
都修改某个资源的状态。 接口 A 在做某项操作执行必须执行 IF (ConditionX)
do something ; DoMyOwnThing ; 接 口 B 也要根据 A 的逻辑相应地执行 if
(ConditionY) do anotherThing;DoMyOwnThing. 而程序员在维护和修改接口 A
的逻辑时, 不一定知道接口 B 的逻辑与之相关,
于是修改不可避免地破坏了接口 B 的逻辑。 耦合的接口数量越多,
或者耦合接口之间的耦合资源越多,
对后期维护和扩展将是一个难以应对的噩梦。

对于逻辑块之间的交互解耦, 或者通俗地说, 模块解耦.

 

这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证withdraw功能的事务性。分布式事务的优点是对于调用者很简单,复杂性都交给了中间件来管理。缺点则是一方面架构太重量级,容易被绑在特定的中间件上,不利于异构系统的集成;另一方面分布式事务虽然能保证事务的ACID性质,而但却无法提供性能和可用性的保证。

通过布尔代数进行集合运算可以获取到不同集合之间的交集、并集或补集,进行逻辑运算可以对不同集合进行与、或、非。

       要健壮地表达和维护大型逻辑,
首先系统整体架构必须足够稳固可靠, 在开发和维护过程中持续加固。
 假设一栋建筑整体设计有问题, 那么, 无论里面的房间装饰得多么漂亮优雅,
都会随着建筑的坍塌而消亡。 这需要深入去探究所使用的应用框架,
挖出可能的不可靠风险, 并加以预防和控制。

  分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。分布式锁是一个在很多环境中非常有用的原语,它是不同的系统或是同一个系统的不同主机之间互斥操作共享资源的有效方法。如在电商系统中,需要保证整个分布式系统内,对一个重要事物(订单,账户等)的有效操作线程
,同一时间内有且只有一个。比如交易中心有N台服务器,订单中心有M台服务器,如何保证一个订单的同一笔支付处理,一个账户的同一笔充值操作是原子性的。

在积极阻碍我们探视对象的内部方面,范畴论具有非凡的意义。范畴论中的一个对象,像一个星云。对于它,你所知的只是它与其他对象之间的关系,亦即它
与其他对象相连接的箭头。这就是 Internet
搜索引擎对网站进行排名时所用的策略,它只分析输入与输出的链接。在面向对象编程中,一个理想的对象应该是只暴露它的抽象接口,其方法则扮演箭头的角色。如果为了理解一个对象如何与其他对象进行复合,当你发现不得不深入挖掘对象的实现之时,此时你所用的编程范式的原本优
势就荡然无存了。

 

withdraw的语义是从account_id对应的账户中扣除amount数额的钱;如果扣除成功则返回true,账户余额减少amount;如果扣除失败则返回false,账户余额不变。值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的情况是withdraw请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被丢掉了,导致客户端无法得知处理结果。如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了withdraw被调用两次,账户也被多扣了一次钱。如下图所示:

这个思维过程,
并非是受计算机的限制而产生,它反映的是人类思维的局限性。我们的大脑一次只能处理很少的概念。生物学中被广为引用的
一篇论文指出我们我们的大脑中只能保存 7± 2
个信息块。我们对人类短期记忆的认识可能会有变化,但是可以肯定的是它是有限的。底线就是我们不能处理一大堆乱糟糟的对象或像兰州拉面似的代码。我们需要
结构化并非是因为结构化的程序看上去有多么美好,而是我们的大脑无法有效的处理非结构化的东西。我们经常说一些代码片段是优雅的或美观的,实际上那只意味
着它们更容易被人类有限的思维所处理。优雅的代码创造出尺度合理的代码块,它正好与我们的『心智消化系统』能够吸收的数量相符。

          5.  
复用经过严格测试的可靠的公共库;
如果库没有经过很好的测试,但有很好的用处, 帮助其添加测试;

1.高等代数中关于幂等idempotence概念解释:

<div ></div>

          3.   防御式编程:
编程时严格校验参数和前置条件;
仔细考虑各种错误与异常的定位和处理;

 

实现逻辑时的容错考虑

程序中的逻辑主要是三类:

  • 获取值: 从数据库、网络或对象中获取值。
    如果数据库或网络访问足够稳定的话, 可以看成是简单的获取值,
    数据库访问和网络访问对获取值是透明的;

  • 检测值: 检测值是否合法, 通常是前置条件校验、
    中间状态校验和后置结果校验,
    根据检测结果执行“获取值”或“设置值”的逻辑;

  • 设置值: 设置数据库、对象中的值;
    或者发送数据和指令给网络。如果数据库或网络访问足够稳定的话,
    可以看成是简单的设置值, 数据库访问和网络访问对设置值是透明的;

这三类逻辑可以称为逻辑元。 具体业务逻辑就是基于物理的或逻辑的资源限制,
将逻辑元的组合封装成逻辑块, 有效控制逻辑块的时序交互和资源分配。
时序控制不合理和资源缺乏导致错误和异常。两个程序同时更新一个共享变量,
如果时序不控制, 就会导致错误的结果; 网络通信错误,
是因为网络带宽资源是有限的。

 

3.总结

之剑 2016.5.6 01:26:31

        程序 = 逻辑 + 控制。 what to do +
when to do.   

bool withdraw(account_id, amount)

它包含集合B连同在其上定义的两个二元运算+,·和一个一元运算′,布尔代数具有下列性质:对B中任意元素a,b,c,有:

     
    1.   在方法前面编写简明扼要的注释: 方法用途, 接收参数,  返回值,
注意事项, 作者, 时间。

Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects 
  of N > 0 identical requests is the same as for a single request.

读到两篇文章,写的不错, 综合摘录一下

 

**

布尔代数起源于数学领域,是一个用于集合运算和逻辑运算的公式:〈B,∨,∧,¬
〉。其中B为一个非空集合,∨,∧为定义在B上的两个二元运算,¬为定义在B上的一个一元运算。

            b.  
对共享有限资源的并发申请。  比如打印机只有两台,
却有多个应用程序连接上去请求打印文档;  

和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用中,幂等设计往往是唯一的选择。

如何应对错误和异常 ?

预防错误的方法就是进行防御性编程, 进行容错考虑。 多思考:
如果这一步发生错误, 会导致什么问题? 该如何做才能预防这个错误?
如果难以预防, 该如何描述, 才能在出现错误时更好地定位出这样的错误?
在出现错误时, 如何才能恢复到正常合法的状态 ? 如果无法程序自动恢复,
怎样做才能让手工处理更加简单 ?

要健壮地表达和维护大型逻辑, 首先系统整体架构必须足够稳固可靠,
在开发和维护过程中持续加固。 假设一栋建筑整体设计有问题, 那么,
无论里面的房间装饰得多么漂亮优雅, 都会随着建筑的坍塌而消亡。
这需要深入去探究所使用的应用框架, 挖出可能的不可靠风险,
并加以预防和控制。

在已确定的设计方案和业务逻辑的情况下, 如何编写BUG更少的代码:

简明扼要的注释 + 契约式/防御式编程 + 更短小的逻辑块 + 复用公共库 +
严格测试

编写更少BUG程序的六条准则:

 1. 在方法前面编写简明扼要的注释: 方法用途, 接收参数, 返回值, 注意事项, 作者, 时间。 2. 契约式编程: 在方法入口处编写前置条件校验,在方法出口处编写后置结果校验 ; 3. 防御式编程: 编程时严格校验参数和前置条件; 仔细考虑各种错误与异常的定位和处理; 4. 编写和保持短小逻辑块, 易于为人的脑容量一次性处理, 容易测试; 5. 复用经过严格测试的可靠的公共库; 如果库没有经过很好的测试,但有很好的用处, 帮助其添加测试; 6. 对所编写的代码, 如果不是逻辑元, 都要进行严格测试。

关于作者: 陈光剑,江苏东海人, 号行走江湖一剑客,字之剑。程序员,诗人,
作家

<link rel=”stylesheet”
href=”;

<script
src=”
src=”;

<script>hljs.initHighlightingOnLoad();</script><script
type=”text/javascript”>$.ready(function(){$(“h2,h3,h4,h5,h6”).each(function{var
tag = $.get.localName;$.attr(“id”,”wow”+i);$(“#category”).append(‘<a
href=”#wow’+i+'”>’+$.text()+'</a></br>’);$.css(“margin-left”,0);$.css(“margin-left”,20);$.css(“margin-left”,40);$.css(“margin-left”,60);$.css(“margin-left”,80);});});</script>

 

永利国际网站 1

让我们暂时撇开平台、框架、技术、设计模式、对象思想、敏捷开发论等。
追问程序本质。

 

Person { 
    private int weight; 
    private int age; 
    //是幂等函数
    public void setAge(int v){ 
        this.age = v; 
    }
    //不是幂等函数
    public void increaseAge(){ 
        this.age++;
    } 
    //是幂等函数
    public void setWeight(int v){ 
        this.weight=v+10;//故意加10斤!!
    }
}
1.a+b=b+a, a·b=b·a.2.a·=a·b+a·c,a+=·.3.a+0=a, a·1=a.4.a+a′=1, a·a′=0.

     
 
设计模式体现的是如何可扩展地解决常见的逻辑交互问题;

  双目运算,x为某集合内的任意数,
f为运算子如果满足f(x,x)=x, f运算的前提是两个参数都同为x,
那么我们也称f运算为具有幂等性。**
比如在实数集中,求两个数的最大值的函数:
max(x,x) = x, 还有布尔代数中,逻辑运算 “与”, “或” 也都是幂等运算,
因为他们符合AND(0,0) = 0, AND(1,1) = 1, OR(0,0) = 0, OR(1,1) =
1。**

独立无交互的大型逻辑或接口实现

独立无交互的逻辑通常体现为公共库, 可以解决常用或公共的日常任务,
对其他逻辑无任何依赖和交互, 即自足逻辑。

应对独立无交互的大型逻辑的首要方法是分解为若干的容易实现、测试和复用的小块逻辑,
编写和严格测试。

其次是运用成熟的编程模式去表达逻辑, 尽可能复用经过严格测试的可靠的库。

独立无交互的大型逻辑通过合理的逻辑块切分、严格的单元测试可以获得充分的测试和可靠度。

 

  分布式锁在分布式应用当中是要经常用到的,主要是解决分布式资源访问冲突的问题。传统的锁ReentrantLock在去实现的时候是有问题的,ReentrantLock的lock和unlock要求必须是在同一线程进行,而分布式应用中,lock和unlock是两次不相关的请求,因此肯定不是同一线程,因此导致无法使用ReentrantLock。

函数式程序员在洞察问题方面会遵循一个奇特的路线。他们首先会问一些似有禅机的问题。例如,在设计一个交互式程序时,他们会问:什么是交互?在实现
基于元胞自动机的生命游戏时,他们可能又去沉思生命的意义。秉持这种精神,我将要问:什么是编程?在最基本的层面,编程就是告诉计算机去做什么,例如『从
内存地址 x 处获取内容,然后将它与寄存器 EAX
中的内容相加』。但是即使我们使用汇编语言去编程,我们向计算机提供的指令也是某种有意义的表达式。假设我们正在解一个难题(如果它不难,就没必要用计算
机了),那么我们是如何求解问题的?我们把大问题分解为更小的问题。如果更小的问题还是还是很大,我们再继续进行分解,以此类推。最后,我们写出求解这些
小问题的代码,然后就出现了编程的本质:我么将这些代码片段复合起来,从而产生大问题的解。如果我们不能将代码片段整合起来并还原回去,那么问题的分解就
毫无意义。

     
       独立无交互的逻辑通常体现为公共库, 可以解决常用或公共的日常任务,
对其他逻辑无任何依赖和交互, 即自足逻辑。

  常见的实现分布式锁的服务有:memcache
zookeeper redis chubby hazelcast。

独立无交互的耗时长的逻辑或接口实现

快速响应的问题: “用户要求等待时间短” 与 “请求处理耗时长”
之间的矛盾导致的。

解决独立无交互的耗时长的逻辑依然可以采用切分逻辑块、严格的单元测试的做法使之更容易处理;

此外, 有两种设计思路可以考虑: 并发 与 异步。

  • 并发思路是将切分的相互独立的逻辑块分配给不同的控制线程中执行,
    从而降低请求处理时长;
    并发方案获得的性能提升取决于串行操作在总操作中的时间占比。

  • 异步思路是“先响应, 后处理, 终通知” 的”先奏后斩”方案。

将一步分离成了三步, 为了让用户首先获得初步的承诺, 再去履行承诺。
这样做能让用户暂时地放心, 却增加了新的问题:
消息中间件组件的开发与部署、异步消息发送与接收、编程模型的变化和适应。如果整个过程运作良好,
将会达到很好的体验,容易为用户接受。如果其中一步发生差错,
就会导致各种问题, 比如数据不一致, 消息堆积、
请求无法被处理。最终用户等待时间并没有降低, 反而使体验更加糟糕。 当然,
如果成功率为 95%, 也是“可以接受”的, 这样用户可能会怪自己“运气不太好”,
而不会过多怪责系统的不完善。毕竟没有任何事情能够做到完美的地步。

并发与异步方案的调试难度和排查问题都比同步方案增加不少。
每一种新的设计方案都会有其优点, 同时也会有其缺点。 权衡优缺点,
择善而从之 。值得注意的是, 并发方案是针对服务端实际处理请求逻辑而言,
而异步方案是针对请求处理之前是否立即回复的方式。 并发与顺序、
异步与同步两两组合, 可得到四种方式:

  • 优点是简单、安全、 容易维护和调试;
  • 缺点是性能较低, 响应时间和吞吐量都不高; 若请求处理时长非常短,
    采用顺序同步的方案佳;

  • 优点是通过并发提高服务端的处理速度和吞吐量, 但若请求处理耗时较长,
    响应时间仍然不高, 影响客户端体验;
  • 若通过并发方案处理请求的时长非常短, 或客户端体验要求不高,
    可以采用并发同步的方案;

  • 优点是提高了响应时间和客户端体验, 由于其逻辑处理仍然采用顺序方式,
    请求处理时长并未有改善, 因此吞吐量并没有改善。
    是一种较好的折衷方案;
  • 若请求处理耗时较长, 影响客户端体验, 且请求处理逻辑复杂,
    采用并发方案容易出错或难以并发, 可采用顺序异步方案;

  • 优点是提高了响应时间、客户端体验和处理速度、吞吐量。
  • 缺点是容易出错, 且不易调试;
  • 若客户端对响应体验要求较高,
    请求处理逻辑简单(比如简单的数据拉取和汇总),
    采用并发方式可有效提升处理速度, 可以采用并发异步方案;

 

二、高并发

在布尔代数上的运算被称为AND和NOT。代数结构要是布尔代数,这些运算的行为就必须和两元素的布尔代数一样(这两个元素是TRUE和FALSE。亦称逻辑代数.布尔为研究思维规律于1847年提出的数学工具.布尔代数是指代数系统B=〈B,+,·,′〉

     
     1. 获取值: 从数据库、网络或对象中获取值。
如果数据库或网络访问足够稳定的话, 可以看成是简单的获取值,
数据库访问和网络访问对获取值是透明的;

  1. HTTP
    GET方法用于获取资源,不应有副作用,所以是幂等的。比如:GET

  2. HTTP
    DELETE方法用于删除资源,有副作用,但它应该满足幂等性。比如:DELETE
  3. 比较容易混淆的是HTTP
    POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:

    The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified
    by the Request-URI in the Request-Line …… If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain
    an entity which describes the status of the request and refers to the new resource, and a Location header.

    The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource,
    the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an
    existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.

公理化

在 1933 年,美国数学家 Edward Vermilye Huntington (1874-1952)
展示了对布尔代数的如下公理化:

交换律: x + y = y + x。结合律:  + z = x + 。Huntington等式: n + n + n = x。

一元函数符号 n 可以读做’补’。

Herbert Robbins 接着摆出下列问题:
Huntington等式能否缩短为下述的等式,并且这个新等式与结合律和交换律一起成为布尔代数的基础?
通过一组叫做 Robbins 代数的公理,问题就变成了: 是否所有的 Robbins
代数都是布尔代数?

Robbins 代数的公理化:

交换律: x + y = y + x结合律:  + z = x + Robbins等式: n + n) = x

这个问题自从 1930 年代一直是公开的,并成为 Alfred Tarski
和他的学生最喜好的问题。

在 1996 年,William McCune 在 Argonne 国家实验室,建造在 Larry
Wos、Steve Winker 和 Bob Veroff
的工作之上,肯定的回答了这个长期存在的问题: 所有的 Robbins
代数都是布尔代数。这项工作是使用 McCune 的自动推理程序 EQP 完成的。

从本质上来说, 程序就是一系列有序执行的指令集合。
如何将指令集合组织成可靠可用可信赖的软件, 这是个问题。

程序 = 逻辑 + 控制。 what to do + when to do.

从编程角度来说, 开发者应对的就是逻辑, 逻辑的表达、组织和维护。
逻辑是事物自此及彼的合乎事物发展规律的序列。指令是逻辑的具体实现形式。

逻辑成立的先决条件是合乎事物发展规律。 程序只能处理数值,
却传入了字符串, 就只能报错而无法继续; 当处理海量数据时, 若内存不足,
就会导致程序崩溃; 若程序存在内存泄露, 随着时间的推移而耗尽内存,
也会导致程序崩溃。 多个线程同时修改一个共享变量, 若不加控制,
就会因为不同线程执行修改变量的时序的不确定导致该变量最终值的不确定。
这些就是程序执行的发展规律。 要编写程序, 必定要先通悉这些规律。

规律的表现形式是:如果条件 (C1, C2, …, Cn) 是产生结果 (R1, R2, … ,
Rn) 的充分必要条件, 那么当 C1, C2, …, Cn 任一不满足条件时,
都不可能产生结果 (R1, R2, …, Rn) ; 反之, 若结果 (R1, R2, …, Rn)
没有出现, 则必定是 C1, C2, …, Cn 某一条件不满足导致。 错误和异常即是
C1, C2, …, Cn 任一不满足条件的表现。规律的性质是必然的,
不存在可能之说;
只存在人们探索的是否足够精确。编程开发首先应当懂得程序执行的规律,
然后才是实际的开发; 否则就会被程序的结果折腾得死去活来。

在通悉程序执行规律之后, 程序需要解决如下问题:

  • 要表达什么逻辑
  • 如何表达该逻辑;
  • 如何维护该逻辑。

软件的复杂性表现在如何表达和维护交互复杂的大型逻辑上

暂时先回到软件的起点, 回顾一下这一切是如何发生的。

最初, 人们使用物理的或逻辑的二进制机器指令来编写程序,
尝试着表达思想中的逻辑, 控制硬件计算和显示, 发现是可行的;

接着, 创造了助记符 —— 汇编语言, 比机器指令更容易记忆;

再接着, 创造了编译器、解释器和计算机高级语言,
能够以人类友好自然的方式去编写程序, 在牺牲少量性能的情况下,
获得比汇编语言更强且更容易使用的语句控制能力:条件、分支、循环,
以及更多的语言特性: 指针、结构体、联合体、枚举等, 还创造了函数,
能够将一系列指令封装成一个独立的逻辑块反复使用;

逐渐地,产生了面向过程的编程方法;

后来, 人们发现将数据和逻辑封装成对象, 更接近于现实世界,
且更容易维护大型软件, 又出现了面向对象的编程语言和编程方法学,
增加了新的语言特性: 继承、 多态、 模板、 异常错误。

为了不必重复开发常见工具和任务, 人们创造和封装了容器及算法、SDK,
垃圾回收器, 甚至是并发库;

为了让计算机语言更有力更有效率地表达各种现实逻辑,
消解软件开发中遇到的冲突, 还在语言中支持了元编程、 高阶函数, 闭包
等有用特性。

为了更高效率地开发可靠的软件和应用程序, 人们逐渐构建了代码编辑器、
IDE、 代码版本管理工具、公共库、应用框架、
可复用组件、系统规范、网络协议、 语言标准等,
针对遇到的问题提出了许多不同的思路和解决方案,
并总结提炼成特定的技术和设计模式, 还探讨和形成了不少软件开发过程,
用来保证最终发布的软件质量。 尽管编写的这些软件和工具还存在不少 BUG
,但是它们都“奇迹般地存活”, 并共同构建了今天蔚为壮观的软件世界。

此外, 软件还经历了“单机程序 => 多机程序 => 分布式程序” 的过程 ,
多机联网程序因为多个子系统的交互变得更加复杂。 这里不再赘述。

但请注意, 无论软件发展到多么复杂的程度, 总有一群人,
在试图从程序的本质中探究软件开发的基本问题,
他们试图论证和确保程序的正确性、提炼软件的基本属性并进行衡量;
程序的正确性本质是逻辑学来保证的。 没有逻辑学, 程序根本就无法立足,
更不可能有今天的大规模应用。

软件开发工具让我们更有效率地创造逻辑、 远离语法错误的困扰;

公共库将常用的通用逻辑块封装成可反复使用的组件, 避免不必要的重复劳动;

设计模式体现的是如何可扩展地解决常见的逻辑交互问题;

应用框架解决的是应用的通用逻辑流的控制的问题,让开发者更多地聚焦具体业务逻辑上;

开发技术是在具体的应用情境下按照既定总体思路去探究具体问题解决的方法。

我们要解决的是更通用的问题:
如何以更不易出错的方式去表达和维护大型逻辑 ?

表达和维护大型逻辑的终极诀窍就是:
将大型逻辑切分为容易消化的一小块一小块, “不急不忙地吃掉”。

在该方法的实践中,
可以充分利用现有的开发工具、公共库、设计模式、应用框架、开发技术。

             
并发思路是将切分的相互独立的逻辑块分配给不同的控制线程中执行,
从而降低请求处理时长;
并发方案获得的性能提升取决于串行操作在总操作中的时间占比。 

一、幂等性 idempotence  [‘aɪdəmpoʊtəns]

       预防错误的方法就是进行防御性编程,
进行容错考虑。 多思考: 如果这一步发生错误,
会导致什么问题? 该如何做才能预防这个错误? 如果难以预防,
 该如何描述, 才能在出现错误时更好地定位出这样的错误? 在出现错误时,
如何才能恢复到正常合法的状态 ?  如果无法程序自动恢复,
怎样做才能让手工处理更加简单 ?   

从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的作用。幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。比如有这样一个业务逻辑,假设有一个从账户取钱的远程API(可以是HTTP的,也可以不是),我们暂时定义接口:

     
     
 应对独立无交互的大型逻辑的首要方法是分解为若干的容易实现、测试和复用的小块逻辑,
编写和严格测试。

 

 

  单目运算,
x为某集合内的任意数, 如果满足f(x)=f(f(x)),
那么我们称f运算为具有幂等性(idempotent)。比如在实数集中,绝对值运算就是一个例子:
abs(a)=abs(abs(a))。

 

POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST

     
     对于 c 情境, 需要进行安全的互斥访问,
谨慎地控制。 

附:

 

  另一种更轻量级的解决方案是幂等设计。我们可以通过一些技巧把withdraw变成幂等的,比如:

   

          并发同步:  改进的编程模型;
优点是通过并发提高服务端的处理速度和吞吐量, 但若请求处理耗时较长,
响应时间仍然不高, 影响客户端体验; 若通过并发方案处理请求的时长非常短,
或客户端体验要求不高, 可以采用并发同步的方案;

2.分布式锁实现

     
 如何应对错误和异常 ?  请参考 《如何使错误日志更加方便排查问题》,
 仔细总结了软件错误产生的各种原因及如何预防和定位。 当然,
还有一些复杂的软件错误, 比如事务与并发, 限于开发经验尚浅,
还给不出有效的方案和措施,
需要根据实践学习和深化。这是当错误和异常已经发生时,
该如何更好地定位和解决问题。

2.HTTP协议中的幂等性

       
在该方法的实践中,
可以充分利用现有的开发工具、公共库、设计模式、应用框架、开发技术。 

幂等性是系统的接口对外一种承诺(而不是实现),
承诺只要调用接口成功,
外部多次调用对系统的影响是一致的。声明为幂等的接口会认为外部调用失败是常态,
并且失败之后必然会有重试。
所以RESTful设计中将幂等性和安全性作为两个不同的指标来衡量POST,PUT,GET,DELETE操作的。因此,post不是幂等性的,put
get
delete都是幂等性的,也即在生成订单的post请求中,我们要做幂等性的控制。如下图,一个ajax请求是一次post请求的示例,如果这个post请求被调用多次,它会向表插入多条记录,很显然post请求并不是幂等性的,所以幂等性的控制交由我们程序中来控制。**

     
 这三类逻辑可以称为逻辑元。 具体业务逻辑就是基于物理的或逻辑的资源限制,
将逻辑元的组合封装成逻辑块, 有效控制逻辑块的时序交互和资源分配。 时序控制不合理和资源缺乏导致错误和异常。两个程序同时更新一个共享变量,
如果时序不控制, 就会导致错误的结果; 网络通信错误,
是因为网络带宽资源是有限的。 

在Java
web项目开发中,经常会听到在做订单系统中生成订单的时候,要做幂等性控制和并发控制,特对此部分内容作出总结,在高并发场景下,代码层面需要实现并发控制;但是幂等性,其实更多的是系统的接口对外的一种承诺,承诺一次请求和多次请求会返回同样的数据。关于幂等性将分别从高等代数中的幂等性、HTTP中的幂等性和订单生成系统中的幂等性阐述;并发性控制则提供了分布式锁等方式来对并发场景进行代码实现。

          顺序同步:  最初的编程模型;
 优点是简单、安全、 容易维护和调试;  缺点是性能较低,
响应时间和吞吐量都不高; 若请求处理时长非常短,
采用顺序同步的方案佳;

int create_ticket() 
bool idempotent_withdraw(ticket_id, account_id, amount)

           对于 a 情境,
通常采用添加前置条件来求解,
 在操作之前校验相关资源是否满足、实体状态是否合理,
实体之间的关联是否正确;  若前置条件不满足, 则直接返回错误提示,
或者暂时挂起以备后续继续执行;

基于幂等性的解决方案中一个完整的取钱流程被分解成了两个步骤:1.调用create_ticket()获取token;2.调用idempotent_withdraw(token,
account_id,
amount)。虽然create_ticket不是幂等的,但在这种设计下,它对系统状态的影响可以忽略,加上idempotent_withdraw是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以重试,直到获得结果。如图2所示:

           
逻辑块之间的交互耦合通常体现在三种情境:

  项目中中的SOA和restful
API接口的流行,都需要应用层HTTP协议的支持,目前的项目结构:Web API +
RIA(Rich Internet Applications富互联网应用),Web
API专注于提供业务服务,RIA专注于用户界面和交互设计,从此两个领域的分工更加明晰。正如简单的Java语言并不意味着高质量的Java程序,简单的HTTP协议也不意味着高质量的Web
API。要想设计出高质量的Web
API,还需要深入理解分布式系统及HTTP协议的特性。在HTTP1.1规范中定义幂等性。

     
  我们要解决的是更通用的问题:
如何以更不易出错的方式去表达和维护大型逻辑 ?

 

           
三种情境的复杂性均是由并发引起的。  假设所有操作都是串行进行的,
逻辑块的交互无非是“你方唱罢我登场”的次序控制,
而资源对单个请求通常是足够的;  一旦采用了并发方案,
就难以控制逻辑块的执行次序和资源分配的具体情况了,
容易导致某资源对单个请求不足的情况,
从而阻塞多个请求的处理甚至死锁。并发提升了应用的性能,
却增加了出错的风险和几率。并发控制是大型逻辑交互的本质性难点。并发控制的难点在于时序的合理控制和有效资源的合理分配。

**

       
软件开发工具让我们更有效率地创造逻辑、 远离语法错误的困扰;

create_ticket的语义是获取一个服务器端生成的唯一的处理号token,它将用于标识后续的操作。idempotent_withdraw和withdraw的区别在于关联了一个token,一个token表示的操作至多只会被处理一次,每次调用都将返回第一次调用时的处理结果。这样,idempotent_withdraw就符合幂等性了,客户端就可以放心地多次调用。也就是说,多次点击提交的时候,附带提交的还有服务端生成的token,由于多次提交带的是同一个token,所以服务端对于同一个token的post订单,至多只会处理一次,所以间接的实现了幂等性的控制。

     
     2. 检测值: 检测值是否合法, 通常是前置条件校验、
中间状态校验和后置结果校验,
根据检测结果执行“获取值”或“设置值”的逻辑;

1、什么是restful风格的API接口?

 

  在电商系统中,常见问题:如何防范post请求的重复提交?**HTTP
POST
操作既不是安全的,也不是幂等的。当我们因为反复刷新浏览器导致多次提交表单,多次发出同样的POST请求,导致远端服务器重复创建出了资源。**所以,对于电商应用来说,第一对应的后端
WebService 一定要做到幂等性,第二服务器端收到 POST
请求,在操作成功后必须跳转到另外一个页面,这样即使用户刷新页面,也不会重复提交表单

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website